|
1 /*! |
|
2 * jQuery JavaScript Library v2.1.1 |
|
3 * http://jquery.com/ |
|
4 * |
|
5 * Includes Sizzle.js |
|
6 * http://sizzlejs.com/ |
|
7 * |
|
8 * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors |
|
9 * Released under the MIT license |
|
10 * http://jquery.org/license |
|
11 * |
|
12 * Date: 2014-05-01T17:11Z |
|
13 */ |
|
14 |
|
15 (function( global, factory ) { |
|
16 |
|
17 if ( typeof module === "object" && typeof module.exports === "object" ) { |
|
18 // For CommonJS and CommonJS-like environments where a proper window is present, |
|
19 // execute the factory and get jQuery |
|
20 // For environments that do not inherently posses a window with a document |
|
21 // (such as Node.js), expose a jQuery-making factory as module.exports |
|
22 // This accentuates the need for the creation of a real window |
|
23 // e.g. var jQuery = require("jquery")(window); |
|
24 // See ticket #14549 for more info |
|
25 module.exports = global.document ? |
|
26 factory( global, true ) : |
|
27 function( w ) { |
|
28 if ( !w.document ) { |
|
29 throw new Error( "jQuery requires a window with a document" ); |
|
30 } |
|
31 return factory( w ); |
|
32 }; |
|
33 } else { |
|
34 factory( global ); |
|
35 } |
|
36 |
|
37 // Pass this if window is not defined yet |
|
38 }(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { |
|
39 |
|
40 // Can't do this because several apps including ASP.NET trace |
|
41 // the stack via arguments.caller.callee and Firefox dies if |
|
42 // you try to trace through "use strict" call chains. (#13335) |
|
43 // Support: Firefox 18+ |
|
44 // |
|
45 |
|
46 var arr = []; |
|
47 |
|
48 var slice = arr.slice; |
|
49 |
|
50 var concat = arr.concat; |
|
51 |
|
52 var push = arr.push; |
|
53 |
|
54 var indexOf = arr.indexOf; |
|
55 |
|
56 var class2type = {}; |
|
57 |
|
58 var toString = class2type.toString; |
|
59 |
|
60 var hasOwn = class2type.hasOwnProperty; |
|
61 |
|
62 var support = {}; |
|
63 |
|
64 |
|
65 |
|
66 var |
|
67 // Use the correct document accordingly with window argument (sandbox) |
|
68 document = window.document, |
|
69 |
|
70 version = "2.1.1", |
|
71 |
|
72 // Define a local copy of jQuery |
|
73 jQuery = function( selector, context ) { |
|
74 // The jQuery object is actually just the init constructor 'enhanced' |
|
75 // Need init if jQuery is called (just allow error to be thrown if not included) |
|
76 return new jQuery.fn.init( selector, context ); |
|
77 }, |
|
78 |
|
79 // Support: Android<4.1 |
|
80 // Make sure we trim BOM and NBSP |
|
81 rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, |
|
82 |
|
83 // Matches dashed string for camelizing |
|
84 rmsPrefix = /^-ms-/, |
|
85 rdashAlpha = /-([\da-z])/gi, |
|
86 |
|
87 // Used by jQuery.camelCase as callback to replace() |
|
88 fcamelCase = function( all, letter ) { |
|
89 return letter.toUpperCase(); |
|
90 }; |
|
91 |
|
92 jQuery.fn = jQuery.prototype = { |
|
93 // The current version of jQuery being used |
|
94 jquery: version, |
|
95 |
|
96 constructor: jQuery, |
|
97 |
|
98 // Start with an empty selector |
|
99 selector: "", |
|
100 |
|
101 // The default length of a jQuery object is 0 |
|
102 length: 0, |
|
103 |
|
104 toArray: function() { |
|
105 return slice.call( this ); |
|
106 }, |
|
107 |
|
108 // Get the Nth element in the matched element set OR |
|
109 // Get the whole matched element set as a clean array |
|
110 get: function( num ) { |
|
111 return num != null ? |
|
112 |
|
113 // Return just the one element from the set |
|
114 ( num < 0 ? this[ num + this.length ] : this[ num ] ) : |
|
115 |
|
116 // Return all the elements in a clean array |
|
117 slice.call( this ); |
|
118 }, |
|
119 |
|
120 // Take an array of elements and push it onto the stack |
|
121 // (returning the new matched element set) |
|
122 pushStack: function( elems ) { |
|
123 |
|
124 // Build a new jQuery matched element set |
|
125 var ret = jQuery.merge( this.constructor(), elems ); |
|
126 |
|
127 // Add the old object onto the stack (as a reference) |
|
128 ret.prevObject = this; |
|
129 ret.context = this.context; |
|
130 |
|
131 // Return the newly-formed element set |
|
132 return ret; |
|
133 }, |
|
134 |
|
135 // Execute a callback for every element in the matched set. |
|
136 // (You can seed the arguments with an array of args, but this is |
|
137 // only used internally.) |
|
138 each: function( callback, args ) { |
|
139 return jQuery.each( this, callback, args ); |
|
140 }, |
|
141 |
|
142 map: function( callback ) { |
|
143 return this.pushStack( jQuery.map(this, function( elem, i ) { |
|
144 return callback.call( elem, i, elem ); |
|
145 })); |
|
146 }, |
|
147 |
|
148 slice: function() { |
|
149 return this.pushStack( slice.apply( this, arguments ) ); |
|
150 }, |
|
151 |
|
152 first: function() { |
|
153 return this.eq( 0 ); |
|
154 }, |
|
155 |
|
156 last: function() { |
|
157 return this.eq( -1 ); |
|
158 }, |
|
159 |
|
160 eq: function( i ) { |
|
161 var len = this.length, |
|
162 j = +i + ( i < 0 ? len : 0 ); |
|
163 return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); |
|
164 }, |
|
165 |
|
166 end: function() { |
|
167 return this.prevObject || this.constructor(null); |
|
168 }, |
|
169 |
|
170 // For internal use only. |
|
171 // Behaves like an Array's method, not like a jQuery method. |
|
172 push: push, |
|
173 sort: arr.sort, |
|
174 splice: arr.splice |
|
175 }; |
|
176 |
|
177 jQuery.extend = jQuery.fn.extend = function() { |
|
178 var options, name, src, copy, copyIsArray, clone, |
|
179 target = arguments[0] || {}, |
|
180 i = 1, |
|
181 length = arguments.length, |
|
182 deep = false; |
|
183 |
|
184 // Handle a deep copy situation |
|
185 if ( typeof target === "boolean" ) { |
|
186 deep = target; |
|
187 |
|
188 // skip the boolean and the target |
|
189 target = arguments[ i ] || {}; |
|
190 i++; |
|
191 } |
|
192 |
|
193 // Handle case when target is a string or something (possible in deep copy) |
|
194 if ( typeof target !== "object" && !jQuery.isFunction(target) ) { |
|
195 target = {}; |
|
196 } |
|
197 |
|
198 // extend jQuery itself if only one argument is passed |
|
199 if ( i === length ) { |
|
200 target = this; |
|
201 i--; |
|
202 } |
|
203 |
|
204 for ( ; i < length; i++ ) { |
|
205 // Only deal with non-null/undefined values |
|
206 if ( (options = arguments[ i ]) != null ) { |
|
207 // Extend the base object |
|
208 for ( name in options ) { |
|
209 src = target[ name ]; |
|
210 copy = options[ name ]; |
|
211 |
|
212 // Prevent never-ending loop |
|
213 if ( target === copy ) { |
|
214 continue; |
|
215 } |
|
216 |
|
217 // Recurse if we're merging plain objects or arrays |
|
218 if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { |
|
219 if ( copyIsArray ) { |
|
220 copyIsArray = false; |
|
221 clone = src && jQuery.isArray(src) ? src : []; |
|
222 |
|
223 } else { |
|
224 clone = src && jQuery.isPlainObject(src) ? src : {}; |
|
225 } |
|
226 |
|
227 // Never move original objects, clone them |
|
228 target[ name ] = jQuery.extend( deep, clone, copy ); |
|
229 |
|
230 // Don't bring in undefined values |
|
231 } else if ( copy !== undefined ) { |
|
232 target[ name ] = copy; |
|
233 } |
|
234 } |
|
235 } |
|
236 } |
|
237 |
|
238 // Return the modified object |
|
239 return target; |
|
240 }; |
|
241 |
|
242 jQuery.extend({ |
|
243 // Unique for each copy of jQuery on the page |
|
244 expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), |
|
245 |
|
246 // Assume jQuery is ready without the ready module |
|
247 isReady: true, |
|
248 |
|
249 error: function( msg ) { |
|
250 throw new Error( msg ); |
|
251 }, |
|
252 |
|
253 noop: function() {}, |
|
254 |
|
255 // See test/unit/core.js for details concerning isFunction. |
|
256 // Since version 1.3, DOM methods and functions like alert |
|
257 // aren't supported. They return false on IE (#2968). |
|
258 isFunction: function( obj ) { |
|
259 return jQuery.type(obj) === "function"; |
|
260 }, |
|
261 |
|
262 isArray: Array.isArray, |
|
263 |
|
264 isWindow: function( obj ) { |
|
265 return obj != null && obj === obj.window; |
|
266 }, |
|
267 |
|
268 isNumeric: function( obj ) { |
|
269 // parseFloat NaNs numeric-cast false positives (null|true|false|"") |
|
270 // ...but misinterprets leading-number strings, particularly hex literals ("0x...") |
|
271 // subtraction forces infinities to NaN |
|
272 return !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0; |
|
273 }, |
|
274 |
|
275 isPlainObject: function( obj ) { |
|
276 // Not plain objects: |
|
277 // - Any object or value whose internal [[Class]] property is not "[object Object]" |
|
278 // - DOM nodes |
|
279 // - window |
|
280 if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { |
|
281 return false; |
|
282 } |
|
283 |
|
284 if ( obj.constructor && |
|
285 !hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) { |
|
286 return false; |
|
287 } |
|
288 |
|
289 // If the function hasn't returned already, we're confident that |
|
290 // |obj| is a plain object, created by {} or constructed with new Object |
|
291 return true; |
|
292 }, |
|
293 |
|
294 isEmptyObject: function( obj ) { |
|
295 var name; |
|
296 for ( name in obj ) { |
|
297 return false; |
|
298 } |
|
299 return true; |
|
300 }, |
|
301 |
|
302 type: function( obj ) { |
|
303 if ( obj == null ) { |
|
304 return obj + ""; |
|
305 } |
|
306 // Support: Android < 4.0, iOS < 6 (functionish RegExp) |
|
307 return typeof obj === "object" || typeof obj === "function" ? |
|
308 class2type[ toString.call(obj) ] || "object" : |
|
309 typeof obj; |
|
310 }, |
|
311 |
|
312 // Evaluates a script in a global context |
|
313 globalEval: function( code ) { |
|
314 var script, |
|
315 indirect = eval; |
|
316 |
|
317 code = jQuery.trim( code ); |
|
318 |
|
319 if ( code ) { |
|
320 // If the code includes a valid, prologue position |
|
321 // strict mode pragma, execute code by injecting a |
|
322 // script tag into the document. |
|
323 if ( code.indexOf("use strict") === 1 ) { |
|
324 script = document.createElement("script"); |
|
325 script.text = code; |
|
326 document.head.appendChild( script ).parentNode.removeChild( script ); |
|
327 } else { |
|
328 // Otherwise, avoid the DOM node creation, insertion |
|
329 // and removal by using an indirect global eval |
|
330 indirect( code ); |
|
331 } |
|
332 } |
|
333 }, |
|
334 |
|
335 // Convert dashed to camelCase; used by the css and data modules |
|
336 // Microsoft forgot to hump their vendor prefix (#9572) |
|
337 camelCase: function( string ) { |
|
338 return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); |
|
339 }, |
|
340 |
|
341 nodeName: function( elem, name ) { |
|
342 return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); |
|
343 }, |
|
344 |
|
345 // args is for internal usage only |
|
346 each: function( obj, callback, args ) { |
|
347 var value, |
|
348 i = 0, |
|
349 length = obj.length, |
|
350 isArray = isArraylike( obj ); |
|
351 |
|
352 if ( args ) { |
|
353 if ( isArray ) { |
|
354 for ( ; i < length; i++ ) { |
|
355 value = callback.apply( obj[ i ], args ); |
|
356 |
|
357 if ( value === false ) { |
|
358 break; |
|
359 } |
|
360 } |
|
361 } else { |
|
362 for ( i in obj ) { |
|
363 value = callback.apply( obj[ i ], args ); |
|
364 |
|
365 if ( value === false ) { |
|
366 break; |
|
367 } |
|
368 } |
|
369 } |
|
370 |
|
371 // A special, fast, case for the most common use of each |
|
372 } else { |
|
373 if ( isArray ) { |
|
374 for ( ; i < length; i++ ) { |
|
375 value = callback.call( obj[ i ], i, obj[ i ] ); |
|
376 |
|
377 if ( value === false ) { |
|
378 break; |
|
379 } |
|
380 } |
|
381 } else { |
|
382 for ( i in obj ) { |
|
383 value = callback.call( obj[ i ], i, obj[ i ] ); |
|
384 |
|
385 if ( value === false ) { |
|
386 break; |
|
387 } |
|
388 } |
|
389 } |
|
390 } |
|
391 |
|
392 return obj; |
|
393 }, |
|
394 |
|
395 // Support: Android<4.1 |
|
396 trim: function( text ) { |
|
397 return text == null ? |
|
398 "" : |
|
399 ( text + "" ).replace( rtrim, "" ); |
|
400 }, |
|
401 |
|
402 // results is for internal usage only |
|
403 makeArray: function( arr, results ) { |
|
404 var ret = results || []; |
|
405 |
|
406 if ( arr != null ) { |
|
407 if ( isArraylike( Object(arr) ) ) { |
|
408 jQuery.merge( ret, |
|
409 typeof arr === "string" ? |
|
410 [ arr ] : arr |
|
411 ); |
|
412 } else { |
|
413 push.call( ret, arr ); |
|
414 } |
|
415 } |
|
416 |
|
417 return ret; |
|
418 }, |
|
419 |
|
420 inArray: function( elem, arr, i ) { |
|
421 return arr == null ? -1 : indexOf.call( arr, elem, i ); |
|
422 }, |
|
423 |
|
424 merge: function( first, second ) { |
|
425 var len = +second.length, |
|
426 j = 0, |
|
427 i = first.length; |
|
428 |
|
429 for ( ; j < len; j++ ) { |
|
430 first[ i++ ] = second[ j ]; |
|
431 } |
|
432 |
|
433 first.length = i; |
|
434 |
|
435 return first; |
|
436 }, |
|
437 |
|
438 grep: function( elems, callback, invert ) { |
|
439 var callbackInverse, |
|
440 matches = [], |
|
441 i = 0, |
|
442 length = elems.length, |
|
443 callbackExpect = !invert; |
|
444 |
|
445 // Go through the array, only saving the items |
|
446 // that pass the validator function |
|
447 for ( ; i < length; i++ ) { |
|
448 callbackInverse = !callback( elems[ i ], i ); |
|
449 if ( callbackInverse !== callbackExpect ) { |
|
450 matches.push( elems[ i ] ); |
|
451 } |
|
452 } |
|
453 |
|
454 return matches; |
|
455 }, |
|
456 |
|
457 // arg is for internal usage only |
|
458 map: function( elems, callback, arg ) { |
|
459 var value, |
|
460 i = 0, |
|
461 length = elems.length, |
|
462 isArray = isArraylike( elems ), |
|
463 ret = []; |
|
464 |
|
465 // Go through the array, translating each of the items to their new values |
|
466 if ( isArray ) { |
|
467 for ( ; i < length; i++ ) { |
|
468 value = callback( elems[ i ], i, arg ); |
|
469 |
|
470 if ( value != null ) { |
|
471 ret.push( value ); |
|
472 } |
|
473 } |
|
474 |
|
475 // Go through every key on the object, |
|
476 } else { |
|
477 for ( i in elems ) { |
|
478 value = callback( elems[ i ], i, arg ); |
|
479 |
|
480 if ( value != null ) { |
|
481 ret.push( value ); |
|
482 } |
|
483 } |
|
484 } |
|
485 |
|
486 // Flatten any nested arrays |
|
487 return concat.apply( [], ret ); |
|
488 }, |
|
489 |
|
490 // A global GUID counter for objects |
|
491 guid: 1, |
|
492 |
|
493 // Bind a function to a context, optionally partially applying any |
|
494 // arguments. |
|
495 proxy: function( fn, context ) { |
|
496 var tmp, args, proxy; |
|
497 |
|
498 if ( typeof context === "string" ) { |
|
499 tmp = fn[ context ]; |
|
500 context = fn; |
|
501 fn = tmp; |
|
502 } |
|
503 |
|
504 // Quick check to determine if target is callable, in the spec |
|
505 // this throws a TypeError, but we will just return undefined. |
|
506 if ( !jQuery.isFunction( fn ) ) { |
|
507 return undefined; |
|
508 } |
|
509 |
|
510 // Simulated bind |
|
511 args = slice.call( arguments, 2 ); |
|
512 proxy = function() { |
|
513 return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); |
|
514 }; |
|
515 |
|
516 // Set the guid of unique handler to the same of original handler, so it can be removed |
|
517 proxy.guid = fn.guid = fn.guid || jQuery.guid++; |
|
518 |
|
519 return proxy; |
|
520 }, |
|
521 |
|
522 now: Date.now, |
|
523 |
|
524 // jQuery.support is not used in Core but other projects attach their |
|
525 // properties to it so it needs to exist. |
|
526 support: support |
|
527 }); |
|
528 |
|
529 // Populate the class2type map |
|
530 jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { |
|
531 class2type[ "[object " + name + "]" ] = name.toLowerCase(); |
|
532 }); |
|
533 |
|
534 function isArraylike( obj ) { |
|
535 var length = obj.length, |
|
536 type = jQuery.type( obj ); |
|
537 |
|
538 if ( type === "function" || jQuery.isWindow( obj ) ) { |
|
539 return false; |
|
540 } |
|
541 |
|
542 if ( obj.nodeType === 1 && length ) { |
|
543 return true; |
|
544 } |
|
545 |
|
546 return type === "array" || length === 0 || |
|
547 typeof length === "number" && length > 0 && ( length - 1 ) in obj; |
|
548 } |
|
549 var Sizzle = |
|
550 /*! |
|
551 * Sizzle CSS Selector Engine v1.10.19 |
|
552 * http://sizzlejs.com/ |
|
553 * |
|
554 * Copyright 2013 jQuery Foundation, Inc. and other contributors |
|
555 * Released under the MIT license |
|
556 * http://jquery.org/license |
|
557 * |
|
558 * Date: 2014-04-18 |
|
559 */ |
|
560 (function( window ) { |
|
561 |
|
562 var i, |
|
563 support, |
|
564 Expr, |
|
565 getText, |
|
566 isXML, |
|
567 tokenize, |
|
568 compile, |
|
569 select, |
|
570 outermostContext, |
|
571 sortInput, |
|
572 hasDuplicate, |
|
573 |
|
574 // Local document vars |
|
575 setDocument, |
|
576 document, |
|
577 docElem, |
|
578 documentIsHTML, |
|
579 rbuggyQSA, |
|
580 rbuggyMatches, |
|
581 matches, |
|
582 contains, |
|
583 |
|
584 // Instance-specific data |
|
585 expando = "sizzle" + -(new Date()), |
|
586 preferredDoc = window.document, |
|
587 dirruns = 0, |
|
588 done = 0, |
|
589 classCache = createCache(), |
|
590 tokenCache = createCache(), |
|
591 compilerCache = createCache(), |
|
592 sortOrder = function( a, b ) { |
|
593 if ( a === b ) { |
|
594 hasDuplicate = true; |
|
595 } |
|
596 return 0; |
|
597 }, |
|
598 |
|
599 // General-purpose constants |
|
600 strundefined = typeof undefined, |
|
601 MAX_NEGATIVE = 1 << 31, |
|
602 |
|
603 // Instance methods |
|
604 hasOwn = ({}).hasOwnProperty, |
|
605 arr = [], |
|
606 pop = arr.pop, |
|
607 push_native = arr.push, |
|
608 push = arr.push, |
|
609 slice = arr.slice, |
|
610 // Use a stripped-down indexOf if we can't use a native one |
|
611 indexOf = arr.indexOf || function( elem ) { |
|
612 var i = 0, |
|
613 len = this.length; |
|
614 for ( ; i < len; i++ ) { |
|
615 if ( this[i] === elem ) { |
|
616 return i; |
|
617 } |
|
618 } |
|
619 return -1; |
|
620 }, |
|
621 |
|
622 booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", |
|
623 |
|
624 // Regular expressions |
|
625 |
|
626 // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace |
|
627 whitespace = "[\\x20\\t\\r\\n\\f]", |
|
628 // http://www.w3.org/TR/css3-syntax/#characters |
|
629 characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", |
|
630 |
|
631 // Loosely modeled on CSS identifier characters |
|
632 // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors |
|
633 // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier |
|
634 identifier = characterEncoding.replace( "w", "w#" ), |
|
635 |
|
636 // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors |
|
637 attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace + |
|
638 // Operator (capture 2) |
|
639 "*([*^$|!~]?=)" + whitespace + |
|
640 // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" |
|
641 "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + |
|
642 "*\\]", |
|
643 |
|
644 pseudos = ":(" + characterEncoding + ")(?:\\((" + |
|
645 // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: |
|
646 // 1. quoted (capture 3; capture 4 or capture 5) |
|
647 "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + |
|
648 // 2. simple (capture 6) |
|
649 "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + |
|
650 // 3. anything else (capture 2) |
|
651 ".*" + |
|
652 ")\\)|)", |
|
653 |
|
654 // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter |
|
655 rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), |
|
656 |
|
657 rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), |
|
658 rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), |
|
659 |
|
660 rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), |
|
661 |
|
662 rpseudo = new RegExp( pseudos ), |
|
663 ridentifier = new RegExp( "^" + identifier + "$" ), |
|
664 |
|
665 matchExpr = { |
|
666 "ID": new RegExp( "^#(" + characterEncoding + ")" ), |
|
667 "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), |
|
668 "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), |
|
669 "ATTR": new RegExp( "^" + attributes ), |
|
670 "PSEUDO": new RegExp( "^" + pseudos ), |
|
671 "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + |
|
672 "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + |
|
673 "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), |
|
674 "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), |
|
675 // For use in libraries implementing .is() |
|
676 // We use this for POS matching in `select` |
|
677 "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + |
|
678 whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) |
|
679 }, |
|
680 |
|
681 rinputs = /^(?:input|select|textarea|button)$/i, |
|
682 rheader = /^h\d$/i, |
|
683 |
|
684 rnative = /^[^{]+\{\s*\[native \w/, |
|
685 |
|
686 // Easily-parseable/retrievable ID or TAG or CLASS selectors |
|
687 rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, |
|
688 |
|
689 rsibling = /[+~]/, |
|
690 rescape = /'|\\/g, |
|
691 |
|
692 // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters |
|
693 runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), |
|
694 funescape = function( _, escaped, escapedWhitespace ) { |
|
695 var high = "0x" + escaped - 0x10000; |
|
696 // NaN means non-codepoint |
|
697 // Support: Firefox<24 |
|
698 // Workaround erroneous numeric interpretation of +"0x" |
|
699 return high !== high || escapedWhitespace ? |
|
700 escaped : |
|
701 high < 0 ? |
|
702 // BMP codepoint |
|
703 String.fromCharCode( high + 0x10000 ) : |
|
704 // Supplemental Plane codepoint (surrogate pair) |
|
705 String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); |
|
706 }; |
|
707 |
|
708 // Optimize for push.apply( _, NodeList ) |
|
709 try { |
|
710 push.apply( |
|
711 (arr = slice.call( preferredDoc.childNodes )), |
|
712 preferredDoc.childNodes |
|
713 ); |
|
714 // Support: Android<4.0 |
|
715 // Detect silently failing push.apply |
|
716 arr[ preferredDoc.childNodes.length ].nodeType; |
|
717 } catch ( e ) { |
|
718 push = { apply: arr.length ? |
|
719 |
|
720 // Leverage slice if possible |
|
721 function( target, els ) { |
|
722 push_native.apply( target, slice.call(els) ); |
|
723 } : |
|
724 |
|
725 // Support: IE<9 |
|
726 // Otherwise append directly |
|
727 function( target, els ) { |
|
728 var j = target.length, |
|
729 i = 0; |
|
730 // Can't trust NodeList.length |
|
731 while ( (target[j++] = els[i++]) ) {} |
|
732 target.length = j - 1; |
|
733 } |
|
734 }; |
|
735 } |
|
736 |
|
737 function Sizzle( selector, context, results, seed ) { |
|
738 var match, elem, m, nodeType, |
|
739 // QSA vars |
|
740 i, groups, old, nid, newContext, newSelector; |
|
741 |
|
742 if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { |
|
743 setDocument( context ); |
|
744 } |
|
745 |
|
746 context = context || document; |
|
747 results = results || []; |
|
748 |
|
749 if ( !selector || typeof selector !== "string" ) { |
|
750 return results; |
|
751 } |
|
752 |
|
753 if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { |
|
754 return []; |
|
755 } |
|
756 |
|
757 if ( documentIsHTML && !seed ) { |
|
758 |
|
759 // Shortcuts |
|
760 if ( (match = rquickExpr.exec( selector )) ) { |
|
761 // Speed-up: Sizzle("#ID") |
|
762 if ( (m = match[1]) ) { |
|
763 if ( nodeType === 9 ) { |
|
764 elem = context.getElementById( m ); |
|
765 // Check parentNode to catch when Blackberry 4.6 returns |
|
766 // nodes that are no longer in the document (jQuery #6963) |
|
767 if ( elem && elem.parentNode ) { |
|
768 // Handle the case where IE, Opera, and Webkit return items |
|
769 // by name instead of ID |
|
770 if ( elem.id === m ) { |
|
771 results.push( elem ); |
|
772 return results; |
|
773 } |
|
774 } else { |
|
775 return results; |
|
776 } |
|
777 } else { |
|
778 // Context is not a document |
|
779 if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && |
|
780 contains( context, elem ) && elem.id === m ) { |
|
781 results.push( elem ); |
|
782 return results; |
|
783 } |
|
784 } |
|
785 |
|
786 // Speed-up: Sizzle("TAG") |
|
787 } else if ( match[2] ) { |
|
788 push.apply( results, context.getElementsByTagName( selector ) ); |
|
789 return results; |
|
790 |
|
791 // Speed-up: Sizzle(".CLASS") |
|
792 } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { |
|
793 push.apply( results, context.getElementsByClassName( m ) ); |
|
794 return results; |
|
795 } |
|
796 } |
|
797 |
|
798 // QSA path |
|
799 if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { |
|
800 nid = old = expando; |
|
801 newContext = context; |
|
802 newSelector = nodeType === 9 && selector; |
|
803 |
|
804 // qSA works strangely on Element-rooted queries |
|
805 // We can work around this by specifying an extra ID on the root |
|
806 // and working up from there (Thanks to Andrew Dupont for the technique) |
|
807 // IE 8 doesn't work on object elements |
|
808 if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { |
|
809 groups = tokenize( selector ); |
|
810 |
|
811 if ( (old = context.getAttribute("id")) ) { |
|
812 nid = old.replace( rescape, "\\$&" ); |
|
813 } else { |
|
814 context.setAttribute( "id", nid ); |
|
815 } |
|
816 nid = "[id='" + nid + "'] "; |
|
817 |
|
818 i = groups.length; |
|
819 while ( i-- ) { |
|
820 groups[i] = nid + toSelector( groups[i] ); |
|
821 } |
|
822 newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; |
|
823 newSelector = groups.join(","); |
|
824 } |
|
825 |
|
826 if ( newSelector ) { |
|
827 try { |
|
828 push.apply( results, |
|
829 newContext.querySelectorAll( newSelector ) |
|
830 ); |
|
831 return results; |
|
832 } catch(qsaError) { |
|
833 } finally { |
|
834 if ( !old ) { |
|
835 context.removeAttribute("id"); |
|
836 } |
|
837 } |
|
838 } |
|
839 } |
|
840 } |
|
841 |
|
842 // All others |
|
843 return select( selector.replace( rtrim, "$1" ), context, results, seed ); |
|
844 } |
|
845 |
|
846 /** |
|
847 * Create key-value caches of limited size |
|
848 * @returns {Function(string, Object)} Returns the Object data after storing it on itself with |
|
849 * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) |
|
850 * deleting the oldest entry |
|
851 */ |
|
852 function createCache() { |
|
853 var keys = []; |
|
854 |
|
855 function cache( key, value ) { |
|
856 // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) |
|
857 if ( keys.push( key + " " ) > Expr.cacheLength ) { |
|
858 // Only keep the most recent entries |
|
859 delete cache[ keys.shift() ]; |
|
860 } |
|
861 return (cache[ key + " " ] = value); |
|
862 } |
|
863 return cache; |
|
864 } |
|
865 |
|
866 /** |
|
867 * Mark a function for special use by Sizzle |
|
868 * @param {Function} fn The function to mark |
|
869 */ |
|
870 function markFunction( fn ) { |
|
871 fn[ expando ] = true; |
|
872 return fn; |
|
873 } |
|
874 |
|
875 /** |
|
876 * Support testing using an element |
|
877 * @param {Function} fn Passed the created div and expects a boolean result |
|
878 */ |
|
879 function assert( fn ) { |
|
880 var div = document.createElement("div"); |
|
881 |
|
882 try { |
|
883 return !!fn( div ); |
|
884 } catch (e) { |
|
885 return false; |
|
886 } finally { |
|
887 // Remove from its parent by default |
|
888 if ( div.parentNode ) { |
|
889 div.parentNode.removeChild( div ); |
|
890 } |
|
891 // release memory in IE |
|
892 div = null; |
|
893 } |
|
894 } |
|
895 |
|
896 /** |
|
897 * Adds the same handler for all of the specified attrs |
|
898 * @param {String} attrs Pipe-separated list of attributes |
|
899 * @param {Function} handler The method that will be applied |
|
900 */ |
|
901 function addHandle( attrs, handler ) { |
|
902 var arr = attrs.split("|"), |
|
903 i = attrs.length; |
|
904 |
|
905 while ( i-- ) { |
|
906 Expr.attrHandle[ arr[i] ] = handler; |
|
907 } |
|
908 } |
|
909 |
|
910 /** |
|
911 * Checks document order of two siblings |
|
912 * @param {Element} a |
|
913 * @param {Element} b |
|
914 * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b |
|
915 */ |
|
916 function siblingCheck( a, b ) { |
|
917 var cur = b && a, |
|
918 diff = cur && a.nodeType === 1 && b.nodeType === 1 && |
|
919 ( ~b.sourceIndex || MAX_NEGATIVE ) - |
|
920 ( ~a.sourceIndex || MAX_NEGATIVE ); |
|
921 |
|
922 // Use IE sourceIndex if available on both nodes |
|
923 if ( diff ) { |
|
924 return diff; |
|
925 } |
|
926 |
|
927 // Check if b follows a |
|
928 if ( cur ) { |
|
929 while ( (cur = cur.nextSibling) ) { |
|
930 if ( cur === b ) { |
|
931 return -1; |
|
932 } |
|
933 } |
|
934 } |
|
935 |
|
936 return a ? 1 : -1; |
|
937 } |
|
938 |
|
939 /** |
|
940 * Returns a function to use in pseudos for input types |
|
941 * @param {String} type |
|
942 */ |
|
943 function createInputPseudo( type ) { |
|
944 return function( elem ) { |
|
945 var name = elem.nodeName.toLowerCase(); |
|
946 return name === "input" && elem.type === type; |
|
947 }; |
|
948 } |
|
949 |
|
950 /** |
|
951 * Returns a function to use in pseudos for buttons |
|
952 * @param {String} type |
|
953 */ |
|
954 function createButtonPseudo( type ) { |
|
955 return function( elem ) { |
|
956 var name = elem.nodeName.toLowerCase(); |
|
957 return (name === "input" || name === "button") && elem.type === type; |
|
958 }; |
|
959 } |
|
960 |
|
961 /** |
|
962 * Returns a function to use in pseudos for positionals |
|
963 * @param {Function} fn |
|
964 */ |
|
965 function createPositionalPseudo( fn ) { |
|
966 return markFunction(function( argument ) { |
|
967 argument = +argument; |
|
968 return markFunction(function( seed, matches ) { |
|
969 var j, |
|
970 matchIndexes = fn( [], seed.length, argument ), |
|
971 i = matchIndexes.length; |
|
972 |
|
973 // Match elements found at the specified indexes |
|
974 while ( i-- ) { |
|
975 if ( seed[ (j = matchIndexes[i]) ] ) { |
|
976 seed[j] = !(matches[j] = seed[j]); |
|
977 } |
|
978 } |
|
979 }); |
|
980 }); |
|
981 } |
|
982 |
|
983 /** |
|
984 * Checks a node for validity as a Sizzle context |
|
985 * @param {Element|Object=} context |
|
986 * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value |
|
987 */ |
|
988 function testContext( context ) { |
|
989 return context && typeof context.getElementsByTagName !== strundefined && context; |
|
990 } |
|
991 |
|
992 // Expose support vars for convenience |
|
993 support = Sizzle.support = {}; |
|
994 |
|
995 /** |
|
996 * Detects XML nodes |
|
997 * @param {Element|Object} elem An element or a document |
|
998 * @returns {Boolean} True iff elem is a non-HTML XML node |
|
999 */ |
|
1000 isXML = Sizzle.isXML = function( elem ) { |
|
1001 // documentElement is verified for cases where it doesn't yet exist |
|
1002 // (such as loading iframes in IE - #4833) |
|
1003 var documentElement = elem && (elem.ownerDocument || elem).documentElement; |
|
1004 return documentElement ? documentElement.nodeName !== "HTML" : false; |
|
1005 }; |
|
1006 |
|
1007 /** |
|
1008 * Sets document-related variables once based on the current document |
|
1009 * @param {Element|Object} [doc] An element or document object to use to set the document |
|
1010 * @returns {Object} Returns the current document |
|
1011 */ |
|
1012 setDocument = Sizzle.setDocument = function( node ) { |
|
1013 var hasCompare, |
|
1014 doc = node ? node.ownerDocument || node : preferredDoc, |
|
1015 parent = doc.defaultView; |
|
1016 |
|
1017 // If no document and documentElement is available, return |
|
1018 if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { |
|
1019 return document; |
|
1020 } |
|
1021 |
|
1022 // Set our document |
|
1023 document = doc; |
|
1024 docElem = doc.documentElement; |
|
1025 |
|
1026 // Support tests |
|
1027 documentIsHTML = !isXML( doc ); |
|
1028 |
|
1029 // Support: IE>8 |
|
1030 // If iframe document is assigned to "document" variable and if iframe has been reloaded, |
|
1031 // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 |
|
1032 // IE6-8 do not support the defaultView property so parent will be undefined |
|
1033 if ( parent && parent !== parent.top ) { |
|
1034 // IE11 does not have attachEvent, so all must suffer |
|
1035 if ( parent.addEventListener ) { |
|
1036 parent.addEventListener( "unload", function() { |
|
1037 setDocument(); |
|
1038 }, false ); |
|
1039 } else if ( parent.attachEvent ) { |
|
1040 parent.attachEvent( "onunload", function() { |
|
1041 setDocument(); |
|
1042 }); |
|
1043 } |
|
1044 } |
|
1045 |
|
1046 /* Attributes |
|
1047 ---------------------------------------------------------------------- */ |
|
1048 |
|
1049 // Support: IE<8 |
|
1050 // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) |
|
1051 support.attributes = assert(function( div ) { |
|
1052 div.className = "i"; |
|
1053 return !div.getAttribute("className"); |
|
1054 }); |
|
1055 |
|
1056 /* getElement(s)By* |
|
1057 ---------------------------------------------------------------------- */ |
|
1058 |
|
1059 // Check if getElementsByTagName("*") returns only elements |
|
1060 support.getElementsByTagName = assert(function( div ) { |
|
1061 div.appendChild( doc.createComment("") ); |
|
1062 return !div.getElementsByTagName("*").length; |
|
1063 }); |
|
1064 |
|
1065 // Check if getElementsByClassName can be trusted |
|
1066 support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) { |
|
1067 div.innerHTML = "<div class='a'></div><div class='a i'></div>"; |
|
1068 |
|
1069 // Support: Safari<4 |
|
1070 // Catch class over-caching |
|
1071 div.firstChild.className = "i"; |
|
1072 // Support: Opera<10 |
|
1073 // Catch gEBCN failure to find non-leading classes |
|
1074 return div.getElementsByClassName("i").length === 2; |
|
1075 }); |
|
1076 |
|
1077 // Support: IE<10 |
|
1078 // Check if getElementById returns elements by name |
|
1079 // The broken getElementById methods don't pick up programatically-set names, |
|
1080 // so use a roundabout getElementsByName test |
|
1081 support.getById = assert(function( div ) { |
|
1082 docElem.appendChild( div ).id = expando; |
|
1083 return !doc.getElementsByName || !doc.getElementsByName( expando ).length; |
|
1084 }); |
|
1085 |
|
1086 // ID find and filter |
|
1087 if ( support.getById ) { |
|
1088 Expr.find["ID"] = function( id, context ) { |
|
1089 if ( typeof context.getElementById !== strundefined && documentIsHTML ) { |
|
1090 var m = context.getElementById( id ); |
|
1091 // Check parentNode to catch when Blackberry 4.6 returns |
|
1092 // nodes that are no longer in the document #6963 |
|
1093 return m && m.parentNode ? [ m ] : []; |
|
1094 } |
|
1095 }; |
|
1096 Expr.filter["ID"] = function( id ) { |
|
1097 var attrId = id.replace( runescape, funescape ); |
|
1098 return function( elem ) { |
|
1099 return elem.getAttribute("id") === attrId; |
|
1100 }; |
|
1101 }; |
|
1102 } else { |
|
1103 // Support: IE6/7 |
|
1104 // getElementById is not reliable as a find shortcut |
|
1105 delete Expr.find["ID"]; |
|
1106 |
|
1107 Expr.filter["ID"] = function( id ) { |
|
1108 var attrId = id.replace( runescape, funescape ); |
|
1109 return function( elem ) { |
|
1110 var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); |
|
1111 return node && node.value === attrId; |
|
1112 }; |
|
1113 }; |
|
1114 } |
|
1115 |
|
1116 // Tag |
|
1117 Expr.find["TAG"] = support.getElementsByTagName ? |
|
1118 function( tag, context ) { |
|
1119 if ( typeof context.getElementsByTagName !== strundefined ) { |
|
1120 return context.getElementsByTagName( tag ); |
|
1121 } |
|
1122 } : |
|
1123 function( tag, context ) { |
|
1124 var elem, |
|
1125 tmp = [], |
|
1126 i = 0, |
|
1127 results = context.getElementsByTagName( tag ); |
|
1128 |
|
1129 // Filter out possible comments |
|
1130 if ( tag === "*" ) { |
|
1131 while ( (elem = results[i++]) ) { |
|
1132 if ( elem.nodeType === 1 ) { |
|
1133 tmp.push( elem ); |
|
1134 } |
|
1135 } |
|
1136 |
|
1137 return tmp; |
|
1138 } |
|
1139 return results; |
|
1140 }; |
|
1141 |
|
1142 // Class |
|
1143 Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { |
|
1144 if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { |
|
1145 return context.getElementsByClassName( className ); |
|
1146 } |
|
1147 }; |
|
1148 |
|
1149 /* QSA/matchesSelector |
|
1150 ---------------------------------------------------------------------- */ |
|
1151 |
|
1152 // QSA and matchesSelector support |
|
1153 |
|
1154 // matchesSelector(:active) reports false when true (IE9/Opera 11.5) |
|
1155 rbuggyMatches = []; |
|
1156 |
|
1157 // qSa(:focus) reports false when true (Chrome 21) |
|
1158 // We allow this because of a bug in IE8/9 that throws an error |
|
1159 // whenever `document.activeElement` is accessed on an iframe |
|
1160 // So, we allow :focus to pass through QSA all the time to avoid the IE error |
|
1161 // See http://bugs.jquery.com/ticket/13378 |
|
1162 rbuggyQSA = []; |
|
1163 |
|
1164 if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { |
|
1165 // Build QSA regex |
|
1166 // Regex strategy adopted from Diego Perini |
|
1167 assert(function( div ) { |
|
1168 // Select is set to empty string on purpose |
|
1169 // This is to test IE's treatment of not explicitly |
|
1170 // setting a boolean content attribute, |
|
1171 // since its presence should be enough |
|
1172 // http://bugs.jquery.com/ticket/12359 |
|
1173 div.innerHTML = "<select msallowclip=''><option selected=''></option></select>"; |
|
1174 |
|
1175 // Support: IE8, Opera 11-12.16 |
|
1176 // Nothing should be selected when empty strings follow ^= or $= or *= |
|
1177 // The test attribute must be unknown in Opera but "safe" for WinRT |
|
1178 // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section |
|
1179 if ( div.querySelectorAll("[msallowclip^='']").length ) { |
|
1180 rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); |
|
1181 } |
|
1182 |
|
1183 // Support: IE8 |
|
1184 // Boolean attributes and "value" are not treated correctly |
|
1185 if ( !div.querySelectorAll("[selected]").length ) { |
|
1186 rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); |
|
1187 } |
|
1188 |
|
1189 // Webkit/Opera - :checked should return selected option elements |
|
1190 // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked |
|
1191 // IE8 throws error here and will not see later tests |
|
1192 if ( !div.querySelectorAll(":checked").length ) { |
|
1193 rbuggyQSA.push(":checked"); |
|
1194 } |
|
1195 }); |
|
1196 |
|
1197 assert(function( div ) { |
|
1198 // Support: Windows 8 Native Apps |
|
1199 // The type and name attributes are restricted during .innerHTML assignment |
|
1200 var input = doc.createElement("input"); |
|
1201 input.setAttribute( "type", "hidden" ); |
|
1202 div.appendChild( input ).setAttribute( "name", "D" ); |
|
1203 |
|
1204 // Support: IE8 |
|
1205 // Enforce case-sensitivity of name attribute |
|
1206 if ( div.querySelectorAll("[name=d]").length ) { |
|
1207 rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); |
|
1208 } |
|
1209 |
|
1210 // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) |
|
1211 // IE8 throws error here and will not see later tests |
|
1212 if ( !div.querySelectorAll(":enabled").length ) { |
|
1213 rbuggyQSA.push( ":enabled", ":disabled" ); |
|
1214 } |
|
1215 |
|
1216 // Opera 10-11 does not throw on post-comma invalid pseudos |
|
1217 div.querySelectorAll("*,:x"); |
|
1218 rbuggyQSA.push(",.*:"); |
|
1219 }); |
|
1220 } |
|
1221 |
|
1222 if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || |
|
1223 docElem.webkitMatchesSelector || |
|
1224 docElem.mozMatchesSelector || |
|
1225 docElem.oMatchesSelector || |
|
1226 docElem.msMatchesSelector) )) ) { |
|
1227 |
|
1228 assert(function( div ) { |
|
1229 // Check to see if it's possible to do matchesSelector |
|
1230 // on a disconnected node (IE 9) |
|
1231 support.disconnectedMatch = matches.call( div, "div" ); |
|
1232 |
|
1233 // This should fail with an exception |
|
1234 // Gecko does not error, returns false instead |
|
1235 matches.call( div, "[s!='']:x" ); |
|
1236 rbuggyMatches.push( "!=", pseudos ); |
|
1237 }); |
|
1238 } |
|
1239 |
|
1240 rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); |
|
1241 rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); |
|
1242 |
|
1243 /* Contains |
|
1244 ---------------------------------------------------------------------- */ |
|
1245 hasCompare = rnative.test( docElem.compareDocumentPosition ); |
|
1246 |
|
1247 // Element contains another |
|
1248 // Purposefully does not implement inclusive descendent |
|
1249 // As in, an element does not contain itself |
|
1250 contains = hasCompare || rnative.test( docElem.contains ) ? |
|
1251 function( a, b ) { |
|
1252 var adown = a.nodeType === 9 ? a.documentElement : a, |
|
1253 bup = b && b.parentNode; |
|
1254 return a === bup || !!( bup && bup.nodeType === 1 && ( |
|
1255 adown.contains ? |
|
1256 adown.contains( bup ) : |
|
1257 a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 |
|
1258 )); |
|
1259 } : |
|
1260 function( a, b ) { |
|
1261 if ( b ) { |
|
1262 while ( (b = b.parentNode) ) { |
|
1263 if ( b === a ) { |
|
1264 return true; |
|
1265 } |
|
1266 } |
|
1267 } |
|
1268 return false; |
|
1269 }; |
|
1270 |
|
1271 /* Sorting |
|
1272 ---------------------------------------------------------------------- */ |
|
1273 |
|
1274 // Document order sorting |
|
1275 sortOrder = hasCompare ? |
|
1276 function( a, b ) { |
|
1277 |
|
1278 // Flag for duplicate removal |
|
1279 if ( a === b ) { |
|
1280 hasDuplicate = true; |
|
1281 return 0; |
|
1282 } |
|
1283 |
|
1284 // Sort on method existence if only one input has compareDocumentPosition |
|
1285 var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; |
|
1286 if ( compare ) { |
|
1287 return compare; |
|
1288 } |
|
1289 |
|
1290 // Calculate position if both inputs belong to the same document |
|
1291 compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? |
|
1292 a.compareDocumentPosition( b ) : |
|
1293 |
|
1294 // Otherwise we know they are disconnected |
|
1295 1; |
|
1296 |
|
1297 // Disconnected nodes |
|
1298 if ( compare & 1 || |
|
1299 (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { |
|
1300 |
|
1301 // Choose the first element that is related to our preferred document |
|
1302 if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { |
|
1303 return -1; |
|
1304 } |
|
1305 if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { |
|
1306 return 1; |
|
1307 } |
|
1308 |
|
1309 // Maintain original order |
|
1310 return sortInput ? |
|
1311 ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : |
|
1312 0; |
|
1313 } |
|
1314 |
|
1315 return compare & 4 ? -1 : 1; |
|
1316 } : |
|
1317 function( a, b ) { |
|
1318 // Exit early if the nodes are identical |
|
1319 if ( a === b ) { |
|
1320 hasDuplicate = true; |
|
1321 return 0; |
|
1322 } |
|
1323 |
|
1324 var cur, |
|
1325 i = 0, |
|
1326 aup = a.parentNode, |
|
1327 bup = b.parentNode, |
|
1328 ap = [ a ], |
|
1329 bp = [ b ]; |
|
1330 |
|
1331 // Parentless nodes are either documents or disconnected |
|
1332 if ( !aup || !bup ) { |
|
1333 return a === doc ? -1 : |
|
1334 b === doc ? 1 : |
|
1335 aup ? -1 : |
|
1336 bup ? 1 : |
|
1337 sortInput ? |
|
1338 ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : |
|
1339 0; |
|
1340 |
|
1341 // If the nodes are siblings, we can do a quick check |
|
1342 } else if ( aup === bup ) { |
|
1343 return siblingCheck( a, b ); |
|
1344 } |
|
1345 |
|
1346 // Otherwise we need full lists of their ancestors for comparison |
|
1347 cur = a; |
|
1348 while ( (cur = cur.parentNode) ) { |
|
1349 ap.unshift( cur ); |
|
1350 } |
|
1351 cur = b; |
|
1352 while ( (cur = cur.parentNode) ) { |
|
1353 bp.unshift( cur ); |
|
1354 } |
|
1355 |
|
1356 // Walk down the tree looking for a discrepancy |
|
1357 while ( ap[i] === bp[i] ) { |
|
1358 i++; |
|
1359 } |
|
1360 |
|
1361 return i ? |
|
1362 // Do a sibling check if the nodes have a common ancestor |
|
1363 siblingCheck( ap[i], bp[i] ) : |
|
1364 |
|
1365 // Otherwise nodes in our document sort first |
|
1366 ap[i] === preferredDoc ? -1 : |
|
1367 bp[i] === preferredDoc ? 1 : |
|
1368 0; |
|
1369 }; |
|
1370 |
|
1371 return doc; |
|
1372 }; |
|
1373 |
|
1374 Sizzle.matches = function( expr, elements ) { |
|
1375 return Sizzle( expr, null, null, elements ); |
|
1376 }; |
|
1377 |
|
1378 Sizzle.matchesSelector = function( elem, expr ) { |
|
1379 // Set document vars if needed |
|
1380 if ( ( elem.ownerDocument || elem ) !== document ) { |
|
1381 setDocument( elem ); |
|
1382 } |
|
1383 |
|
1384 // Make sure that attribute selectors are quoted |
|
1385 expr = expr.replace( rattributeQuotes, "='$1']" ); |
|
1386 |
|
1387 if ( support.matchesSelector && documentIsHTML && |
|
1388 ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && |
|
1389 ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { |
|
1390 |
|
1391 try { |
|
1392 var ret = matches.call( elem, expr ); |
|
1393 |
|
1394 // IE 9's matchesSelector returns false on disconnected nodes |
|
1395 if ( ret || support.disconnectedMatch || |
|
1396 // As well, disconnected nodes are said to be in a document |
|
1397 // fragment in IE 9 |
|
1398 elem.document && elem.document.nodeType !== 11 ) { |
|
1399 return ret; |
|
1400 } |
|
1401 } catch(e) {} |
|
1402 } |
|
1403 |
|
1404 return Sizzle( expr, document, null, [ elem ] ).length > 0; |
|
1405 }; |
|
1406 |
|
1407 Sizzle.contains = function( context, elem ) { |
|
1408 // Set document vars if needed |
|
1409 if ( ( context.ownerDocument || context ) !== document ) { |
|
1410 setDocument( context ); |
|
1411 } |
|
1412 return contains( context, elem ); |
|
1413 }; |
|
1414 |
|
1415 Sizzle.attr = function( elem, name ) { |
|
1416 // Set document vars if needed |
|
1417 if ( ( elem.ownerDocument || elem ) !== document ) { |
|
1418 setDocument( elem ); |
|
1419 } |
|
1420 |
|
1421 var fn = Expr.attrHandle[ name.toLowerCase() ], |
|
1422 // Don't get fooled by Object.prototype properties (jQuery #13807) |
|
1423 val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? |
|
1424 fn( elem, name, !documentIsHTML ) : |
|
1425 undefined; |
|
1426 |
|
1427 return val !== undefined ? |
|
1428 val : |
|
1429 support.attributes || !documentIsHTML ? |
|
1430 elem.getAttribute( name ) : |
|
1431 (val = elem.getAttributeNode(name)) && val.specified ? |
|
1432 val.value : |
|
1433 null; |
|
1434 }; |
|
1435 |
|
1436 Sizzle.error = function( msg ) { |
|
1437 throw new Error( "Syntax error, unrecognized expression: " + msg ); |
|
1438 }; |
|
1439 |
|
1440 /** |
|
1441 * Document sorting and removing duplicates |
|
1442 * @param {ArrayLike} results |
|
1443 */ |
|
1444 Sizzle.uniqueSort = function( results ) { |
|
1445 var elem, |
|
1446 duplicates = [], |
|
1447 j = 0, |
|
1448 i = 0; |
|
1449 |
|
1450 // Unless we *know* we can detect duplicates, assume their presence |
|
1451 hasDuplicate = !support.detectDuplicates; |
|
1452 sortInput = !support.sortStable && results.slice( 0 ); |
|
1453 results.sort( sortOrder ); |
|
1454 |
|
1455 if ( hasDuplicate ) { |
|
1456 while ( (elem = results[i++]) ) { |
|
1457 if ( elem === results[ i ] ) { |
|
1458 j = duplicates.push( i ); |
|
1459 } |
|
1460 } |
|
1461 while ( j-- ) { |
|
1462 results.splice( duplicates[ j ], 1 ); |
|
1463 } |
|
1464 } |
|
1465 |
|
1466 // Clear input after sorting to release objects |
|
1467 // See https://github.com/jquery/sizzle/pull/225 |
|
1468 sortInput = null; |
|
1469 |
|
1470 return results; |
|
1471 }; |
|
1472 |
|
1473 /** |
|
1474 * Utility function for retrieving the text value of an array of DOM nodes |
|
1475 * @param {Array|Element} elem |
|
1476 */ |
|
1477 getText = Sizzle.getText = function( elem ) { |
|
1478 var node, |
|
1479 ret = "", |
|
1480 i = 0, |
|
1481 nodeType = elem.nodeType; |
|
1482 |
|
1483 if ( !nodeType ) { |
|
1484 // If no nodeType, this is expected to be an array |
|
1485 while ( (node = elem[i++]) ) { |
|
1486 // Do not traverse comment nodes |
|
1487 ret += getText( node ); |
|
1488 } |
|
1489 } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { |
|
1490 // Use textContent for elements |
|
1491 // innerText usage removed for consistency of new lines (jQuery #11153) |
|
1492 if ( typeof elem.textContent === "string" ) { |
|
1493 return elem.textContent; |
|
1494 } else { |
|
1495 // Traverse its children |
|
1496 for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { |
|
1497 ret += getText( elem ); |
|
1498 } |
|
1499 } |
|
1500 } else if ( nodeType === 3 || nodeType === 4 ) { |
|
1501 return elem.nodeValue; |
|
1502 } |
|
1503 // Do not include comment or processing instruction nodes |
|
1504 |
|
1505 return ret; |
|
1506 }; |
|
1507 |
|
1508 Expr = Sizzle.selectors = { |
|
1509 |
|
1510 // Can be adjusted by the user |
|
1511 cacheLength: 50, |
|
1512 |
|
1513 createPseudo: markFunction, |
|
1514 |
|
1515 match: matchExpr, |
|
1516 |
|
1517 attrHandle: {}, |
|
1518 |
|
1519 find: {}, |
|
1520 |
|
1521 relative: { |
|
1522 ">": { dir: "parentNode", first: true }, |
|
1523 " ": { dir: "parentNode" }, |
|
1524 "+": { dir: "previousSibling", first: true }, |
|
1525 "~": { dir: "previousSibling" } |
|
1526 }, |
|
1527 |
|
1528 preFilter: { |
|
1529 "ATTR": function( match ) { |
|
1530 match[1] = match[1].replace( runescape, funescape ); |
|
1531 |
|
1532 // Move the given value to match[3] whether quoted or unquoted |
|
1533 match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); |
|
1534 |
|
1535 if ( match[2] === "~=" ) { |
|
1536 match[3] = " " + match[3] + " "; |
|
1537 } |
|
1538 |
|
1539 return match.slice( 0, 4 ); |
|
1540 }, |
|
1541 |
|
1542 "CHILD": function( match ) { |
|
1543 /* matches from matchExpr["CHILD"] |
|
1544 1 type (only|nth|...) |
|
1545 2 what (child|of-type) |
|
1546 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) |
|
1547 4 xn-component of xn+y argument ([+-]?\d*n|) |
|
1548 5 sign of xn-component |
|
1549 6 x of xn-component |
|
1550 7 sign of y-component |
|
1551 8 y of y-component |
|
1552 */ |
|
1553 match[1] = match[1].toLowerCase(); |
|
1554 |
|
1555 if ( match[1].slice( 0, 3 ) === "nth" ) { |
|
1556 // nth-* requires argument |
|
1557 if ( !match[3] ) { |
|
1558 Sizzle.error( match[0] ); |
|
1559 } |
|
1560 |
|
1561 // numeric x and y parameters for Expr.filter.CHILD |
|
1562 // remember that false/true cast respectively to 0/1 |
|
1563 match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); |
|
1564 match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); |
|
1565 |
|
1566 // other types prohibit arguments |
|
1567 } else if ( match[3] ) { |
|
1568 Sizzle.error( match[0] ); |
|
1569 } |
|
1570 |
|
1571 return match; |
|
1572 }, |
|
1573 |
|
1574 "PSEUDO": function( match ) { |
|
1575 var excess, |
|
1576 unquoted = !match[6] && match[2]; |
|
1577 |
|
1578 if ( matchExpr["CHILD"].test( match[0] ) ) { |
|
1579 return null; |
|
1580 } |
|
1581 |
|
1582 // Accept quoted arguments as-is |
|
1583 if ( match[3] ) { |
|
1584 match[2] = match[4] || match[5] || ""; |
|
1585 |
|
1586 // Strip excess characters from unquoted arguments |
|
1587 } else if ( unquoted && rpseudo.test( unquoted ) && |
|
1588 // Get excess from tokenize (recursively) |
|
1589 (excess = tokenize( unquoted, true )) && |
|
1590 // advance to the next closing parenthesis |
|
1591 (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { |
|
1592 |
|
1593 // excess is a negative index |
|
1594 match[0] = match[0].slice( 0, excess ); |
|
1595 match[2] = unquoted.slice( 0, excess ); |
|
1596 } |
|
1597 |
|
1598 // Return only captures needed by the pseudo filter method (type and argument) |
|
1599 return match.slice( 0, 3 ); |
|
1600 } |
|
1601 }, |
|
1602 |
|
1603 filter: { |
|
1604 |
|
1605 "TAG": function( nodeNameSelector ) { |
|
1606 var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); |
|
1607 return nodeNameSelector === "*" ? |
|
1608 function() { return true; } : |
|
1609 function( elem ) { |
|
1610 return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; |
|
1611 }; |
|
1612 }, |
|
1613 |
|
1614 "CLASS": function( className ) { |
|
1615 var pattern = classCache[ className + " " ]; |
|
1616 |
|
1617 return pattern || |
|
1618 (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && |
|
1619 classCache( className, function( elem ) { |
|
1620 return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); |
|
1621 }); |
|
1622 }, |
|
1623 |
|
1624 "ATTR": function( name, operator, check ) { |
|
1625 return function( elem ) { |
|
1626 var result = Sizzle.attr( elem, name ); |
|
1627 |
|
1628 if ( result == null ) { |
|
1629 return operator === "!="; |
|
1630 } |
|
1631 if ( !operator ) { |
|
1632 return true; |
|
1633 } |
|
1634 |
|
1635 result += ""; |
|
1636 |
|
1637 return operator === "=" ? result === check : |
|
1638 operator === "!=" ? result !== check : |
|
1639 operator === "^=" ? check && result.indexOf( check ) === 0 : |
|
1640 operator === "*=" ? check && result.indexOf( check ) > -1 : |
|
1641 operator === "$=" ? check && result.slice( -check.length ) === check : |
|
1642 operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : |
|
1643 operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : |
|
1644 false; |
|
1645 }; |
|
1646 }, |
|
1647 |
|
1648 "CHILD": function( type, what, argument, first, last ) { |
|
1649 var simple = type.slice( 0, 3 ) !== "nth", |
|
1650 forward = type.slice( -4 ) !== "last", |
|
1651 ofType = what === "of-type"; |
|
1652 |
|
1653 return first === 1 && last === 0 ? |
|
1654 |
|
1655 // Shortcut for :nth-*(n) |
|
1656 function( elem ) { |
|
1657 return !!elem.parentNode; |
|
1658 } : |
|
1659 |
|
1660 function( elem, context, xml ) { |
|
1661 var cache, outerCache, node, diff, nodeIndex, start, |
|
1662 dir = simple !== forward ? "nextSibling" : "previousSibling", |
|
1663 parent = elem.parentNode, |
|
1664 name = ofType && elem.nodeName.toLowerCase(), |
|
1665 useCache = !xml && !ofType; |
|
1666 |
|
1667 if ( parent ) { |
|
1668 |
|
1669 // :(first|last|only)-(child|of-type) |
|
1670 if ( simple ) { |
|
1671 while ( dir ) { |
|
1672 node = elem; |
|
1673 while ( (node = node[ dir ]) ) { |
|
1674 if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { |
|
1675 return false; |
|
1676 } |
|
1677 } |
|
1678 // Reverse direction for :only-* (if we haven't yet done so) |
|
1679 start = dir = type === "only" && !start && "nextSibling"; |
|
1680 } |
|
1681 return true; |
|
1682 } |
|
1683 |
|
1684 start = [ forward ? parent.firstChild : parent.lastChild ]; |
|
1685 |
|
1686 // non-xml :nth-child(...) stores cache data on `parent` |
|
1687 if ( forward && useCache ) { |
|
1688 // Seek `elem` from a previously-cached index |
|
1689 outerCache = parent[ expando ] || (parent[ expando ] = {}); |
|
1690 cache = outerCache[ type ] || []; |
|
1691 nodeIndex = cache[0] === dirruns && cache[1]; |
|
1692 diff = cache[0] === dirruns && cache[2]; |
|
1693 node = nodeIndex && parent.childNodes[ nodeIndex ]; |
|
1694 |
|
1695 while ( (node = ++nodeIndex && node && node[ dir ] || |
|
1696 |
|
1697 // Fallback to seeking `elem` from the start |
|
1698 (diff = nodeIndex = 0) || start.pop()) ) { |
|
1699 |
|
1700 // When found, cache indexes on `parent` and break |
|
1701 if ( node.nodeType === 1 && ++diff && node === elem ) { |
|
1702 outerCache[ type ] = [ dirruns, nodeIndex, diff ]; |
|
1703 break; |
|
1704 } |
|
1705 } |
|
1706 |
|
1707 // Use previously-cached element index if available |
|
1708 } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { |
|
1709 diff = cache[1]; |
|
1710 |
|
1711 // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) |
|
1712 } else { |
|
1713 // Use the same loop as above to seek `elem` from the start |
|
1714 while ( (node = ++nodeIndex && node && node[ dir ] || |
|
1715 (diff = nodeIndex = 0) || start.pop()) ) { |
|
1716 |
|
1717 if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { |
|
1718 // Cache the index of each encountered element |
|
1719 if ( useCache ) { |
|
1720 (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; |
|
1721 } |
|
1722 |
|
1723 if ( node === elem ) { |
|
1724 break; |
|
1725 } |
|
1726 } |
|
1727 } |
|
1728 } |
|
1729 |
|
1730 // Incorporate the offset, then check against cycle size |
|
1731 diff -= last; |
|
1732 return diff === first || ( diff % first === 0 && diff / first >= 0 ); |
|
1733 } |
|
1734 }; |
|
1735 }, |
|
1736 |
|
1737 "PSEUDO": function( pseudo, argument ) { |
|
1738 // pseudo-class names are case-insensitive |
|
1739 // http://www.w3.org/TR/selectors/#pseudo-classes |
|
1740 // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters |
|
1741 // Remember that setFilters inherits from pseudos |
|
1742 var args, |
|
1743 fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || |
|
1744 Sizzle.error( "unsupported pseudo: " + pseudo ); |
|
1745 |
|
1746 // The user may use createPseudo to indicate that |
|
1747 // arguments are needed to create the filter function |
|
1748 // just as Sizzle does |
|
1749 if ( fn[ expando ] ) { |
|
1750 return fn( argument ); |
|
1751 } |
|
1752 |
|
1753 // But maintain support for old signatures |
|
1754 if ( fn.length > 1 ) { |
|
1755 args = [ pseudo, pseudo, "", argument ]; |
|
1756 return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? |
|
1757 markFunction(function( seed, matches ) { |
|
1758 var idx, |
|
1759 matched = fn( seed, argument ), |
|
1760 i = matched.length; |
|
1761 while ( i-- ) { |
|
1762 idx = indexOf.call( seed, matched[i] ); |
|
1763 seed[ idx ] = !( matches[ idx ] = matched[i] ); |
|
1764 } |
|
1765 }) : |
|
1766 function( elem ) { |
|
1767 return fn( elem, 0, args ); |
|
1768 }; |
|
1769 } |
|
1770 |
|
1771 return fn; |
|
1772 } |
|
1773 }, |
|
1774 |
|
1775 pseudos: { |
|
1776 // Potentially complex pseudos |
|
1777 "not": markFunction(function( selector ) { |
|
1778 // Trim the selector passed to compile |
|
1779 // to avoid treating leading and trailing |
|
1780 // spaces as combinators |
|
1781 var input = [], |
|
1782 results = [], |
|
1783 matcher = compile( selector.replace( rtrim, "$1" ) ); |
|
1784 |
|
1785 return matcher[ expando ] ? |
|
1786 markFunction(function( seed, matches, context, xml ) { |
|
1787 var elem, |
|
1788 unmatched = matcher( seed, null, xml, [] ), |
|
1789 i = seed.length; |
|
1790 |
|
1791 // Match elements unmatched by `matcher` |
|
1792 while ( i-- ) { |
|
1793 if ( (elem = unmatched[i]) ) { |
|
1794 seed[i] = !(matches[i] = elem); |
|
1795 } |
|
1796 } |
|
1797 }) : |
|
1798 function( elem, context, xml ) { |
|
1799 input[0] = elem; |
|
1800 matcher( input, null, xml, results ); |
|
1801 return !results.pop(); |
|
1802 }; |
|
1803 }), |
|
1804 |
|
1805 "has": markFunction(function( selector ) { |
|
1806 return function( elem ) { |
|
1807 return Sizzle( selector, elem ).length > 0; |
|
1808 }; |
|
1809 }), |
|
1810 |
|
1811 "contains": markFunction(function( text ) { |
|
1812 return function( elem ) { |
|
1813 return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; |
|
1814 }; |
|
1815 }), |
|
1816 |
|
1817 // "Whether an element is represented by a :lang() selector |
|
1818 // is based solely on the element's language value |
|
1819 // being equal to the identifier C, |
|
1820 // or beginning with the identifier C immediately followed by "-". |
|
1821 // The matching of C against the element's language value is performed case-insensitively. |
|
1822 // The identifier C does not have to be a valid language name." |
|
1823 // http://www.w3.org/TR/selectors/#lang-pseudo |
|
1824 "lang": markFunction( function( lang ) { |
|
1825 // lang value must be a valid identifier |
|
1826 if ( !ridentifier.test(lang || "") ) { |
|
1827 Sizzle.error( "unsupported lang: " + lang ); |
|
1828 } |
|
1829 lang = lang.replace( runescape, funescape ).toLowerCase(); |
|
1830 return function( elem ) { |
|
1831 var elemLang; |
|
1832 do { |
|
1833 if ( (elemLang = documentIsHTML ? |
|
1834 elem.lang : |
|
1835 elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { |
|
1836 |
|
1837 elemLang = elemLang.toLowerCase(); |
|
1838 return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; |
|
1839 } |
|
1840 } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); |
|
1841 return false; |
|
1842 }; |
|
1843 }), |
|
1844 |
|
1845 // Miscellaneous |
|
1846 "target": function( elem ) { |
|
1847 var hash = window.location && window.location.hash; |
|
1848 return hash && hash.slice( 1 ) === elem.id; |
|
1849 }, |
|
1850 |
|
1851 "root": function( elem ) { |
|
1852 return elem === docElem; |
|
1853 }, |
|
1854 |
|
1855 "focus": function( elem ) { |
|
1856 return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); |
|
1857 }, |
|
1858 |
|
1859 // Boolean properties |
|
1860 "enabled": function( elem ) { |
|
1861 return elem.disabled === false; |
|
1862 }, |
|
1863 |
|
1864 "disabled": function( elem ) { |
|
1865 return elem.disabled === true; |
|
1866 }, |
|
1867 |
|
1868 "checked": function( elem ) { |
|
1869 // In CSS3, :checked should return both checked and selected elements |
|
1870 // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked |
|
1871 var nodeName = elem.nodeName.toLowerCase(); |
|
1872 return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); |
|
1873 }, |
|
1874 |
|
1875 "selected": function( elem ) { |
|
1876 // Accessing this property makes selected-by-default |
|
1877 // options in Safari work properly |
|
1878 if ( elem.parentNode ) { |
|
1879 elem.parentNode.selectedIndex; |
|
1880 } |
|
1881 |
|
1882 return elem.selected === true; |
|
1883 }, |
|
1884 |
|
1885 // Contents |
|
1886 "empty": function( elem ) { |
|
1887 // http://www.w3.org/TR/selectors/#empty-pseudo |
|
1888 // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), |
|
1889 // but not by others (comment: 8; processing instruction: 7; etc.) |
|
1890 // nodeType < 6 works because attributes (2) do not appear as children |
|
1891 for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { |
|
1892 if ( elem.nodeType < 6 ) { |
|
1893 return false; |
|
1894 } |
|
1895 } |
|
1896 return true; |
|
1897 }, |
|
1898 |
|
1899 "parent": function( elem ) { |
|
1900 return !Expr.pseudos["empty"]( elem ); |
|
1901 }, |
|
1902 |
|
1903 // Element/input types |
|
1904 "header": function( elem ) { |
|
1905 return rheader.test( elem.nodeName ); |
|
1906 }, |
|
1907 |
|
1908 "input": function( elem ) { |
|
1909 return rinputs.test( elem.nodeName ); |
|
1910 }, |
|
1911 |
|
1912 "button": function( elem ) { |
|
1913 var name = elem.nodeName.toLowerCase(); |
|
1914 return name === "input" && elem.type === "button" || name === "button"; |
|
1915 }, |
|
1916 |
|
1917 "text": function( elem ) { |
|
1918 var attr; |
|
1919 return elem.nodeName.toLowerCase() === "input" && |
|
1920 elem.type === "text" && |
|
1921 |
|
1922 // Support: IE<8 |
|
1923 // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" |
|
1924 ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); |
|
1925 }, |
|
1926 |
|
1927 // Position-in-collection |
|
1928 "first": createPositionalPseudo(function() { |
|
1929 return [ 0 ]; |
|
1930 }), |
|
1931 |
|
1932 "last": createPositionalPseudo(function( matchIndexes, length ) { |
|
1933 return [ length - 1 ]; |
|
1934 }), |
|
1935 |
|
1936 "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { |
|
1937 return [ argument < 0 ? argument + length : argument ]; |
|
1938 }), |
|
1939 |
|
1940 "even": createPositionalPseudo(function( matchIndexes, length ) { |
|
1941 var i = 0; |
|
1942 for ( ; i < length; i += 2 ) { |
|
1943 matchIndexes.push( i ); |
|
1944 } |
|
1945 return matchIndexes; |
|
1946 }), |
|
1947 |
|
1948 "odd": createPositionalPseudo(function( matchIndexes, length ) { |
|
1949 var i = 1; |
|
1950 for ( ; i < length; i += 2 ) { |
|
1951 matchIndexes.push( i ); |
|
1952 } |
|
1953 return matchIndexes; |
|
1954 }), |
|
1955 |
|
1956 "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { |
|
1957 var i = argument < 0 ? argument + length : argument; |
|
1958 for ( ; --i >= 0; ) { |
|
1959 matchIndexes.push( i ); |
|
1960 } |
|
1961 return matchIndexes; |
|
1962 }), |
|
1963 |
|
1964 "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { |
|
1965 var i = argument < 0 ? argument + length : argument; |
|
1966 for ( ; ++i < length; ) { |
|
1967 matchIndexes.push( i ); |
|
1968 } |
|
1969 return matchIndexes; |
|
1970 }) |
|
1971 } |
|
1972 }; |
|
1973 |
|
1974 Expr.pseudos["nth"] = Expr.pseudos["eq"]; |
|
1975 |
|
1976 // Add button/input type pseudos |
|
1977 for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { |
|
1978 Expr.pseudos[ i ] = createInputPseudo( i ); |
|
1979 } |
|
1980 for ( i in { submit: true, reset: true } ) { |
|
1981 Expr.pseudos[ i ] = createButtonPseudo( i ); |
|
1982 } |
|
1983 |
|
1984 // Easy API for creating new setFilters |
|
1985 function setFilters() {} |
|
1986 setFilters.prototype = Expr.filters = Expr.pseudos; |
|
1987 Expr.setFilters = new setFilters(); |
|
1988 |
|
1989 tokenize = Sizzle.tokenize = function( selector, parseOnly ) { |
|
1990 var matched, match, tokens, type, |
|
1991 soFar, groups, preFilters, |
|
1992 cached = tokenCache[ selector + " " ]; |
|
1993 |
|
1994 if ( cached ) { |
|
1995 return parseOnly ? 0 : cached.slice( 0 ); |
|
1996 } |
|
1997 |
|
1998 soFar = selector; |
|
1999 groups = []; |
|
2000 preFilters = Expr.preFilter; |
|
2001 |
|
2002 while ( soFar ) { |
|
2003 |
|
2004 // Comma and first run |
|
2005 if ( !matched || (match = rcomma.exec( soFar )) ) { |
|
2006 if ( match ) { |
|
2007 // Don't consume trailing commas as valid |
|
2008 soFar = soFar.slice( match[0].length ) || soFar; |
|
2009 } |
|
2010 groups.push( (tokens = []) ); |
|
2011 } |
|
2012 |
|
2013 matched = false; |
|
2014 |
|
2015 // Combinators |
|
2016 if ( (match = rcombinators.exec( soFar )) ) { |
|
2017 matched = match.shift(); |
|
2018 tokens.push({ |
|
2019 value: matched, |
|
2020 // Cast descendant combinators to space |
|
2021 type: match[0].replace( rtrim, " " ) |
|
2022 }); |
|
2023 soFar = soFar.slice( matched.length ); |
|
2024 } |
|
2025 |
|
2026 // Filters |
|
2027 for ( type in Expr.filter ) { |
|
2028 if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || |
|
2029 (match = preFilters[ type ]( match ))) ) { |
|
2030 matched = match.shift(); |
|
2031 tokens.push({ |
|
2032 value: matched, |
|
2033 type: type, |
|
2034 matches: match |
|
2035 }); |
|
2036 soFar = soFar.slice( matched.length ); |
|
2037 } |
|
2038 } |
|
2039 |
|
2040 if ( !matched ) { |
|
2041 break; |
|
2042 } |
|
2043 } |
|
2044 |
|
2045 // Return the length of the invalid excess |
|
2046 // if we're just parsing |
|
2047 // Otherwise, throw an error or return tokens |
|
2048 return parseOnly ? |
|
2049 soFar.length : |
|
2050 soFar ? |
|
2051 Sizzle.error( selector ) : |
|
2052 // Cache the tokens |
|
2053 tokenCache( selector, groups ).slice( 0 ); |
|
2054 }; |
|
2055 |
|
2056 function toSelector( tokens ) { |
|
2057 var i = 0, |
|
2058 len = tokens.length, |
|
2059 selector = ""; |
|
2060 for ( ; i < len; i++ ) { |
|
2061 selector += tokens[i].value; |
|
2062 } |
|
2063 return selector; |
|
2064 } |
|
2065 |
|
2066 function addCombinator( matcher, combinator, base ) { |
|
2067 var dir = combinator.dir, |
|
2068 checkNonElements = base && dir === "parentNode", |
|
2069 doneName = done++; |
|
2070 |
|
2071 return combinator.first ? |
|
2072 // Check against closest ancestor/preceding element |
|
2073 function( elem, context, xml ) { |
|
2074 while ( (elem = elem[ dir ]) ) { |
|
2075 if ( elem.nodeType === 1 || checkNonElements ) { |
|
2076 return matcher( elem, context, xml ); |
|
2077 } |
|
2078 } |
|
2079 } : |
|
2080 |
|
2081 // Check against all ancestor/preceding elements |
|
2082 function( elem, context, xml ) { |
|
2083 var oldCache, outerCache, |
|
2084 newCache = [ dirruns, doneName ]; |
|
2085 |
|
2086 // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching |
|
2087 if ( xml ) { |
|
2088 while ( (elem = elem[ dir ]) ) { |
|
2089 if ( elem.nodeType === 1 || checkNonElements ) { |
|
2090 if ( matcher( elem, context, xml ) ) { |
|
2091 return true; |
|
2092 } |
|
2093 } |
|
2094 } |
|
2095 } else { |
|
2096 while ( (elem = elem[ dir ]) ) { |
|
2097 if ( elem.nodeType === 1 || checkNonElements ) { |
|
2098 outerCache = elem[ expando ] || (elem[ expando ] = {}); |
|
2099 if ( (oldCache = outerCache[ dir ]) && |
|
2100 oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { |
|
2101 |
|
2102 // Assign to newCache so results back-propagate to previous elements |
|
2103 return (newCache[ 2 ] = oldCache[ 2 ]); |
|
2104 } else { |
|
2105 // Reuse newcache so results back-propagate to previous elements |
|
2106 outerCache[ dir ] = newCache; |
|
2107 |
|
2108 // A match means we're done; a fail means we have to keep checking |
|
2109 if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { |
|
2110 return true; |
|
2111 } |
|
2112 } |
|
2113 } |
|
2114 } |
|
2115 } |
|
2116 }; |
|
2117 } |
|
2118 |
|
2119 function elementMatcher( matchers ) { |
|
2120 return matchers.length > 1 ? |
|
2121 function( elem, context, xml ) { |
|
2122 var i = matchers.length; |
|
2123 while ( i-- ) { |
|
2124 if ( !matchers[i]( elem, context, xml ) ) { |
|
2125 return false; |
|
2126 } |
|
2127 } |
|
2128 return true; |
|
2129 } : |
|
2130 matchers[0]; |
|
2131 } |
|
2132 |
|
2133 function multipleContexts( selector, contexts, results ) { |
|
2134 var i = 0, |
|
2135 len = contexts.length; |
|
2136 for ( ; i < len; i++ ) { |
|
2137 Sizzle( selector, contexts[i], results ); |
|
2138 } |
|
2139 return results; |
|
2140 } |
|
2141 |
|
2142 function condense( unmatched, map, filter, context, xml ) { |
|
2143 var elem, |
|
2144 newUnmatched = [], |
|
2145 i = 0, |
|
2146 len = unmatched.length, |
|
2147 mapped = map != null; |
|
2148 |
|
2149 for ( ; i < len; i++ ) { |
|
2150 if ( (elem = unmatched[i]) ) { |
|
2151 if ( !filter || filter( elem, context, xml ) ) { |
|
2152 newUnmatched.push( elem ); |
|
2153 if ( mapped ) { |
|
2154 map.push( i ); |
|
2155 } |
|
2156 } |
|
2157 } |
|
2158 } |
|
2159 |
|
2160 return newUnmatched; |
|
2161 } |
|
2162 |
|
2163 function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { |
|
2164 if ( postFilter && !postFilter[ expando ] ) { |
|
2165 postFilter = setMatcher( postFilter ); |
|
2166 } |
|
2167 if ( postFinder && !postFinder[ expando ] ) { |
|
2168 postFinder = setMatcher( postFinder, postSelector ); |
|
2169 } |
|
2170 return markFunction(function( seed, results, context, xml ) { |
|
2171 var temp, i, elem, |
|
2172 preMap = [], |
|
2173 postMap = [], |
|
2174 preexisting = results.length, |
|
2175 |
|
2176 // Get initial elements from seed or context |
|
2177 elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), |
|
2178 |
|
2179 // Prefilter to get matcher input, preserving a map for seed-results synchronization |
|
2180 matcherIn = preFilter && ( seed || !selector ) ? |
|
2181 condense( elems, preMap, preFilter, context, xml ) : |
|
2182 elems, |
|
2183 |
|
2184 matcherOut = matcher ? |
|
2185 // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, |
|
2186 postFinder || ( seed ? preFilter : preexisting || postFilter ) ? |
|
2187 |
|
2188 // ...intermediate processing is necessary |
|
2189 [] : |
|
2190 |
|
2191 // ...otherwise use results directly |
|
2192 results : |
|
2193 matcherIn; |
|
2194 |
|
2195 // Find primary matches |
|
2196 if ( matcher ) { |
|
2197 matcher( matcherIn, matcherOut, context, xml ); |
|
2198 } |
|
2199 |
|
2200 // Apply postFilter |
|
2201 if ( postFilter ) { |
|
2202 temp = condense( matcherOut, postMap ); |
|
2203 postFilter( temp, [], context, xml ); |
|
2204 |
|
2205 // Un-match failing elements by moving them back to matcherIn |
|
2206 i = temp.length; |
|
2207 while ( i-- ) { |
|
2208 if ( (elem = temp[i]) ) { |
|
2209 matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); |
|
2210 } |
|
2211 } |
|
2212 } |
|
2213 |
|
2214 if ( seed ) { |
|
2215 if ( postFinder || preFilter ) { |
|
2216 if ( postFinder ) { |
|
2217 // Get the final matcherOut by condensing this intermediate into postFinder contexts |
|
2218 temp = []; |
|
2219 i = matcherOut.length; |
|
2220 while ( i-- ) { |
|
2221 if ( (elem = matcherOut[i]) ) { |
|
2222 // Restore matcherIn since elem is not yet a final match |
|
2223 temp.push( (matcherIn[i] = elem) ); |
|
2224 } |
|
2225 } |
|
2226 postFinder( null, (matcherOut = []), temp, xml ); |
|
2227 } |
|
2228 |
|
2229 // Move matched elements from seed to results to keep them synchronized |
|
2230 i = matcherOut.length; |
|
2231 while ( i-- ) { |
|
2232 if ( (elem = matcherOut[i]) && |
|
2233 (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { |
|
2234 |
|
2235 seed[temp] = !(results[temp] = elem); |
|
2236 } |
|
2237 } |
|
2238 } |
|
2239 |
|
2240 // Add elements to results, through postFinder if defined |
|
2241 } else { |
|
2242 matcherOut = condense( |
|
2243 matcherOut === results ? |
|
2244 matcherOut.splice( preexisting, matcherOut.length ) : |
|
2245 matcherOut |
|
2246 ); |
|
2247 if ( postFinder ) { |
|
2248 postFinder( null, results, matcherOut, xml ); |
|
2249 } else { |
|
2250 push.apply( results, matcherOut ); |
|
2251 } |
|
2252 } |
|
2253 }); |
|
2254 } |
|
2255 |
|
2256 function matcherFromTokens( tokens ) { |
|
2257 var checkContext, matcher, j, |
|
2258 len = tokens.length, |
|
2259 leadingRelative = Expr.relative[ tokens[0].type ], |
|
2260 implicitRelative = leadingRelative || Expr.relative[" "], |
|
2261 i = leadingRelative ? 1 : 0, |
|
2262 |
|
2263 // The foundational matcher ensures that elements are reachable from top-level context(s) |
|
2264 matchContext = addCombinator( function( elem ) { |
|
2265 return elem === checkContext; |
|
2266 }, implicitRelative, true ), |
|
2267 matchAnyContext = addCombinator( function( elem ) { |
|
2268 return indexOf.call( checkContext, elem ) > -1; |
|
2269 }, implicitRelative, true ), |
|
2270 matchers = [ function( elem, context, xml ) { |
|
2271 return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( |
|
2272 (checkContext = context).nodeType ? |
|
2273 matchContext( elem, context, xml ) : |
|
2274 matchAnyContext( elem, context, xml ) ); |
|
2275 } ]; |
|
2276 |
|
2277 for ( ; i < len; i++ ) { |
|
2278 if ( (matcher = Expr.relative[ tokens[i].type ]) ) { |
|
2279 matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; |
|
2280 } else { |
|
2281 matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); |
|
2282 |
|
2283 // Return special upon seeing a positional matcher |
|
2284 if ( matcher[ expando ] ) { |
|
2285 // Find the next relative operator (if any) for proper handling |
|
2286 j = ++i; |
|
2287 for ( ; j < len; j++ ) { |
|
2288 if ( Expr.relative[ tokens[j].type ] ) { |
|
2289 break; |
|
2290 } |
|
2291 } |
|
2292 return setMatcher( |
|
2293 i > 1 && elementMatcher( matchers ), |
|
2294 i > 1 && toSelector( |
|
2295 // If the preceding token was a descendant combinator, insert an implicit any-element `*` |
|
2296 tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) |
|
2297 ).replace( rtrim, "$1" ), |
|
2298 matcher, |
|
2299 i < j && matcherFromTokens( tokens.slice( i, j ) ), |
|
2300 j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), |
|
2301 j < len && toSelector( tokens ) |
|
2302 ); |
|
2303 } |
|
2304 matchers.push( matcher ); |
|
2305 } |
|
2306 } |
|
2307 |
|
2308 return elementMatcher( matchers ); |
|
2309 } |
|
2310 |
|
2311 function matcherFromGroupMatchers( elementMatchers, setMatchers ) { |
|
2312 var bySet = setMatchers.length > 0, |
|
2313 byElement = elementMatchers.length > 0, |
|
2314 superMatcher = function( seed, context, xml, results, outermost ) { |
|
2315 var elem, j, matcher, |
|
2316 matchedCount = 0, |
|
2317 i = "0", |
|
2318 unmatched = seed && [], |
|
2319 setMatched = [], |
|
2320 contextBackup = outermostContext, |
|
2321 // We must always have either seed elements or outermost context |
|
2322 elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), |
|
2323 // Use integer dirruns iff this is the outermost matcher |
|
2324 dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), |
|
2325 len = elems.length; |
|
2326 |
|
2327 if ( outermost ) { |
|
2328 outermostContext = context !== document && context; |
|
2329 } |
|
2330 |
|
2331 // Add elements passing elementMatchers directly to results |
|
2332 // Keep `i` a string if there are no elements so `matchedCount` will be "00" below |
|
2333 // Support: IE<9, Safari |
|
2334 // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id |
|
2335 for ( ; i !== len && (elem = elems[i]) != null; i++ ) { |
|
2336 if ( byElement && elem ) { |
|
2337 j = 0; |
|
2338 while ( (matcher = elementMatchers[j++]) ) { |
|
2339 if ( matcher( elem, context, xml ) ) { |
|
2340 results.push( elem ); |
|
2341 break; |
|
2342 } |
|
2343 } |
|
2344 if ( outermost ) { |
|
2345 dirruns = dirrunsUnique; |
|
2346 } |
|
2347 } |
|
2348 |
|
2349 // Track unmatched elements for set filters |
|
2350 if ( bySet ) { |
|
2351 // They will have gone through all possible matchers |
|
2352 if ( (elem = !matcher && elem) ) { |
|
2353 matchedCount--; |
|
2354 } |
|
2355 |
|
2356 // Lengthen the array for every element, matched or not |
|
2357 if ( seed ) { |
|
2358 unmatched.push( elem ); |
|
2359 } |
|
2360 } |
|
2361 } |
|
2362 |
|
2363 // Apply set filters to unmatched elements |
|
2364 matchedCount += i; |
|
2365 if ( bySet && i !== matchedCount ) { |
|
2366 j = 0; |
|
2367 while ( (matcher = setMatchers[j++]) ) { |
|
2368 matcher( unmatched, setMatched, context, xml ); |
|
2369 } |
|
2370 |
|
2371 if ( seed ) { |
|
2372 // Reintegrate element matches to eliminate the need for sorting |
|
2373 if ( matchedCount > 0 ) { |
|
2374 while ( i-- ) { |
|
2375 if ( !(unmatched[i] || setMatched[i]) ) { |
|
2376 setMatched[i] = pop.call( results ); |
|
2377 } |
|
2378 } |
|
2379 } |
|
2380 |
|
2381 // Discard index placeholder values to get only actual matches |
|
2382 setMatched = condense( setMatched ); |
|
2383 } |
|
2384 |
|
2385 // Add matches to results |
|
2386 push.apply( results, setMatched ); |
|
2387 |
|
2388 // Seedless set matches succeeding multiple successful matchers stipulate sorting |
|
2389 if ( outermost && !seed && setMatched.length > 0 && |
|
2390 ( matchedCount + setMatchers.length ) > 1 ) { |
|
2391 |
|
2392 Sizzle.uniqueSort( results ); |
|
2393 } |
|
2394 } |
|
2395 |
|
2396 // Override manipulation of globals by nested matchers |
|
2397 if ( outermost ) { |
|
2398 dirruns = dirrunsUnique; |
|
2399 outermostContext = contextBackup; |
|
2400 } |
|
2401 |
|
2402 return unmatched; |
|
2403 }; |
|
2404 |
|
2405 return bySet ? |
|
2406 markFunction( superMatcher ) : |
|
2407 superMatcher; |
|
2408 } |
|
2409 |
|
2410 compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { |
|
2411 var i, |
|
2412 setMatchers = [], |
|
2413 elementMatchers = [], |
|
2414 cached = compilerCache[ selector + " " ]; |
|
2415 |
|
2416 if ( !cached ) { |
|
2417 // Generate a function of recursive functions that can be used to check each element |
|
2418 if ( !match ) { |
|
2419 match = tokenize( selector ); |
|
2420 } |
|
2421 i = match.length; |
|
2422 while ( i-- ) { |
|
2423 cached = matcherFromTokens( match[i] ); |
|
2424 if ( cached[ expando ] ) { |
|
2425 setMatchers.push( cached ); |
|
2426 } else { |
|
2427 elementMatchers.push( cached ); |
|
2428 } |
|
2429 } |
|
2430 |
|
2431 // Cache the compiled function |
|
2432 cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); |
|
2433 |
|
2434 // Save selector and tokenization |
|
2435 cached.selector = selector; |
|
2436 } |
|
2437 return cached; |
|
2438 }; |
|
2439 |
|
2440 /** |
|
2441 * A low-level selection function that works with Sizzle's compiled |
|
2442 * selector functions |
|
2443 * @param {String|Function} selector A selector or a pre-compiled |
|
2444 * selector function built with Sizzle.compile |
|
2445 * @param {Element} context |
|
2446 * @param {Array} [results] |
|
2447 * @param {Array} [seed] A set of elements to match against |
|
2448 */ |
|
2449 select = Sizzle.select = function( selector, context, results, seed ) { |
|
2450 var i, tokens, token, type, find, |
|
2451 compiled = typeof selector === "function" && selector, |
|
2452 match = !seed && tokenize( (selector = compiled.selector || selector) ); |
|
2453 |
|
2454 results = results || []; |
|
2455 |
|
2456 // Try to minimize operations if there is no seed and only one group |
|
2457 if ( match.length === 1 ) { |
|
2458 |
|
2459 // Take a shortcut and set the context if the root selector is an ID |
|
2460 tokens = match[0] = match[0].slice( 0 ); |
|
2461 if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && |
|
2462 support.getById && context.nodeType === 9 && documentIsHTML && |
|
2463 Expr.relative[ tokens[1].type ] ) { |
|
2464 |
|
2465 context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; |
|
2466 if ( !context ) { |
|
2467 return results; |
|
2468 |
|
2469 // Precompiled matchers will still verify ancestry, so step up a level |
|
2470 } else if ( compiled ) { |
|
2471 context = context.parentNode; |
|
2472 } |
|
2473 |
|
2474 selector = selector.slice( tokens.shift().value.length ); |
|
2475 } |
|
2476 |
|
2477 // Fetch a seed set for right-to-left matching |
|
2478 i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; |
|
2479 while ( i-- ) { |
|
2480 token = tokens[i]; |
|
2481 |
|
2482 // Abort if we hit a combinator |
|
2483 if ( Expr.relative[ (type = token.type) ] ) { |
|
2484 break; |
|
2485 } |
|
2486 if ( (find = Expr.find[ type ]) ) { |
|
2487 // Search, expanding context for leading sibling combinators |
|
2488 if ( (seed = find( |
|
2489 token.matches[0].replace( runescape, funescape ), |
|
2490 rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context |
|
2491 )) ) { |
|
2492 |
|
2493 // If seed is empty or no tokens remain, we can return early |
|
2494 tokens.splice( i, 1 ); |
|
2495 selector = seed.length && toSelector( tokens ); |
|
2496 if ( !selector ) { |
|
2497 push.apply( results, seed ); |
|
2498 return results; |
|
2499 } |
|
2500 |
|
2501 break; |
|
2502 } |
|
2503 } |
|
2504 } |
|
2505 } |
|
2506 |
|
2507 // Compile and execute a filtering function if one is not provided |
|
2508 // Provide `match` to avoid retokenization if we modified the selector above |
|
2509 ( compiled || compile( selector, match ) )( |
|
2510 seed, |
|
2511 context, |
|
2512 !documentIsHTML, |
|
2513 results, |
|
2514 rsibling.test( selector ) && testContext( context.parentNode ) || context |
|
2515 ); |
|
2516 return results; |
|
2517 }; |
|
2518 |
|
2519 // One-time assignments |
|
2520 |
|
2521 // Sort stability |
|
2522 support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; |
|
2523 |
|
2524 // Support: Chrome<14 |
|
2525 // Always assume duplicates if they aren't passed to the comparison function |
|
2526 support.detectDuplicates = !!hasDuplicate; |
|
2527 |
|
2528 // Initialize against the default document |
|
2529 setDocument(); |
|
2530 |
|
2531 // Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) |
|
2532 // Detached nodes confoundingly follow *each other* |
|
2533 support.sortDetached = assert(function( div1 ) { |
|
2534 // Should return 1, but returns 4 (following) |
|
2535 return div1.compareDocumentPosition( document.createElement("div") ) & 1; |
|
2536 }); |
|
2537 |
|
2538 // Support: IE<8 |
|
2539 // Prevent attribute/property "interpolation" |
|
2540 // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx |
|
2541 if ( !assert(function( div ) { |
|
2542 div.innerHTML = "<a href='#'></a>"; |
|
2543 return div.firstChild.getAttribute("href") === "#" ; |
|
2544 }) ) { |
|
2545 addHandle( "type|href|height|width", function( elem, name, isXML ) { |
|
2546 if ( !isXML ) { |
|
2547 return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); |
|
2548 } |
|
2549 }); |
|
2550 } |
|
2551 |
|
2552 // Support: IE<9 |
|
2553 // Use defaultValue in place of getAttribute("value") |
|
2554 if ( !support.attributes || !assert(function( div ) { |
|
2555 div.innerHTML = "<input/>"; |
|
2556 div.firstChild.setAttribute( "value", "" ); |
|
2557 return div.firstChild.getAttribute( "value" ) === ""; |
|
2558 }) ) { |
|
2559 addHandle( "value", function( elem, name, isXML ) { |
|
2560 if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { |
|
2561 return elem.defaultValue; |
|
2562 } |
|
2563 }); |
|
2564 } |
|
2565 |
|
2566 // Support: IE<9 |
|
2567 // Use getAttributeNode to fetch booleans when getAttribute lies |
|
2568 if ( !assert(function( div ) { |
|
2569 return div.getAttribute("disabled") == null; |
|
2570 }) ) { |
|
2571 addHandle( booleans, function( elem, name, isXML ) { |
|
2572 var val; |
|
2573 if ( !isXML ) { |
|
2574 return elem[ name ] === true ? name.toLowerCase() : |
|
2575 (val = elem.getAttributeNode( name )) && val.specified ? |
|
2576 val.value : |
|
2577 null; |
|
2578 } |
|
2579 }); |
|
2580 } |
|
2581 |
|
2582 return Sizzle; |
|
2583 |
|
2584 })( window ); |
|
2585 |
|
2586 |
|
2587 |
|
2588 jQuery.find = Sizzle; |
|
2589 jQuery.expr = Sizzle.selectors; |
|
2590 jQuery.expr[":"] = jQuery.expr.pseudos; |
|
2591 jQuery.unique = Sizzle.uniqueSort; |
|
2592 jQuery.text = Sizzle.getText; |
|
2593 jQuery.isXMLDoc = Sizzle.isXML; |
|
2594 jQuery.contains = Sizzle.contains; |
|
2595 |
|
2596 |
|
2597 |
|
2598 var rneedsContext = jQuery.expr.match.needsContext; |
|
2599 |
|
2600 var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); |
|
2601 |
|
2602 |
|
2603 |
|
2604 var risSimple = /^.[^:#\[\.,]*$/; |
|
2605 |
|
2606 // Implement the identical functionality for filter and not |
|
2607 function winnow( elements, qualifier, not ) { |
|
2608 if ( jQuery.isFunction( qualifier ) ) { |
|
2609 return jQuery.grep( elements, function( elem, i ) { |
|
2610 /* jshint -W018 */ |
|
2611 return !!qualifier.call( elem, i, elem ) !== not; |
|
2612 }); |
|
2613 |
|
2614 } |
|
2615 |
|
2616 if ( qualifier.nodeType ) { |
|
2617 return jQuery.grep( elements, function( elem ) { |
|
2618 return ( elem === qualifier ) !== not; |
|
2619 }); |
|
2620 |
|
2621 } |
|
2622 |
|
2623 if ( typeof qualifier === "string" ) { |
|
2624 if ( risSimple.test( qualifier ) ) { |
|
2625 return jQuery.filter( qualifier, elements, not ); |
|
2626 } |
|
2627 |
|
2628 qualifier = jQuery.filter( qualifier, elements ); |
|
2629 } |
|
2630 |
|
2631 return jQuery.grep( elements, function( elem ) { |
|
2632 return ( indexOf.call( qualifier, elem ) >= 0 ) !== not; |
|
2633 }); |
|
2634 } |
|
2635 |
|
2636 jQuery.filter = function( expr, elems, not ) { |
|
2637 var elem = elems[ 0 ]; |
|
2638 |
|
2639 if ( not ) { |
|
2640 expr = ":not(" + expr + ")"; |
|
2641 } |
|
2642 |
|
2643 return elems.length === 1 && elem.nodeType === 1 ? |
|
2644 jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : |
|
2645 jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { |
|
2646 return elem.nodeType === 1; |
|
2647 })); |
|
2648 }; |
|
2649 |
|
2650 jQuery.fn.extend({ |
|
2651 find: function( selector ) { |
|
2652 var i, |
|
2653 len = this.length, |
|
2654 ret = [], |
|
2655 self = this; |
|
2656 |
|
2657 if ( typeof selector !== "string" ) { |
|
2658 return this.pushStack( jQuery( selector ).filter(function() { |
|
2659 for ( i = 0; i < len; i++ ) { |
|
2660 if ( jQuery.contains( self[ i ], this ) ) { |
|
2661 return true; |
|
2662 } |
|
2663 } |
|
2664 }) ); |
|
2665 } |
|
2666 |
|
2667 for ( i = 0; i < len; i++ ) { |
|
2668 jQuery.find( selector, self[ i ], ret ); |
|
2669 } |
|
2670 |
|
2671 // Needed because $( selector, context ) becomes $( context ).find( selector ) |
|
2672 ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); |
|
2673 ret.selector = this.selector ? this.selector + " " + selector : selector; |
|
2674 return ret; |
|
2675 }, |
|
2676 filter: function( selector ) { |
|
2677 return this.pushStack( winnow(this, selector || [], false) ); |
|
2678 }, |
|
2679 not: function( selector ) { |
|
2680 return this.pushStack( winnow(this, selector || [], true) ); |
|
2681 }, |
|
2682 is: function( selector ) { |
|
2683 return !!winnow( |
|
2684 this, |
|
2685 |
|
2686 // If this is a positional/relative selector, check membership in the returned set |
|
2687 // so $("p:first").is("p:last") won't return true for a doc with two "p". |
|
2688 typeof selector === "string" && rneedsContext.test( selector ) ? |
|
2689 jQuery( selector ) : |
|
2690 selector || [], |
|
2691 false |
|
2692 ).length; |
|
2693 } |
|
2694 }); |
|
2695 |
|
2696 |
|
2697 // Initialize a jQuery object |
|
2698 |
|
2699 |
|
2700 // A central reference to the root jQuery(document) |
|
2701 var rootjQuery, |
|
2702 |
|
2703 // A simple way to check for HTML strings |
|
2704 // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) |
|
2705 // Strict HTML recognition (#11290: must start with <) |
|
2706 rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, |
|
2707 |
|
2708 init = jQuery.fn.init = function( selector, context ) { |
|
2709 var match, elem; |
|
2710 |
|
2711 // HANDLE: $(""), $(null), $(undefined), $(false) |
|
2712 if ( !selector ) { |
|
2713 return this; |
|
2714 } |
|
2715 |
|
2716 // Handle HTML strings |
|
2717 if ( typeof selector === "string" ) { |
|
2718 if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) { |
|
2719 // Assume that strings that start and end with <> are HTML and skip the regex check |
|
2720 match = [ null, selector, null ]; |
|
2721 |
|
2722 } else { |
|
2723 match = rquickExpr.exec( selector ); |
|
2724 } |
|
2725 |
|
2726 // Match html or make sure no context is specified for #id |
|
2727 if ( match && (match[1] || !context) ) { |
|
2728 |
|
2729 // HANDLE: $(html) -> $(array) |
|
2730 if ( match[1] ) { |
|
2731 context = context instanceof jQuery ? context[0] : context; |
|
2732 |
|
2733 // scripts is true for back-compat |
|
2734 // Intentionally let the error be thrown if parseHTML is not present |
|
2735 jQuery.merge( this, jQuery.parseHTML( |
|
2736 match[1], |
|
2737 context && context.nodeType ? context.ownerDocument || context : document, |
|
2738 true |
|
2739 ) ); |
|
2740 |
|
2741 // HANDLE: $(html, props) |
|
2742 if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { |
|
2743 for ( match in context ) { |
|
2744 // Properties of context are called as methods if possible |
|
2745 if ( jQuery.isFunction( this[ match ] ) ) { |
|
2746 this[ match ]( context[ match ] ); |
|
2747 |
|
2748 // ...and otherwise set as attributes |
|
2749 } else { |
|
2750 this.attr( match, context[ match ] ); |
|
2751 } |
|
2752 } |
|
2753 } |
|
2754 |
|
2755 return this; |
|
2756 |
|
2757 // HANDLE: $(#id) |
|
2758 } else { |
|
2759 elem = document.getElementById( match[2] ); |
|
2760 |
|
2761 // Check parentNode to catch when Blackberry 4.6 returns |
|
2762 // nodes that are no longer in the document #6963 |
|
2763 if ( elem && elem.parentNode ) { |
|
2764 // Inject the element directly into the jQuery object |
|
2765 this.length = 1; |
|
2766 this[0] = elem; |
|
2767 } |
|
2768 |
|
2769 this.context = document; |
|
2770 this.selector = selector; |
|
2771 return this; |
|
2772 } |
|
2773 |
|
2774 // HANDLE: $(expr, $(...)) |
|
2775 } else if ( !context || context.jquery ) { |
|
2776 return ( context || rootjQuery ).find( selector ); |
|
2777 |
|
2778 // HANDLE: $(expr, context) |
|
2779 // (which is just equivalent to: $(context).find(expr) |
|
2780 } else { |
|
2781 return this.constructor( context ).find( selector ); |
|
2782 } |
|
2783 |
|
2784 // HANDLE: $(DOMElement) |
|
2785 } else if ( selector.nodeType ) { |
|
2786 this.context = this[0] = selector; |
|
2787 this.length = 1; |
|
2788 return this; |
|
2789 |
|
2790 // HANDLE: $(function) |
|
2791 // Shortcut for document ready |
|
2792 } else if ( jQuery.isFunction( selector ) ) { |
|
2793 return typeof rootjQuery.ready !== "undefined" ? |
|
2794 rootjQuery.ready( selector ) : |
|
2795 // Execute immediately if ready is not present |
|
2796 selector( jQuery ); |
|
2797 } |
|
2798 |
|
2799 if ( selector.selector !== undefined ) { |
|
2800 this.selector = selector.selector; |
|
2801 this.context = selector.context; |
|
2802 } |
|
2803 |
|
2804 return jQuery.makeArray( selector, this ); |
|
2805 }; |
|
2806 |
|
2807 // Give the init function the jQuery prototype for later instantiation |
|
2808 init.prototype = jQuery.fn; |
|
2809 |
|
2810 // Initialize central reference |
|
2811 rootjQuery = jQuery( document ); |
|
2812 |
|
2813 |
|
2814 var rparentsprev = /^(?:parents|prev(?:Until|All))/, |
|
2815 // methods guaranteed to produce a unique set when starting from a unique set |
|
2816 guaranteedUnique = { |
|
2817 children: true, |
|
2818 contents: true, |
|
2819 next: true, |
|
2820 prev: true |
|
2821 }; |
|
2822 |
|
2823 jQuery.extend({ |
|
2824 dir: function( elem, dir, until ) { |
|
2825 var matched = [], |
|
2826 truncate = until !== undefined; |
|
2827 |
|
2828 while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) { |
|
2829 if ( elem.nodeType === 1 ) { |
|
2830 if ( truncate && jQuery( elem ).is( until ) ) { |
|
2831 break; |
|
2832 } |
|
2833 matched.push( elem ); |
|
2834 } |
|
2835 } |
|
2836 return matched; |
|
2837 }, |
|
2838 |
|
2839 sibling: function( n, elem ) { |
|
2840 var matched = []; |
|
2841 |
|
2842 for ( ; n; n = n.nextSibling ) { |
|
2843 if ( n.nodeType === 1 && n !== elem ) { |
|
2844 matched.push( n ); |
|
2845 } |
|
2846 } |
|
2847 |
|
2848 return matched; |
|
2849 } |
|
2850 }); |
|
2851 |
|
2852 jQuery.fn.extend({ |
|
2853 has: function( target ) { |
|
2854 var targets = jQuery( target, this ), |
|
2855 l = targets.length; |
|
2856 |
|
2857 return this.filter(function() { |
|
2858 var i = 0; |
|
2859 for ( ; i < l; i++ ) { |
|
2860 if ( jQuery.contains( this, targets[i] ) ) { |
|
2861 return true; |
|
2862 } |
|
2863 } |
|
2864 }); |
|
2865 }, |
|
2866 |
|
2867 closest: function( selectors, context ) { |
|
2868 var cur, |
|
2869 i = 0, |
|
2870 l = this.length, |
|
2871 matched = [], |
|
2872 pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? |
|
2873 jQuery( selectors, context || this.context ) : |
|
2874 0; |
|
2875 |
|
2876 for ( ; i < l; i++ ) { |
|
2877 for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { |
|
2878 // Always skip document fragments |
|
2879 if ( cur.nodeType < 11 && (pos ? |
|
2880 pos.index(cur) > -1 : |
|
2881 |
|
2882 // Don't pass non-elements to Sizzle |
|
2883 cur.nodeType === 1 && |
|
2884 jQuery.find.matchesSelector(cur, selectors)) ) { |
|
2885 |
|
2886 matched.push( cur ); |
|
2887 break; |
|
2888 } |
|
2889 } |
|
2890 } |
|
2891 |
|
2892 return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); |
|
2893 }, |
|
2894 |
|
2895 // Determine the position of an element within |
|
2896 // the matched set of elements |
|
2897 index: function( elem ) { |
|
2898 |
|
2899 // No argument, return index in parent |
|
2900 if ( !elem ) { |
|
2901 return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; |
|
2902 } |
|
2903 |
|
2904 // index in selector |
|
2905 if ( typeof elem === "string" ) { |
|
2906 return indexOf.call( jQuery( elem ), this[ 0 ] ); |
|
2907 } |
|
2908 |
|
2909 // Locate the position of the desired element |
|
2910 return indexOf.call( this, |
|
2911 |
|
2912 // If it receives a jQuery object, the first element is used |
|
2913 elem.jquery ? elem[ 0 ] : elem |
|
2914 ); |
|
2915 }, |
|
2916 |
|
2917 add: function( selector, context ) { |
|
2918 return this.pushStack( |
|
2919 jQuery.unique( |
|
2920 jQuery.merge( this.get(), jQuery( selector, context ) ) |
|
2921 ) |
|
2922 ); |
|
2923 }, |
|
2924 |
|
2925 addBack: function( selector ) { |
|
2926 return this.add( selector == null ? |
|
2927 this.prevObject : this.prevObject.filter(selector) |
|
2928 ); |
|
2929 } |
|
2930 }); |
|
2931 |
|
2932 function sibling( cur, dir ) { |
|
2933 while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {} |
|
2934 return cur; |
|
2935 } |
|
2936 |
|
2937 jQuery.each({ |
|
2938 parent: function( elem ) { |
|
2939 var parent = elem.parentNode; |
|
2940 return parent && parent.nodeType !== 11 ? parent : null; |
|
2941 }, |
|
2942 parents: function( elem ) { |
|
2943 return jQuery.dir( elem, "parentNode" ); |
|
2944 }, |
|
2945 parentsUntil: function( elem, i, until ) { |
|
2946 return jQuery.dir( elem, "parentNode", until ); |
|
2947 }, |
|
2948 next: function( elem ) { |
|
2949 return sibling( elem, "nextSibling" ); |
|
2950 }, |
|
2951 prev: function( elem ) { |
|
2952 return sibling( elem, "previousSibling" ); |
|
2953 }, |
|
2954 nextAll: function( elem ) { |
|
2955 return jQuery.dir( elem, "nextSibling" ); |
|
2956 }, |
|
2957 prevAll: function( elem ) { |
|
2958 return jQuery.dir( elem, "previousSibling" ); |
|
2959 }, |
|
2960 nextUntil: function( elem, i, until ) { |
|
2961 return jQuery.dir( elem, "nextSibling", until ); |
|
2962 }, |
|
2963 prevUntil: function( elem, i, until ) { |
|
2964 return jQuery.dir( elem, "previousSibling", until ); |
|
2965 }, |
|
2966 siblings: function( elem ) { |
|
2967 return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); |
|
2968 }, |
|
2969 children: function( elem ) { |
|
2970 return jQuery.sibling( elem.firstChild ); |
|
2971 }, |
|
2972 contents: function( elem ) { |
|
2973 return elem.contentDocument || jQuery.merge( [], elem.childNodes ); |
|
2974 } |
|
2975 }, function( name, fn ) { |
|
2976 jQuery.fn[ name ] = function( until, selector ) { |
|
2977 var matched = jQuery.map( this, fn, until ); |
|
2978 |
|
2979 if ( name.slice( -5 ) !== "Until" ) { |
|
2980 selector = until; |
|
2981 } |
|
2982 |
|
2983 if ( selector && typeof selector === "string" ) { |
|
2984 matched = jQuery.filter( selector, matched ); |
|
2985 } |
|
2986 |
|
2987 if ( this.length > 1 ) { |
|
2988 // Remove duplicates |
|
2989 if ( !guaranteedUnique[ name ] ) { |
|
2990 jQuery.unique( matched ); |
|
2991 } |
|
2992 |
|
2993 // Reverse order for parents* and prev-derivatives |
|
2994 if ( rparentsprev.test( name ) ) { |
|
2995 matched.reverse(); |
|
2996 } |
|
2997 } |
|
2998 |
|
2999 return this.pushStack( matched ); |
|
3000 }; |
|
3001 }); |
|
3002 var rnotwhite = (/\S+/g); |
|
3003 |
|
3004 |
|
3005 |
|
3006 // String to Object options format cache |
|
3007 var optionsCache = {}; |
|
3008 |
|
3009 // Convert String-formatted options into Object-formatted ones and store in cache |
|
3010 function createOptions( options ) { |
|
3011 var object = optionsCache[ options ] = {}; |
|
3012 jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { |
|
3013 object[ flag ] = true; |
|
3014 }); |
|
3015 return object; |
|
3016 } |
|
3017 |
|
3018 /* |
|
3019 * Create a callback list using the following parameters: |
|
3020 * |
|
3021 * options: an optional list of space-separated options that will change how |
|
3022 * the callback list behaves or a more traditional option object |
|
3023 * |
|
3024 * By default a callback list will act like an event callback list and can be |
|
3025 * "fired" multiple times. |
|
3026 * |
|
3027 * Possible options: |
|
3028 * |
|
3029 * once: will ensure the callback list can only be fired once (like a Deferred) |
|
3030 * |
|
3031 * memory: will keep track of previous values and will call any callback added |
|
3032 * after the list has been fired right away with the latest "memorized" |
|
3033 * values (like a Deferred) |
|
3034 * |
|
3035 * unique: will ensure a callback can only be added once (no duplicate in the list) |
|
3036 * |
|
3037 * stopOnFalse: interrupt callings when a callback returns false |
|
3038 * |
|
3039 */ |
|
3040 jQuery.Callbacks = function( options ) { |
|
3041 |
|
3042 // Convert options from String-formatted to Object-formatted if needed |
|
3043 // (we check in cache first) |
|
3044 options = typeof options === "string" ? |
|
3045 ( optionsCache[ options ] || createOptions( options ) ) : |
|
3046 jQuery.extend( {}, options ); |
|
3047 |
|
3048 var // Last fire value (for non-forgettable lists) |
|
3049 memory, |
|
3050 // Flag to know if list was already fired |
|
3051 fired, |
|
3052 // Flag to know if list is currently firing |
|
3053 firing, |
|
3054 // First callback to fire (used internally by add and fireWith) |
|
3055 firingStart, |
|
3056 // End of the loop when firing |
|
3057 firingLength, |
|
3058 // Index of currently firing callback (modified by remove if needed) |
|
3059 firingIndex, |
|
3060 // Actual callback list |
|
3061 list = [], |
|
3062 // Stack of fire calls for repeatable lists |
|
3063 stack = !options.once && [], |
|
3064 // Fire callbacks |
|
3065 fire = function( data ) { |
|
3066 memory = options.memory && data; |
|
3067 fired = true; |
|
3068 firingIndex = firingStart || 0; |
|
3069 firingStart = 0; |
|
3070 firingLength = list.length; |
|
3071 firing = true; |
|
3072 for ( ; list && firingIndex < firingLength; firingIndex++ ) { |
|
3073 if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { |
|
3074 memory = false; // To prevent further calls using add |
|
3075 break; |
|
3076 } |
|
3077 } |
|
3078 firing = false; |
|
3079 if ( list ) { |
|
3080 if ( stack ) { |
|
3081 if ( stack.length ) { |
|
3082 fire( stack.shift() ); |
|
3083 } |
|
3084 } else if ( memory ) { |
|
3085 list = []; |
|
3086 } else { |
|
3087 self.disable(); |
|
3088 } |
|
3089 } |
|
3090 }, |
|
3091 // Actual Callbacks object |
|
3092 self = { |
|
3093 // Add a callback or a collection of callbacks to the list |
|
3094 add: function() { |
|
3095 if ( list ) { |
|
3096 // First, we save the current length |
|
3097 var start = list.length; |
|
3098 (function add( args ) { |
|
3099 jQuery.each( args, function( _, arg ) { |
|
3100 var type = jQuery.type( arg ); |
|
3101 if ( type === "function" ) { |
|
3102 if ( !options.unique || !self.has( arg ) ) { |
|
3103 list.push( arg ); |
|
3104 } |
|
3105 } else if ( arg && arg.length && type !== "string" ) { |
|
3106 // Inspect recursively |
|
3107 add( arg ); |
|
3108 } |
|
3109 }); |
|
3110 })( arguments ); |
|
3111 // Do we need to add the callbacks to the |
|
3112 // current firing batch? |
|
3113 if ( firing ) { |
|
3114 firingLength = list.length; |
|
3115 // With memory, if we're not firing then |
|
3116 // we should call right away |
|
3117 } else if ( memory ) { |
|
3118 firingStart = start; |
|
3119 fire( memory ); |
|
3120 } |
|
3121 } |
|
3122 return this; |
|
3123 }, |
|
3124 // Remove a callback from the list |
|
3125 remove: function() { |
|
3126 if ( list ) { |
|
3127 jQuery.each( arguments, function( _, arg ) { |
|
3128 var index; |
|
3129 while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { |
|
3130 list.splice( index, 1 ); |
|
3131 // Handle firing indexes |
|
3132 if ( firing ) { |
|
3133 if ( index <= firingLength ) { |
|
3134 firingLength--; |
|
3135 } |
|
3136 if ( index <= firingIndex ) { |
|
3137 firingIndex--; |
|
3138 } |
|
3139 } |
|
3140 } |
|
3141 }); |
|
3142 } |
|
3143 return this; |
|
3144 }, |
|
3145 // Check if a given callback is in the list. |
|
3146 // If no argument is given, return whether or not list has callbacks attached. |
|
3147 has: function( fn ) { |
|
3148 return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); |
|
3149 }, |
|
3150 // Remove all callbacks from the list |
|
3151 empty: function() { |
|
3152 list = []; |
|
3153 firingLength = 0; |
|
3154 return this; |
|
3155 }, |
|
3156 // Have the list do nothing anymore |
|
3157 disable: function() { |
|
3158 list = stack = memory = undefined; |
|
3159 return this; |
|
3160 }, |
|
3161 // Is it disabled? |
|
3162 disabled: function() { |
|
3163 return !list; |
|
3164 }, |
|
3165 // Lock the list in its current state |
|
3166 lock: function() { |
|
3167 stack = undefined; |
|
3168 if ( !memory ) { |
|
3169 self.disable(); |
|
3170 } |
|
3171 return this; |
|
3172 }, |
|
3173 // Is it locked? |
|
3174 locked: function() { |
|
3175 return !stack; |
|
3176 }, |
|
3177 // Call all callbacks with the given context and arguments |
|
3178 fireWith: function( context, args ) { |
|
3179 if ( list && ( !fired || stack ) ) { |
|
3180 args = args || []; |
|
3181 args = [ context, args.slice ? args.slice() : args ]; |
|
3182 if ( firing ) { |
|
3183 stack.push( args ); |
|
3184 } else { |
|
3185 fire( args ); |
|
3186 } |
|
3187 } |
|
3188 return this; |
|
3189 }, |
|
3190 // Call all the callbacks with the given arguments |
|
3191 fire: function() { |
|
3192 self.fireWith( this, arguments ); |
|
3193 return this; |
|
3194 }, |
|
3195 // To know if the callbacks have already been called at least once |
|
3196 fired: function() { |
|
3197 return !!fired; |
|
3198 } |
|
3199 }; |
|
3200 |
|
3201 return self; |
|
3202 }; |
|
3203 |
|
3204 |
|
3205 jQuery.extend({ |
|
3206 |
|
3207 Deferred: function( func ) { |
|
3208 var tuples = [ |
|
3209 // action, add listener, listener list, final state |
|
3210 [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], |
|
3211 [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], |
|
3212 [ "notify", "progress", jQuery.Callbacks("memory") ] |
|
3213 ], |
|
3214 state = "pending", |
|
3215 promise = { |
|
3216 state: function() { |
|
3217 return state; |
|
3218 }, |
|
3219 always: function() { |
|
3220 deferred.done( arguments ).fail( arguments ); |
|
3221 return this; |
|
3222 }, |
|
3223 then: function( /* fnDone, fnFail, fnProgress */ ) { |
|
3224 var fns = arguments; |
|
3225 return jQuery.Deferred(function( newDefer ) { |
|
3226 jQuery.each( tuples, function( i, tuple ) { |
|
3227 var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; |
|
3228 // deferred[ done | fail | progress ] for forwarding actions to newDefer |
|
3229 deferred[ tuple[1] ](function() { |
|
3230 var returned = fn && fn.apply( this, arguments ); |
|
3231 if ( returned && jQuery.isFunction( returned.promise ) ) { |
|
3232 returned.promise() |
|
3233 .done( newDefer.resolve ) |
|
3234 .fail( newDefer.reject ) |
|
3235 .progress( newDefer.notify ); |
|
3236 } else { |
|
3237 newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); |
|
3238 } |
|
3239 }); |
|
3240 }); |
|
3241 fns = null; |
|
3242 }).promise(); |
|
3243 }, |
|
3244 // Get a promise for this deferred |
|
3245 // If obj is provided, the promise aspect is added to the object |
|
3246 promise: function( obj ) { |
|
3247 return obj != null ? jQuery.extend( obj, promise ) : promise; |
|
3248 } |
|
3249 }, |
|
3250 deferred = {}; |
|
3251 |
|
3252 // Keep pipe for back-compat |
|
3253 promise.pipe = promise.then; |
|
3254 |
|
3255 // Add list-specific methods |
|
3256 jQuery.each( tuples, function( i, tuple ) { |
|
3257 var list = tuple[ 2 ], |
|
3258 stateString = tuple[ 3 ]; |
|
3259 |
|
3260 // promise[ done | fail | progress ] = list.add |
|
3261 promise[ tuple[1] ] = list.add; |
|
3262 |
|
3263 // Handle state |
|
3264 if ( stateString ) { |
|
3265 list.add(function() { |
|
3266 // state = [ resolved | rejected ] |
|
3267 state = stateString; |
|
3268 |
|
3269 // [ reject_list | resolve_list ].disable; progress_list.lock |
|
3270 }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); |
|
3271 } |
|
3272 |
|
3273 // deferred[ resolve | reject | notify ] |
|
3274 deferred[ tuple[0] ] = function() { |
|
3275 deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); |
|
3276 return this; |
|
3277 }; |
|
3278 deferred[ tuple[0] + "With" ] = list.fireWith; |
|
3279 }); |
|
3280 |
|
3281 // Make the deferred a promise |
|
3282 promise.promise( deferred ); |
|
3283 |
|
3284 // Call given func if any |
|
3285 if ( func ) { |
|
3286 func.call( deferred, deferred ); |
|
3287 } |
|
3288 |
|
3289 // All done! |
|
3290 return deferred; |
|
3291 }, |
|
3292 |
|
3293 // Deferred helper |
|
3294 when: function( subordinate /* , ..., subordinateN */ ) { |
|
3295 var i = 0, |
|
3296 resolveValues = slice.call( arguments ), |
|
3297 length = resolveValues.length, |
|
3298 |
|
3299 // the count of uncompleted subordinates |
|
3300 remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, |
|
3301 |
|
3302 // the master Deferred. If resolveValues consist of only a single Deferred, just use that. |
|
3303 deferred = remaining === 1 ? subordinate : jQuery.Deferred(), |
|
3304 |
|
3305 // Update function for both resolve and progress values |
|
3306 updateFunc = function( i, contexts, values ) { |
|
3307 return function( value ) { |
|
3308 contexts[ i ] = this; |
|
3309 values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; |
|
3310 if ( values === progressValues ) { |
|
3311 deferred.notifyWith( contexts, values ); |
|
3312 } else if ( !( --remaining ) ) { |
|
3313 deferred.resolveWith( contexts, values ); |
|
3314 } |
|
3315 }; |
|
3316 }, |
|
3317 |
|
3318 progressValues, progressContexts, resolveContexts; |
|
3319 |
|
3320 // add listeners to Deferred subordinates; treat others as resolved |
|
3321 if ( length > 1 ) { |
|
3322 progressValues = new Array( length ); |
|
3323 progressContexts = new Array( length ); |
|
3324 resolveContexts = new Array( length ); |
|
3325 for ( ; i < length; i++ ) { |
|
3326 if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { |
|
3327 resolveValues[ i ].promise() |
|
3328 .done( updateFunc( i, resolveContexts, resolveValues ) ) |
|
3329 .fail( deferred.reject ) |
|
3330 .progress( updateFunc( i, progressContexts, progressValues ) ); |
|
3331 } else { |
|
3332 --remaining; |
|
3333 } |
|
3334 } |
|
3335 } |
|
3336 |
|
3337 // if we're not waiting on anything, resolve the master |
|
3338 if ( !remaining ) { |
|
3339 deferred.resolveWith( resolveContexts, resolveValues ); |
|
3340 } |
|
3341 |
|
3342 return deferred.promise(); |
|
3343 } |
|
3344 }); |
|
3345 |
|
3346 |
|
3347 // The deferred used on DOM ready |
|
3348 var readyList; |
|
3349 |
|
3350 jQuery.fn.ready = function( fn ) { |
|
3351 // Add the callback |
|
3352 jQuery.ready.promise().done( fn ); |
|
3353 |
|
3354 return this; |
|
3355 }; |
|
3356 |
|
3357 jQuery.extend({ |
|
3358 // Is the DOM ready to be used? Set to true once it occurs. |
|
3359 isReady: false, |
|
3360 |
|
3361 // A counter to track how many items to wait for before |
|
3362 // the ready event fires. See #6781 |
|
3363 readyWait: 1, |
|
3364 |
|
3365 // Hold (or release) the ready event |
|
3366 holdReady: function( hold ) { |
|
3367 if ( hold ) { |
|
3368 jQuery.readyWait++; |
|
3369 } else { |
|
3370 jQuery.ready( true ); |
|
3371 } |
|
3372 }, |
|
3373 |
|
3374 // Handle when the DOM is ready |
|
3375 ready: function( wait ) { |
|
3376 |
|
3377 // Abort if there are pending holds or we're already ready |
|
3378 if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { |
|
3379 return; |
|
3380 } |
|
3381 |
|
3382 // Remember that the DOM is ready |
|
3383 jQuery.isReady = true; |
|
3384 |
|
3385 // If a normal DOM Ready event fired, decrement, and wait if need be |
|
3386 if ( wait !== true && --jQuery.readyWait > 0 ) { |
|
3387 return; |
|
3388 } |
|
3389 |
|
3390 // If there are functions bound, to execute |
|
3391 readyList.resolveWith( document, [ jQuery ] ); |
|
3392 |
|
3393 // Trigger any bound ready events |
|
3394 if ( jQuery.fn.triggerHandler ) { |
|
3395 jQuery( document ).triggerHandler( "ready" ); |
|
3396 jQuery( document ).off( "ready" ); |
|
3397 } |
|
3398 } |
|
3399 }); |
|
3400 |
|
3401 /** |
|
3402 * The ready event handler and self cleanup method |
|
3403 */ |
|
3404 function completed() { |
|
3405 document.removeEventListener( "DOMContentLoaded", completed, false ); |
|
3406 window.removeEventListener( "load", completed, false ); |
|
3407 jQuery.ready(); |
|
3408 } |
|
3409 |
|
3410 jQuery.ready.promise = function( obj ) { |
|
3411 if ( !readyList ) { |
|
3412 |
|
3413 readyList = jQuery.Deferred(); |
|
3414 |
|
3415 // Catch cases where $(document).ready() is called after the browser event has already occurred. |
|
3416 // we once tried to use readyState "interactive" here, but it caused issues like the one |
|
3417 // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 |
|
3418 if ( document.readyState === "complete" ) { |
|
3419 // Handle it asynchronously to allow scripts the opportunity to delay ready |
|
3420 setTimeout( jQuery.ready ); |
|
3421 |
|
3422 } else { |
|
3423 |
|
3424 // Use the handy event callback |
|
3425 document.addEventListener( "DOMContentLoaded", completed, false ); |
|
3426 |
|
3427 // A fallback to window.onload, that will always work |
|
3428 window.addEventListener( "load", completed, false ); |
|
3429 } |
|
3430 } |
|
3431 return readyList.promise( obj ); |
|
3432 }; |
|
3433 |
|
3434 // Kick off the DOM ready check even if the user does not |
|
3435 jQuery.ready.promise(); |
|
3436 |
|
3437 |
|
3438 |
|
3439 |
|
3440 // Multifunctional method to get and set values of a collection |
|
3441 // The value/s can optionally be executed if it's a function |
|
3442 var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { |
|
3443 var i = 0, |
|
3444 len = elems.length, |
|
3445 bulk = key == null; |
|
3446 |
|
3447 // Sets many values |
|
3448 if ( jQuery.type( key ) === "object" ) { |
|
3449 chainable = true; |
|
3450 for ( i in key ) { |
|
3451 jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); |
|
3452 } |
|
3453 |
|
3454 // Sets one value |
|
3455 } else if ( value !== undefined ) { |
|
3456 chainable = true; |
|
3457 |
|
3458 if ( !jQuery.isFunction( value ) ) { |
|
3459 raw = true; |
|
3460 } |
|
3461 |
|
3462 if ( bulk ) { |
|
3463 // Bulk operations run against the entire set |
|
3464 if ( raw ) { |
|
3465 fn.call( elems, value ); |
|
3466 fn = null; |
|
3467 |
|
3468 // ...except when executing function values |
|
3469 } else { |
|
3470 bulk = fn; |
|
3471 fn = function( elem, key, value ) { |
|
3472 return bulk.call( jQuery( elem ), value ); |
|
3473 }; |
|
3474 } |
|
3475 } |
|
3476 |
|
3477 if ( fn ) { |
|
3478 for ( ; i < len; i++ ) { |
|
3479 fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); |
|
3480 } |
|
3481 } |
|
3482 } |
|
3483 |
|
3484 return chainable ? |
|
3485 elems : |
|
3486 |
|
3487 // Gets |
|
3488 bulk ? |
|
3489 fn.call( elems ) : |
|
3490 len ? fn( elems[0], key ) : emptyGet; |
|
3491 }; |
|
3492 |
|
3493 |
|
3494 /** |
|
3495 * Determines whether an object can have data |
|
3496 */ |
|
3497 jQuery.acceptData = function( owner ) { |
|
3498 // Accepts only: |
|
3499 // - Node |
|
3500 // - Node.ELEMENT_NODE |
|
3501 // - Node.DOCUMENT_NODE |
|
3502 // - Object |
|
3503 // - Any |
|
3504 /* jshint -W018 */ |
|
3505 return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); |
|
3506 }; |
|
3507 |
|
3508 |
|
3509 function Data() { |
|
3510 // Support: Android < 4, |
|
3511 // Old WebKit does not have Object.preventExtensions/freeze method, |
|
3512 // return new empty object instead with no [[set]] accessor |
|
3513 Object.defineProperty( this.cache = {}, 0, { |
|
3514 get: function() { |
|
3515 return {}; |
|
3516 } |
|
3517 }); |
|
3518 |
|
3519 this.expando = jQuery.expando + Math.random(); |
|
3520 } |
|
3521 |
|
3522 Data.uid = 1; |
|
3523 Data.accepts = jQuery.acceptData; |
|
3524 |
|
3525 Data.prototype = { |
|
3526 key: function( owner ) { |
|
3527 // We can accept data for non-element nodes in modern browsers, |
|
3528 // but we should not, see #8335. |
|
3529 // Always return the key for a frozen object. |
|
3530 if ( !Data.accepts( owner ) ) { |
|
3531 return 0; |
|
3532 } |
|
3533 |
|
3534 var descriptor = {}, |
|
3535 // Check if the owner object already has a cache key |
|
3536 unlock = owner[ this.expando ]; |
|
3537 |
|
3538 // If not, create one |
|
3539 if ( !unlock ) { |
|
3540 unlock = Data.uid++; |
|
3541 |
|
3542 // Secure it in a non-enumerable, non-writable property |
|
3543 try { |
|
3544 descriptor[ this.expando ] = { value: unlock }; |
|
3545 Object.defineProperties( owner, descriptor ); |
|
3546 |
|
3547 // Support: Android < 4 |
|
3548 // Fallback to a less secure definition |
|
3549 } catch ( e ) { |
|
3550 descriptor[ this.expando ] = unlock; |
|
3551 jQuery.extend( owner, descriptor ); |
|
3552 } |
|
3553 } |
|
3554 |
|
3555 // Ensure the cache object |
|
3556 if ( !this.cache[ unlock ] ) { |
|
3557 this.cache[ unlock ] = {}; |
|
3558 } |
|
3559 |
|
3560 return unlock; |
|
3561 }, |
|
3562 set: function( owner, data, value ) { |
|
3563 var prop, |
|
3564 // There may be an unlock assigned to this node, |
|
3565 // if there is no entry for this "owner", create one inline |
|
3566 // and set the unlock as though an owner entry had always existed |
|
3567 unlock = this.key( owner ), |
|
3568 cache = this.cache[ unlock ]; |
|
3569 |
|
3570 // Handle: [ owner, key, value ] args |
|
3571 if ( typeof data === "string" ) { |
|
3572 cache[ data ] = value; |
|
3573 |
|
3574 // Handle: [ owner, { properties } ] args |
|
3575 } else { |
|
3576 // Fresh assignments by object are shallow copied |
|
3577 if ( jQuery.isEmptyObject( cache ) ) { |
|
3578 jQuery.extend( this.cache[ unlock ], data ); |
|
3579 // Otherwise, copy the properties one-by-one to the cache object |
|
3580 } else { |
|
3581 for ( prop in data ) { |
|
3582 cache[ prop ] = data[ prop ]; |
|
3583 } |
|
3584 } |
|
3585 } |
|
3586 return cache; |
|
3587 }, |
|
3588 get: function( owner, key ) { |
|
3589 // Either a valid cache is found, or will be created. |
|
3590 // New caches will be created and the unlock returned, |
|
3591 // allowing direct access to the newly created |
|
3592 // empty data object. A valid owner object must be provided. |
|
3593 var cache = this.cache[ this.key( owner ) ]; |
|
3594 |
|
3595 return key === undefined ? |
|
3596 cache : cache[ key ]; |
|
3597 }, |
|
3598 access: function( owner, key, value ) { |
|
3599 var stored; |
|
3600 // In cases where either: |
|
3601 // |
|
3602 // 1. No key was specified |
|
3603 // 2. A string key was specified, but no value provided |
|
3604 // |
|
3605 // Take the "read" path and allow the get method to determine |
|
3606 // which value to return, respectively either: |
|
3607 // |
|
3608 // 1. The entire cache object |
|
3609 // 2. The data stored at the key |
|
3610 // |
|
3611 if ( key === undefined || |
|
3612 ((key && typeof key === "string") && value === undefined) ) { |
|
3613 |
|
3614 stored = this.get( owner, key ); |
|
3615 |
|
3616 return stored !== undefined ? |
|
3617 stored : this.get( owner, jQuery.camelCase(key) ); |
|
3618 } |
|
3619 |
|
3620 // [*]When the key is not a string, or both a key and value |
|
3621 // are specified, set or extend (existing objects) with either: |
|
3622 // |
|
3623 // 1. An object of properties |
|
3624 // 2. A key and value |
|
3625 // |
|
3626 this.set( owner, key, value ); |
|
3627 |
|
3628 // Since the "set" path can have two possible entry points |
|
3629 // return the expected data based on which path was taken[*] |
|
3630 return value !== undefined ? value : key; |
|
3631 }, |
|
3632 remove: function( owner, key ) { |
|
3633 var i, name, camel, |
|
3634 unlock = this.key( owner ), |
|
3635 cache = this.cache[ unlock ]; |
|
3636 |
|
3637 if ( key === undefined ) { |
|
3638 this.cache[ unlock ] = {}; |
|
3639 |
|
3640 } else { |
|
3641 // Support array or space separated string of keys |
|
3642 if ( jQuery.isArray( key ) ) { |
|
3643 // If "name" is an array of keys... |
|
3644 // When data is initially created, via ("key", "val") signature, |
|
3645 // keys will be converted to camelCase. |
|
3646 // Since there is no way to tell _how_ a key was added, remove |
|
3647 // both plain key and camelCase key. #12786 |
|
3648 // This will only penalize the array argument path. |
|
3649 name = key.concat( key.map( jQuery.camelCase ) ); |
|
3650 } else { |
|
3651 camel = jQuery.camelCase( key ); |
|
3652 // Try the string as a key before any manipulation |
|
3653 if ( key in cache ) { |
|
3654 name = [ key, camel ]; |
|
3655 } else { |
|
3656 // If a key with the spaces exists, use it. |
|
3657 // Otherwise, create an array by matching non-whitespace |
|
3658 name = camel; |
|
3659 name = name in cache ? |
|
3660 [ name ] : ( name.match( rnotwhite ) || [] ); |
|
3661 } |
|
3662 } |
|
3663 |
|
3664 i = name.length; |
|
3665 while ( i-- ) { |
|
3666 delete cache[ name[ i ] ]; |
|
3667 } |
|
3668 } |
|
3669 }, |
|
3670 hasData: function( owner ) { |
|
3671 return !jQuery.isEmptyObject( |
|
3672 this.cache[ owner[ this.expando ] ] || {} |
|
3673 ); |
|
3674 }, |
|
3675 discard: function( owner ) { |
|
3676 if ( owner[ this.expando ] ) { |
|
3677 delete this.cache[ owner[ this.expando ] ]; |
|
3678 } |
|
3679 } |
|
3680 }; |
|
3681 var data_priv = new Data(); |
|
3682 |
|
3683 var data_user = new Data(); |
|
3684 |
|
3685 |
|
3686 |
|
3687 /* |
|
3688 Implementation Summary |
|
3689 |
|
3690 1. Enforce API surface and semantic compatibility with 1.9.x branch |
|
3691 2. Improve the module's maintainability by reducing the storage |
|
3692 paths to a single mechanism. |
|
3693 3. Use the same single mechanism to support "private" and "user" data. |
|
3694 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) |
|
3695 5. Avoid exposing implementation details on user objects (eg. expando properties) |
|
3696 6. Provide a clear path for implementation upgrade to WeakMap in 2014 |
|
3697 */ |
|
3698 var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, |
|
3699 rmultiDash = /([A-Z])/g; |
|
3700 |
|
3701 function dataAttr( elem, key, data ) { |
|
3702 var name; |
|
3703 |
|
3704 // If nothing was found internally, try to fetch any |
|
3705 // data from the HTML5 data-* attribute |
|
3706 if ( data === undefined && elem.nodeType === 1 ) { |
|
3707 name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); |
|
3708 data = elem.getAttribute( name ); |
|
3709 |
|
3710 if ( typeof data === "string" ) { |
|
3711 try { |
|
3712 data = data === "true" ? true : |
|
3713 data === "false" ? false : |
|
3714 data === "null" ? null : |
|
3715 // Only convert to a number if it doesn't change the string |
|
3716 +data + "" === data ? +data : |
|
3717 rbrace.test( data ) ? jQuery.parseJSON( data ) : |
|
3718 data; |
|
3719 } catch( e ) {} |
|
3720 |
|
3721 // Make sure we set the data so it isn't changed later |
|
3722 data_user.set( elem, key, data ); |
|
3723 } else { |
|
3724 data = undefined; |
|
3725 } |
|
3726 } |
|
3727 return data; |
|
3728 } |
|
3729 |
|
3730 jQuery.extend({ |
|
3731 hasData: function( elem ) { |
|
3732 return data_user.hasData( elem ) || data_priv.hasData( elem ); |
|
3733 }, |
|
3734 |
|
3735 data: function( elem, name, data ) { |
|
3736 return data_user.access( elem, name, data ); |
|
3737 }, |
|
3738 |
|
3739 removeData: function( elem, name ) { |
|
3740 data_user.remove( elem, name ); |
|
3741 }, |
|
3742 |
|
3743 // TODO: Now that all calls to _data and _removeData have been replaced |
|
3744 // with direct calls to data_priv methods, these can be deprecated. |
|
3745 _data: function( elem, name, data ) { |
|
3746 return data_priv.access( elem, name, data ); |
|
3747 }, |
|
3748 |
|
3749 _removeData: function( elem, name ) { |
|
3750 data_priv.remove( elem, name ); |
|
3751 } |
|
3752 }); |
|
3753 |
|
3754 jQuery.fn.extend({ |
|
3755 data: function( key, value ) { |
|
3756 var i, name, data, |
|
3757 elem = this[ 0 ], |
|
3758 attrs = elem && elem.attributes; |
|
3759 |
|
3760 // Gets all values |
|
3761 if ( key === undefined ) { |
|
3762 if ( this.length ) { |
|
3763 data = data_user.get( elem ); |
|
3764 |
|
3765 if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) { |
|
3766 i = attrs.length; |
|
3767 while ( i-- ) { |
|
3768 |
|
3769 // Support: IE11+ |
|
3770 // The attrs elements can be null (#14894) |
|
3771 if ( attrs[ i ] ) { |
|
3772 name = attrs[ i ].name; |
|
3773 if ( name.indexOf( "data-" ) === 0 ) { |
|
3774 name = jQuery.camelCase( name.slice(5) ); |
|
3775 dataAttr( elem, name, data[ name ] ); |
|
3776 } |
|
3777 } |
|
3778 } |
|
3779 data_priv.set( elem, "hasDataAttrs", true ); |
|
3780 } |
|
3781 } |
|
3782 |
|
3783 return data; |
|
3784 } |
|
3785 |
|
3786 // Sets multiple values |
|
3787 if ( typeof key === "object" ) { |
|
3788 return this.each(function() { |
|
3789 data_user.set( this, key ); |
|
3790 }); |
|
3791 } |
|
3792 |
|
3793 return access( this, function( value ) { |
|
3794 var data, |
|
3795 camelKey = jQuery.camelCase( key ); |
|
3796 |
|
3797 // The calling jQuery object (element matches) is not empty |
|
3798 // (and therefore has an element appears at this[ 0 ]) and the |
|
3799 // `value` parameter was not undefined. An empty jQuery object |
|
3800 // will result in `undefined` for elem = this[ 0 ] which will |
|
3801 // throw an exception if an attempt to read a data cache is made. |
|
3802 if ( elem && value === undefined ) { |
|
3803 // Attempt to get data from the cache |
|
3804 // with the key as-is |
|
3805 data = data_user.get( elem, key ); |
|
3806 if ( data !== undefined ) { |
|
3807 return data; |
|
3808 } |
|
3809 |
|
3810 // Attempt to get data from the cache |
|
3811 // with the key camelized |
|
3812 data = data_user.get( elem, camelKey ); |
|
3813 if ( data !== undefined ) { |
|
3814 return data; |
|
3815 } |
|
3816 |
|
3817 // Attempt to "discover" the data in |
|
3818 // HTML5 custom data-* attrs |
|
3819 data = dataAttr( elem, camelKey, undefined ); |
|
3820 if ( data !== undefined ) { |
|
3821 return data; |
|
3822 } |
|
3823 |
|
3824 // We tried really hard, but the data doesn't exist. |
|
3825 return; |
|
3826 } |
|
3827 |
|
3828 // Set the data... |
|
3829 this.each(function() { |
|
3830 // First, attempt to store a copy or reference of any |
|
3831 // data that might've been store with a camelCased key. |
|
3832 var data = data_user.get( this, camelKey ); |
|
3833 |
|
3834 // For HTML5 data-* attribute interop, we have to |
|
3835 // store property names with dashes in a camelCase form. |
|
3836 // This might not apply to all properties...* |
|
3837 data_user.set( this, camelKey, value ); |
|
3838 |
|
3839 // *... In the case of properties that might _actually_ |
|
3840 // have dashes, we need to also store a copy of that |
|
3841 // unchanged property. |
|
3842 if ( key.indexOf("-") !== -1 && data !== undefined ) { |
|
3843 data_user.set( this, key, value ); |
|
3844 } |
|
3845 }); |
|
3846 }, null, value, arguments.length > 1, null, true ); |
|
3847 }, |
|
3848 |
|
3849 removeData: function( key ) { |
|
3850 return this.each(function() { |
|
3851 data_user.remove( this, key ); |
|
3852 }); |
|
3853 } |
|
3854 }); |
|
3855 |
|
3856 |
|
3857 jQuery.extend({ |
|
3858 queue: function( elem, type, data ) { |
|
3859 var queue; |
|
3860 |
|
3861 if ( elem ) { |
|
3862 type = ( type || "fx" ) + "queue"; |
|
3863 queue = data_priv.get( elem, type ); |
|
3864 |
|
3865 // Speed up dequeue by getting out quickly if this is just a lookup |
|
3866 if ( data ) { |
|
3867 if ( !queue || jQuery.isArray( data ) ) { |
|
3868 queue = data_priv.access( elem, type, jQuery.makeArray(data) ); |
|
3869 } else { |
|
3870 queue.push( data ); |
|
3871 } |
|
3872 } |
|
3873 return queue || []; |
|
3874 } |
|
3875 }, |
|
3876 |
|
3877 dequeue: function( elem, type ) { |
|
3878 type = type || "fx"; |
|
3879 |
|
3880 var queue = jQuery.queue( elem, type ), |
|
3881 startLength = queue.length, |
|
3882 fn = queue.shift(), |
|
3883 hooks = jQuery._queueHooks( elem, type ), |
|
3884 next = function() { |
|
3885 jQuery.dequeue( elem, type ); |
|
3886 }; |
|
3887 |
|
3888 // If the fx queue is dequeued, always remove the progress sentinel |
|
3889 if ( fn === "inprogress" ) { |
|
3890 fn = queue.shift(); |
|
3891 startLength--; |
|
3892 } |
|
3893 |
|
3894 if ( fn ) { |
|
3895 |
|
3896 // Add a progress sentinel to prevent the fx queue from being |
|
3897 // automatically dequeued |
|
3898 if ( type === "fx" ) { |
|
3899 queue.unshift( "inprogress" ); |
|
3900 } |
|
3901 |
|
3902 // clear up the last queue stop function |
|
3903 delete hooks.stop; |
|
3904 fn.call( elem, next, hooks ); |
|
3905 } |
|
3906 |
|
3907 if ( !startLength && hooks ) { |
|
3908 hooks.empty.fire(); |
|
3909 } |
|
3910 }, |
|
3911 |
|
3912 // not intended for public consumption - generates a queueHooks object, or returns the current one |
|
3913 _queueHooks: function( elem, type ) { |
|
3914 var key = type + "queueHooks"; |
|
3915 return data_priv.get( elem, key ) || data_priv.access( elem, key, { |
|
3916 empty: jQuery.Callbacks("once memory").add(function() { |
|
3917 data_priv.remove( elem, [ type + "queue", key ] ); |
|
3918 }) |
|
3919 }); |
|
3920 } |
|
3921 }); |
|
3922 |
|
3923 jQuery.fn.extend({ |
|
3924 queue: function( type, data ) { |
|
3925 var setter = 2; |
|
3926 |
|
3927 if ( typeof type !== "string" ) { |
|
3928 data = type; |
|
3929 type = "fx"; |
|
3930 setter--; |
|
3931 } |
|
3932 |
|
3933 if ( arguments.length < setter ) { |
|
3934 return jQuery.queue( this[0], type ); |
|
3935 } |
|
3936 |
|
3937 return data === undefined ? |
|
3938 this : |
|
3939 this.each(function() { |
|
3940 var queue = jQuery.queue( this, type, data ); |
|
3941 |
|
3942 // ensure a hooks for this queue |
|
3943 jQuery._queueHooks( this, type ); |
|
3944 |
|
3945 if ( type === "fx" && queue[0] !== "inprogress" ) { |
|
3946 jQuery.dequeue( this, type ); |
|
3947 } |
|
3948 }); |
|
3949 }, |
|
3950 dequeue: function( type ) { |
|
3951 return this.each(function() { |
|
3952 jQuery.dequeue( this, type ); |
|
3953 }); |
|
3954 }, |
|
3955 clearQueue: function( type ) { |
|
3956 return this.queue( type || "fx", [] ); |
|
3957 }, |
|
3958 // Get a promise resolved when queues of a certain type |
|
3959 // are emptied (fx is the type by default) |
|
3960 promise: function( type, obj ) { |
|
3961 var tmp, |
|
3962 count = 1, |
|
3963 defer = jQuery.Deferred(), |
|
3964 elements = this, |
|
3965 i = this.length, |
|
3966 resolve = function() { |
|
3967 if ( !( --count ) ) { |
|
3968 defer.resolveWith( elements, [ elements ] ); |
|
3969 } |
|
3970 }; |
|
3971 |
|
3972 if ( typeof type !== "string" ) { |
|
3973 obj = type; |
|
3974 type = undefined; |
|
3975 } |
|
3976 type = type || "fx"; |
|
3977 |
|
3978 while ( i-- ) { |
|
3979 tmp = data_priv.get( elements[ i ], type + "queueHooks" ); |
|
3980 if ( tmp && tmp.empty ) { |
|
3981 count++; |
|
3982 tmp.empty.add( resolve ); |
|
3983 } |
|
3984 } |
|
3985 resolve(); |
|
3986 return defer.promise( obj ); |
|
3987 } |
|
3988 }); |
|
3989 var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; |
|
3990 |
|
3991 var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; |
|
3992 |
|
3993 var isHidden = function( elem, el ) { |
|
3994 // isHidden might be called from jQuery#filter function; |
|
3995 // in that case, element will be second argument |
|
3996 elem = el || elem; |
|
3997 return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); |
|
3998 }; |
|
3999 |
|
4000 var rcheckableType = (/^(?:checkbox|radio)$/i); |
|
4001 |
|
4002 |
|
4003 |
|
4004 (function() { |
|
4005 var fragment = document.createDocumentFragment(), |
|
4006 div = fragment.appendChild( document.createElement( "div" ) ), |
|
4007 input = document.createElement( "input" ); |
|
4008 |
|
4009 // #11217 - WebKit loses check when the name is after the checked attribute |
|
4010 // Support: Windows Web Apps (WWA) |
|
4011 // `name` and `type` need .setAttribute for WWA |
|
4012 input.setAttribute( "type", "radio" ); |
|
4013 input.setAttribute( "checked", "checked" ); |
|
4014 input.setAttribute( "name", "t" ); |
|
4015 |
|
4016 div.appendChild( input ); |
|
4017 |
|
4018 // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 |
|
4019 // old WebKit doesn't clone checked state correctly in fragments |
|
4020 support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; |
|
4021 |
|
4022 // Make sure textarea (and checkbox) defaultValue is properly cloned |
|
4023 // Support: IE9-IE11+ |
|
4024 div.innerHTML = "<textarea>x</textarea>"; |
|
4025 support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; |
|
4026 })(); |
|
4027 var strundefined = typeof undefined; |
|
4028 |
|
4029 |
|
4030 |
|
4031 support.focusinBubbles = "onfocusin" in window; |
|
4032 |
|
4033 |
|
4034 var |
|
4035 rkeyEvent = /^key/, |
|
4036 rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/, |
|
4037 rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, |
|
4038 rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; |
|
4039 |
|
4040 function returnTrue() { |
|
4041 return true; |
|
4042 } |
|
4043 |
|
4044 function returnFalse() { |
|
4045 return false; |
|
4046 } |
|
4047 |
|
4048 function safeActiveElement() { |
|
4049 try { |
|
4050 return document.activeElement; |
|
4051 } catch ( err ) { } |
|
4052 } |
|
4053 |
|
4054 /* |
|
4055 * Helper functions for managing events -- not part of the public interface. |
|
4056 * Props to Dean Edwards' addEvent library for many of the ideas. |
|
4057 */ |
|
4058 jQuery.event = { |
|
4059 |
|
4060 global: {}, |
|
4061 |
|
4062 add: function( elem, types, handler, data, selector ) { |
|
4063 |
|
4064 var handleObjIn, eventHandle, tmp, |
|
4065 events, t, handleObj, |
|
4066 special, handlers, type, namespaces, origType, |
|
4067 elemData = data_priv.get( elem ); |
|
4068 |
|
4069 // Don't attach events to noData or text/comment nodes (but allow plain objects) |
|
4070 if ( !elemData ) { |
|
4071 return; |
|
4072 } |
|
4073 |
|
4074 // Caller can pass in an object of custom data in lieu of the handler |
|
4075 if ( handler.handler ) { |
|
4076 handleObjIn = handler; |
|
4077 handler = handleObjIn.handler; |
|
4078 selector = handleObjIn.selector; |
|
4079 } |
|
4080 |
|
4081 // Make sure that the handler has a unique ID, used to find/remove it later |
|
4082 if ( !handler.guid ) { |
|
4083 handler.guid = jQuery.guid++; |
|
4084 } |
|
4085 |
|
4086 // Init the element's event structure and main handler, if this is the first |
|
4087 if ( !(events = elemData.events) ) { |
|
4088 events = elemData.events = {}; |
|
4089 } |
|
4090 if ( !(eventHandle = elemData.handle) ) { |
|
4091 eventHandle = elemData.handle = function( e ) { |
|
4092 // Discard the second event of a jQuery.event.trigger() and |
|
4093 // when an event is called after a page has unloaded |
|
4094 return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ? |
|
4095 jQuery.event.dispatch.apply( elem, arguments ) : undefined; |
|
4096 }; |
|
4097 } |
|
4098 |
|
4099 // Handle multiple events separated by a space |
|
4100 types = ( types || "" ).match( rnotwhite ) || [ "" ]; |
|
4101 t = types.length; |
|
4102 while ( t-- ) { |
|
4103 tmp = rtypenamespace.exec( types[t] ) || []; |
|
4104 type = origType = tmp[1]; |
|
4105 namespaces = ( tmp[2] || "" ).split( "." ).sort(); |
|
4106 |
|
4107 // There *must* be a type, no attaching namespace-only handlers |
|
4108 if ( !type ) { |
|
4109 continue; |
|
4110 } |
|
4111 |
|
4112 // If event changes its type, use the special event handlers for the changed type |
|
4113 special = jQuery.event.special[ type ] || {}; |
|
4114 |
|
4115 // If selector defined, determine special event api type, otherwise given type |
|
4116 type = ( selector ? special.delegateType : special.bindType ) || type; |
|
4117 |
|
4118 // Update special based on newly reset type |
|
4119 special = jQuery.event.special[ type ] || {}; |
|
4120 |
|
4121 // handleObj is passed to all event handlers |
|
4122 handleObj = jQuery.extend({ |
|
4123 type: type, |
|
4124 origType: origType, |
|
4125 data: data, |
|
4126 handler: handler, |
|
4127 guid: handler.guid, |
|
4128 selector: selector, |
|
4129 needsContext: selector && jQuery.expr.match.needsContext.test( selector ), |
|
4130 namespace: namespaces.join(".") |
|
4131 }, handleObjIn ); |
|
4132 |
|
4133 // Init the event handler queue if we're the first |
|
4134 if ( !(handlers = events[ type ]) ) { |
|
4135 handlers = events[ type ] = []; |
|
4136 handlers.delegateCount = 0; |
|
4137 |
|
4138 // Only use addEventListener if the special events handler returns false |
|
4139 if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { |
|
4140 if ( elem.addEventListener ) { |
|
4141 elem.addEventListener( type, eventHandle, false ); |
|
4142 } |
|
4143 } |
|
4144 } |
|
4145 |
|
4146 if ( special.add ) { |
|
4147 special.add.call( elem, handleObj ); |
|
4148 |
|
4149 if ( !handleObj.handler.guid ) { |
|
4150 handleObj.handler.guid = handler.guid; |
|
4151 } |
|
4152 } |
|
4153 |
|
4154 // Add to the element's handler list, delegates in front |
|
4155 if ( selector ) { |
|
4156 handlers.splice( handlers.delegateCount++, 0, handleObj ); |
|
4157 } else { |
|
4158 handlers.push( handleObj ); |
|
4159 } |
|
4160 |
|
4161 // Keep track of which events have ever been used, for event optimization |
|
4162 jQuery.event.global[ type ] = true; |
|
4163 } |
|
4164 |
|
4165 }, |
|
4166 |
|
4167 // Detach an event or set of events from an element |
|
4168 remove: function( elem, types, handler, selector, mappedTypes ) { |
|
4169 |
|
4170 var j, origCount, tmp, |
|
4171 events, t, handleObj, |
|
4172 special, handlers, type, namespaces, origType, |
|
4173 elemData = data_priv.hasData( elem ) && data_priv.get( elem ); |
|
4174 |
|
4175 if ( !elemData || !(events = elemData.events) ) { |
|
4176 return; |
|
4177 } |
|
4178 |
|
4179 // Once for each type.namespace in types; type may be omitted |
|
4180 types = ( types || "" ).match( rnotwhite ) || [ "" ]; |
|
4181 t = types.length; |
|
4182 while ( t-- ) { |
|
4183 tmp = rtypenamespace.exec( types[t] ) || []; |
|
4184 type = origType = tmp[1]; |
|
4185 namespaces = ( tmp[2] || "" ).split( "." ).sort(); |
|
4186 |
|
4187 // Unbind all events (on this namespace, if provided) for the element |
|
4188 if ( !type ) { |
|
4189 for ( type in events ) { |
|
4190 jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); |
|
4191 } |
|
4192 continue; |
|
4193 } |
|
4194 |
|
4195 special = jQuery.event.special[ type ] || {}; |
|
4196 type = ( selector ? special.delegateType : special.bindType ) || type; |
|
4197 handlers = events[ type ] || []; |
|
4198 tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); |
|
4199 |
|
4200 // Remove matching events |
|
4201 origCount = j = handlers.length; |
|
4202 while ( j-- ) { |
|
4203 handleObj = handlers[ j ]; |
|
4204 |
|
4205 if ( ( mappedTypes || origType === handleObj.origType ) && |
|
4206 ( !handler || handler.guid === handleObj.guid ) && |
|
4207 ( !tmp || tmp.test( handleObj.namespace ) ) && |
|
4208 ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { |
|
4209 handlers.splice( j, 1 ); |
|
4210 |
|
4211 if ( handleObj.selector ) { |
|
4212 handlers.delegateCount--; |
|
4213 } |
|
4214 if ( special.remove ) { |
|
4215 special.remove.call( elem, handleObj ); |
|
4216 } |
|
4217 } |
|
4218 } |
|
4219 |
|
4220 // Remove generic event handler if we removed something and no more handlers exist |
|
4221 // (avoids potential for endless recursion during removal of special event handlers) |
|
4222 if ( origCount && !handlers.length ) { |
|
4223 if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { |
|
4224 jQuery.removeEvent( elem, type, elemData.handle ); |
|
4225 } |
|
4226 |
|
4227 delete events[ type ]; |
|
4228 } |
|
4229 } |
|
4230 |
|
4231 // Remove the expando if it's no longer used |
|
4232 if ( jQuery.isEmptyObject( events ) ) { |
|
4233 delete elemData.handle; |
|
4234 data_priv.remove( elem, "events" ); |
|
4235 } |
|
4236 }, |
|
4237 |
|
4238 trigger: function( event, data, elem, onlyHandlers ) { |
|
4239 |
|
4240 var i, cur, tmp, bubbleType, ontype, handle, special, |
|
4241 eventPath = [ elem || document ], |
|
4242 type = hasOwn.call( event, "type" ) ? event.type : event, |
|
4243 namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; |
|
4244 |
|
4245 cur = tmp = elem = elem || document; |
|
4246 |
|
4247 // Don't do events on text and comment nodes |
|
4248 if ( elem.nodeType === 3 || elem.nodeType === 8 ) { |
|
4249 return; |
|
4250 } |
|
4251 |
|
4252 // focus/blur morphs to focusin/out; ensure we're not firing them right now |
|
4253 if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { |
|
4254 return; |
|
4255 } |
|
4256 |
|
4257 if ( type.indexOf(".") >= 0 ) { |
|
4258 // Namespaced trigger; create a regexp to match event type in handle() |
|
4259 namespaces = type.split("."); |
|
4260 type = namespaces.shift(); |
|
4261 namespaces.sort(); |
|
4262 } |
|
4263 ontype = type.indexOf(":") < 0 && "on" + type; |
|
4264 |
|
4265 // Caller can pass in a jQuery.Event object, Object, or just an event type string |
|
4266 event = event[ jQuery.expando ] ? |
|
4267 event : |
|
4268 new jQuery.Event( type, typeof event === "object" && event ); |
|
4269 |
|
4270 // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) |
|
4271 event.isTrigger = onlyHandlers ? 2 : 3; |
|
4272 event.namespace = namespaces.join("."); |
|
4273 event.namespace_re = event.namespace ? |
|
4274 new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : |
|
4275 null; |
|
4276 |
|
4277 // Clean up the event in case it is being reused |
|
4278 event.result = undefined; |
|
4279 if ( !event.target ) { |
|
4280 event.target = elem; |
|
4281 } |
|
4282 |
|
4283 // Clone any incoming data and prepend the event, creating the handler arg list |
|
4284 data = data == null ? |
|
4285 [ event ] : |
|
4286 jQuery.makeArray( data, [ event ] ); |
|
4287 |
|
4288 // Allow special events to draw outside the lines |
|
4289 special = jQuery.event.special[ type ] || {}; |
|
4290 if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { |
|
4291 return; |
|
4292 } |
|
4293 |
|
4294 // Determine event propagation path in advance, per W3C events spec (#9951) |
|
4295 // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) |
|
4296 if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { |
|
4297 |
|
4298 bubbleType = special.delegateType || type; |
|
4299 if ( !rfocusMorph.test( bubbleType + type ) ) { |
|
4300 cur = cur.parentNode; |
|
4301 } |
|
4302 for ( ; cur; cur = cur.parentNode ) { |
|
4303 eventPath.push( cur ); |
|
4304 tmp = cur; |
|
4305 } |
|
4306 |
|
4307 // Only add window if we got to document (e.g., not plain obj or detached DOM) |
|
4308 if ( tmp === (elem.ownerDocument || document) ) { |
|
4309 eventPath.push( tmp.defaultView || tmp.parentWindow || window ); |
|
4310 } |
|
4311 } |
|
4312 |
|
4313 // Fire handlers on the event path |
|
4314 i = 0; |
|
4315 while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { |
|
4316 |
|
4317 event.type = i > 1 ? |
|
4318 bubbleType : |
|
4319 special.bindType || type; |
|
4320 |
|
4321 // jQuery handler |
|
4322 handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" ); |
|
4323 if ( handle ) { |
|
4324 handle.apply( cur, data ); |
|
4325 } |
|
4326 |
|
4327 // Native handler |
|
4328 handle = ontype && cur[ ontype ]; |
|
4329 if ( handle && handle.apply && jQuery.acceptData( cur ) ) { |
|
4330 event.result = handle.apply( cur, data ); |
|
4331 if ( event.result === false ) { |
|
4332 event.preventDefault(); |
|
4333 } |
|
4334 } |
|
4335 } |
|
4336 event.type = type; |
|
4337 |
|
4338 // If nobody prevented the default action, do it now |
|
4339 if ( !onlyHandlers && !event.isDefaultPrevented() ) { |
|
4340 |
|
4341 if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && |
|
4342 jQuery.acceptData( elem ) ) { |
|
4343 |
|
4344 // Call a native DOM method on the target with the same name name as the event. |
|
4345 // Don't do default actions on window, that's where global variables be (#6170) |
|
4346 if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { |
|
4347 |
|
4348 // Don't re-trigger an onFOO event when we call its FOO() method |
|
4349 tmp = elem[ ontype ]; |
|
4350 |
|
4351 if ( tmp ) { |
|
4352 elem[ ontype ] = null; |
|
4353 } |
|
4354 |
|
4355 // Prevent re-triggering of the same event, since we already bubbled it above |
|
4356 jQuery.event.triggered = type; |
|
4357 elem[ type ](); |
|
4358 jQuery.event.triggered = undefined; |
|
4359 |
|
4360 if ( tmp ) { |
|
4361 elem[ ontype ] = tmp; |
|
4362 } |
|
4363 } |
|
4364 } |
|
4365 } |
|
4366 |
|
4367 return event.result; |
|
4368 }, |
|
4369 |
|
4370 dispatch: function( event ) { |
|
4371 |
|
4372 // Make a writable jQuery.Event from the native event object |
|
4373 event = jQuery.event.fix( event ); |
|
4374 |
|
4375 var i, j, ret, matched, handleObj, |
|
4376 handlerQueue = [], |
|
4377 args = slice.call( arguments ), |
|
4378 handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [], |
|
4379 special = jQuery.event.special[ event.type ] || {}; |
|
4380 |
|
4381 // Use the fix-ed jQuery.Event rather than the (read-only) native event |
|
4382 args[0] = event; |
|
4383 event.delegateTarget = this; |
|
4384 |
|
4385 // Call the preDispatch hook for the mapped type, and let it bail if desired |
|
4386 if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { |
|
4387 return; |
|
4388 } |
|
4389 |
|
4390 // Determine handlers |
|
4391 handlerQueue = jQuery.event.handlers.call( this, event, handlers ); |
|
4392 |
|
4393 // Run delegates first; they may want to stop propagation beneath us |
|
4394 i = 0; |
|
4395 while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { |
|
4396 event.currentTarget = matched.elem; |
|
4397 |
|
4398 j = 0; |
|
4399 while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { |
|
4400 |
|
4401 // Triggered event must either 1) have no namespace, or |
|
4402 // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). |
|
4403 if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { |
|
4404 |
|
4405 event.handleObj = handleObj; |
|
4406 event.data = handleObj.data; |
|
4407 |
|
4408 ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) |
|
4409 .apply( matched.elem, args ); |
|
4410 |
|
4411 if ( ret !== undefined ) { |
|
4412 if ( (event.result = ret) === false ) { |
|
4413 event.preventDefault(); |
|
4414 event.stopPropagation(); |
|
4415 } |
|
4416 } |
|
4417 } |
|
4418 } |
|
4419 } |
|
4420 |
|
4421 // Call the postDispatch hook for the mapped type |
|
4422 if ( special.postDispatch ) { |
|
4423 special.postDispatch.call( this, event ); |
|
4424 } |
|
4425 |
|
4426 return event.result; |
|
4427 }, |
|
4428 |
|
4429 handlers: function( event, handlers ) { |
|
4430 var i, matches, sel, handleObj, |
|
4431 handlerQueue = [], |
|
4432 delegateCount = handlers.delegateCount, |
|
4433 cur = event.target; |
|
4434 |
|
4435 // Find delegate handlers |
|
4436 // Black-hole SVG <use> instance trees (#13180) |
|
4437 // Avoid non-left-click bubbling in Firefox (#3861) |
|
4438 if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { |
|
4439 |
|
4440 for ( ; cur !== this; cur = cur.parentNode || this ) { |
|
4441 |
|
4442 // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) |
|
4443 if ( cur.disabled !== true || event.type !== "click" ) { |
|
4444 matches = []; |
|
4445 for ( i = 0; i < delegateCount; i++ ) { |
|
4446 handleObj = handlers[ i ]; |
|
4447 |
|
4448 // Don't conflict with Object.prototype properties (#13203) |
|
4449 sel = handleObj.selector + " "; |
|
4450 |
|
4451 if ( matches[ sel ] === undefined ) { |
|
4452 matches[ sel ] = handleObj.needsContext ? |
|
4453 jQuery( sel, this ).index( cur ) >= 0 : |
|
4454 jQuery.find( sel, this, null, [ cur ] ).length; |
|
4455 } |
|
4456 if ( matches[ sel ] ) { |
|
4457 matches.push( handleObj ); |
|
4458 } |
|
4459 } |
|
4460 if ( matches.length ) { |
|
4461 handlerQueue.push({ elem: cur, handlers: matches }); |
|
4462 } |
|
4463 } |
|
4464 } |
|
4465 } |
|
4466 |
|
4467 // Add the remaining (directly-bound) handlers |
|
4468 if ( delegateCount < handlers.length ) { |
|
4469 handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); |
|
4470 } |
|
4471 |
|
4472 return handlerQueue; |
|
4473 }, |
|
4474 |
|
4475 // Includes some event props shared by KeyEvent and MouseEvent |
|
4476 props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), |
|
4477 |
|
4478 fixHooks: {}, |
|
4479 |
|
4480 keyHooks: { |
|
4481 props: "char charCode key keyCode".split(" "), |
|
4482 filter: function( event, original ) { |
|
4483 |
|
4484 // Add which for key events |
|
4485 if ( event.which == null ) { |
|
4486 event.which = original.charCode != null ? original.charCode : original.keyCode; |
|
4487 } |
|
4488 |
|
4489 return event; |
|
4490 } |
|
4491 }, |
|
4492 |
|
4493 mouseHooks: { |
|
4494 props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "), |
|
4495 filter: function( event, original ) { |
|
4496 var eventDoc, doc, body, |
|
4497 button = original.button; |
|
4498 |
|
4499 // Calculate pageX/Y if missing and clientX/Y available |
|
4500 if ( event.pageX == null && original.clientX != null ) { |
|
4501 eventDoc = event.target.ownerDocument || document; |
|
4502 doc = eventDoc.documentElement; |
|
4503 body = eventDoc.body; |
|
4504 |
|
4505 event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); |
|
4506 event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); |
|
4507 } |
|
4508 |
|
4509 // Add which for click: 1 === left; 2 === middle; 3 === right |
|
4510 // Note: button is not normalized, so don't use it |
|
4511 if ( !event.which && button !== undefined ) { |
|
4512 event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); |
|
4513 } |
|
4514 |
|
4515 return event; |
|
4516 } |
|
4517 }, |
|
4518 |
|
4519 fix: function( event ) { |
|
4520 if ( event[ jQuery.expando ] ) { |
|
4521 return event; |
|
4522 } |
|
4523 |
|
4524 // Create a writable copy of the event object and normalize some properties |
|
4525 var i, prop, copy, |
|
4526 type = event.type, |
|
4527 originalEvent = event, |
|
4528 fixHook = this.fixHooks[ type ]; |
|
4529 |
|
4530 if ( !fixHook ) { |
|
4531 this.fixHooks[ type ] = fixHook = |
|
4532 rmouseEvent.test( type ) ? this.mouseHooks : |
|
4533 rkeyEvent.test( type ) ? this.keyHooks : |
|
4534 {}; |
|
4535 } |
|
4536 copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; |
|
4537 |
|
4538 event = new jQuery.Event( originalEvent ); |
|
4539 |
|
4540 i = copy.length; |
|
4541 while ( i-- ) { |
|
4542 prop = copy[ i ]; |
|
4543 event[ prop ] = originalEvent[ prop ]; |
|
4544 } |
|
4545 |
|
4546 // Support: Cordova 2.5 (WebKit) (#13255) |
|
4547 // All events should have a target; Cordova deviceready doesn't |
|
4548 if ( !event.target ) { |
|
4549 event.target = document; |
|
4550 } |
|
4551 |
|
4552 // Support: Safari 6.0+, Chrome < 28 |
|
4553 // Target should not be a text node (#504, #13143) |
|
4554 if ( event.target.nodeType === 3 ) { |
|
4555 event.target = event.target.parentNode; |
|
4556 } |
|
4557 |
|
4558 return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; |
|
4559 }, |
|
4560 |
|
4561 special: { |
|
4562 load: { |
|
4563 // Prevent triggered image.load events from bubbling to window.load |
|
4564 noBubble: true |
|
4565 }, |
|
4566 focus: { |
|
4567 // Fire native event if possible so blur/focus sequence is correct |
|
4568 trigger: function() { |
|
4569 if ( this !== safeActiveElement() && this.focus ) { |
|
4570 this.focus(); |
|
4571 return false; |
|
4572 } |
|
4573 }, |
|
4574 delegateType: "focusin" |
|
4575 }, |
|
4576 blur: { |
|
4577 trigger: function() { |
|
4578 if ( this === safeActiveElement() && this.blur ) { |
|
4579 this.blur(); |
|
4580 return false; |
|
4581 } |
|
4582 }, |
|
4583 delegateType: "focusout" |
|
4584 }, |
|
4585 click: { |
|
4586 // For checkbox, fire native event so checked state will be right |
|
4587 trigger: function() { |
|
4588 if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { |
|
4589 this.click(); |
|
4590 return false; |
|
4591 } |
|
4592 }, |
|
4593 |
|
4594 // For cross-browser consistency, don't fire native .click() on links |
|
4595 _default: function( event ) { |
|
4596 return jQuery.nodeName( event.target, "a" ); |
|
4597 } |
|
4598 }, |
|
4599 |
|
4600 beforeunload: { |
|
4601 postDispatch: function( event ) { |
|
4602 |
|
4603 // Support: Firefox 20+ |
|
4604 // Firefox doesn't alert if the returnValue field is not set. |
|
4605 if ( event.result !== undefined && event.originalEvent ) { |
|
4606 event.originalEvent.returnValue = event.result; |
|
4607 } |
|
4608 } |
|
4609 } |
|
4610 }, |
|
4611 |
|
4612 simulate: function( type, elem, event, bubble ) { |
|
4613 // Piggyback on a donor event to simulate a different one. |
|
4614 // Fake originalEvent to avoid donor's stopPropagation, but if the |
|
4615 // simulated event prevents default then we do the same on the donor. |
|
4616 var e = jQuery.extend( |
|
4617 new jQuery.Event(), |
|
4618 event, |
|
4619 { |
|
4620 type: type, |
|
4621 isSimulated: true, |
|
4622 originalEvent: {} |
|
4623 } |
|
4624 ); |
|
4625 if ( bubble ) { |
|
4626 jQuery.event.trigger( e, null, elem ); |
|
4627 } else { |
|
4628 jQuery.event.dispatch.call( elem, e ); |
|
4629 } |
|
4630 if ( e.isDefaultPrevented() ) { |
|
4631 event.preventDefault(); |
|
4632 } |
|
4633 } |
|
4634 }; |
|
4635 |
|
4636 jQuery.removeEvent = function( elem, type, handle ) { |
|
4637 if ( elem.removeEventListener ) { |
|
4638 elem.removeEventListener( type, handle, false ); |
|
4639 } |
|
4640 }; |
|
4641 |
|
4642 jQuery.Event = function( src, props ) { |
|
4643 // Allow instantiation without the 'new' keyword |
|
4644 if ( !(this instanceof jQuery.Event) ) { |
|
4645 return new jQuery.Event( src, props ); |
|
4646 } |
|
4647 |
|
4648 // Event object |
|
4649 if ( src && src.type ) { |
|
4650 this.originalEvent = src; |
|
4651 this.type = src.type; |
|
4652 |
|
4653 // Events bubbling up the document may have been marked as prevented |
|
4654 // by a handler lower down the tree; reflect the correct value. |
|
4655 this.isDefaultPrevented = src.defaultPrevented || |
|
4656 src.defaultPrevented === undefined && |
|
4657 // Support: Android < 4.0 |
|
4658 src.returnValue === false ? |
|
4659 returnTrue : |
|
4660 returnFalse; |
|
4661 |
|
4662 // Event type |
|
4663 } else { |
|
4664 this.type = src; |
|
4665 } |
|
4666 |
|
4667 // Put explicitly provided properties onto the event object |
|
4668 if ( props ) { |
|
4669 jQuery.extend( this, props ); |
|
4670 } |
|
4671 |
|
4672 // Create a timestamp if incoming event doesn't have one |
|
4673 this.timeStamp = src && src.timeStamp || jQuery.now(); |
|
4674 |
|
4675 // Mark it as fixed |
|
4676 this[ jQuery.expando ] = true; |
|
4677 }; |
|
4678 |
|
4679 // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding |
|
4680 // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html |
|
4681 jQuery.Event.prototype = { |
|
4682 isDefaultPrevented: returnFalse, |
|
4683 isPropagationStopped: returnFalse, |
|
4684 isImmediatePropagationStopped: returnFalse, |
|
4685 |
|
4686 preventDefault: function() { |
|
4687 var e = this.originalEvent; |
|
4688 |
|
4689 this.isDefaultPrevented = returnTrue; |
|
4690 |
|
4691 if ( e && e.preventDefault ) { |
|
4692 e.preventDefault(); |
|
4693 } |
|
4694 }, |
|
4695 stopPropagation: function() { |
|
4696 var e = this.originalEvent; |
|
4697 |
|
4698 this.isPropagationStopped = returnTrue; |
|
4699 |
|
4700 if ( e && e.stopPropagation ) { |
|
4701 e.stopPropagation(); |
|
4702 } |
|
4703 }, |
|
4704 stopImmediatePropagation: function() { |
|
4705 var e = this.originalEvent; |
|
4706 |
|
4707 this.isImmediatePropagationStopped = returnTrue; |
|
4708 |
|
4709 if ( e && e.stopImmediatePropagation ) { |
|
4710 e.stopImmediatePropagation(); |
|
4711 } |
|
4712 |
|
4713 this.stopPropagation(); |
|
4714 } |
|
4715 }; |
|
4716 |
|
4717 // Create mouseenter/leave events using mouseover/out and event-time checks |
|
4718 // Support: Chrome 15+ |
|
4719 jQuery.each({ |
|
4720 mouseenter: "mouseover", |
|
4721 mouseleave: "mouseout", |
|
4722 pointerenter: "pointerover", |
|
4723 pointerleave: "pointerout" |
|
4724 }, function( orig, fix ) { |
|
4725 jQuery.event.special[ orig ] = { |
|
4726 delegateType: fix, |
|
4727 bindType: fix, |
|
4728 |
|
4729 handle: function( event ) { |
|
4730 var ret, |
|
4731 target = this, |
|
4732 related = event.relatedTarget, |
|
4733 handleObj = event.handleObj; |
|
4734 |
|
4735 // For mousenter/leave call the handler if related is outside the target. |
|
4736 // NB: No relatedTarget if the mouse left/entered the browser window |
|
4737 if ( !related || (related !== target && !jQuery.contains( target, related )) ) { |
|
4738 event.type = handleObj.origType; |
|
4739 ret = handleObj.handler.apply( this, arguments ); |
|
4740 event.type = fix; |
|
4741 } |
|
4742 return ret; |
|
4743 } |
|
4744 }; |
|
4745 }); |
|
4746 |
|
4747 // Create "bubbling" focus and blur events |
|
4748 // Support: Firefox, Chrome, Safari |
|
4749 if ( !support.focusinBubbles ) { |
|
4750 jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { |
|
4751 |
|
4752 // Attach a single capturing handler on the document while someone wants focusin/focusout |
|
4753 var handler = function( event ) { |
|
4754 jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); |
|
4755 }; |
|
4756 |
|
4757 jQuery.event.special[ fix ] = { |
|
4758 setup: function() { |
|
4759 var doc = this.ownerDocument || this, |
|
4760 attaches = data_priv.access( doc, fix ); |
|
4761 |
|
4762 if ( !attaches ) { |
|
4763 doc.addEventListener( orig, handler, true ); |
|
4764 } |
|
4765 data_priv.access( doc, fix, ( attaches || 0 ) + 1 ); |
|
4766 }, |
|
4767 teardown: function() { |
|
4768 var doc = this.ownerDocument || this, |
|
4769 attaches = data_priv.access( doc, fix ) - 1; |
|
4770 |
|
4771 if ( !attaches ) { |
|
4772 doc.removeEventListener( orig, handler, true ); |
|
4773 data_priv.remove( doc, fix ); |
|
4774 |
|
4775 } else { |
|
4776 data_priv.access( doc, fix, attaches ); |
|
4777 } |
|
4778 } |
|
4779 }; |
|
4780 }); |
|
4781 } |
|
4782 |
|
4783 jQuery.fn.extend({ |
|
4784 |
|
4785 on: function( types, selector, data, fn, /*INTERNAL*/ one ) { |
|
4786 var origFn, type; |
|
4787 |
|
4788 // Types can be a map of types/handlers |
|
4789 if ( typeof types === "object" ) { |
|
4790 // ( types-Object, selector, data ) |
|
4791 if ( typeof selector !== "string" ) { |
|
4792 // ( types-Object, data ) |
|
4793 data = data || selector; |
|
4794 selector = undefined; |
|
4795 } |
|
4796 for ( type in types ) { |
|
4797 this.on( type, selector, data, types[ type ], one ); |
|
4798 } |
|
4799 return this; |
|
4800 } |
|
4801 |
|
4802 if ( data == null && fn == null ) { |
|
4803 // ( types, fn ) |
|
4804 fn = selector; |
|
4805 data = selector = undefined; |
|
4806 } else if ( fn == null ) { |
|
4807 if ( typeof selector === "string" ) { |
|
4808 // ( types, selector, fn ) |
|
4809 fn = data; |
|
4810 data = undefined; |
|
4811 } else { |
|
4812 // ( types, data, fn ) |
|
4813 fn = data; |
|
4814 data = selector; |
|
4815 selector = undefined; |
|
4816 } |
|
4817 } |
|
4818 if ( fn === false ) { |
|
4819 fn = returnFalse; |
|
4820 } else if ( !fn ) { |
|
4821 return this; |
|
4822 } |
|
4823 |
|
4824 if ( one === 1 ) { |
|
4825 origFn = fn; |
|
4826 fn = function( event ) { |
|
4827 // Can use an empty set, since event contains the info |
|
4828 jQuery().off( event ); |
|
4829 return origFn.apply( this, arguments ); |
|
4830 }; |
|
4831 // Use same guid so caller can remove using origFn |
|
4832 fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); |
|
4833 } |
|
4834 return this.each( function() { |
|
4835 jQuery.event.add( this, types, fn, data, selector ); |
|
4836 }); |
|
4837 }, |
|
4838 one: function( types, selector, data, fn ) { |
|
4839 return this.on( types, selector, data, fn, 1 ); |
|
4840 }, |
|
4841 off: function( types, selector, fn ) { |
|
4842 var handleObj, type; |
|
4843 if ( types && types.preventDefault && types.handleObj ) { |
|
4844 // ( event ) dispatched jQuery.Event |
|
4845 handleObj = types.handleObj; |
|
4846 jQuery( types.delegateTarget ).off( |
|
4847 handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, |
|
4848 handleObj.selector, |
|
4849 handleObj.handler |
|
4850 ); |
|
4851 return this; |
|
4852 } |
|
4853 if ( typeof types === "object" ) { |
|
4854 // ( types-object [, selector] ) |
|
4855 for ( type in types ) { |
|
4856 this.off( type, selector, types[ type ] ); |
|
4857 } |
|
4858 return this; |
|
4859 } |
|
4860 if ( selector === false || typeof selector === "function" ) { |
|
4861 // ( types [, fn] ) |
|
4862 fn = selector; |
|
4863 selector = undefined; |
|
4864 } |
|
4865 if ( fn === false ) { |
|
4866 fn = returnFalse; |
|
4867 } |
|
4868 return this.each(function() { |
|
4869 jQuery.event.remove( this, types, fn, selector ); |
|
4870 }); |
|
4871 }, |
|
4872 |
|
4873 trigger: function( type, data ) { |
|
4874 return this.each(function() { |
|
4875 jQuery.event.trigger( type, data, this ); |
|
4876 }); |
|
4877 }, |
|
4878 triggerHandler: function( type, data ) { |
|
4879 var elem = this[0]; |
|
4880 if ( elem ) { |
|
4881 return jQuery.event.trigger( type, data, elem, true ); |
|
4882 } |
|
4883 } |
|
4884 }); |
|
4885 |
|
4886 |
|
4887 var |
|
4888 rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, |
|
4889 rtagName = /<([\w:]+)/, |
|
4890 rhtml = /<|&#?\w+;/, |
|
4891 rnoInnerhtml = /<(?:script|style|link)/i, |
|
4892 // checked="checked" or checked |
|
4893 rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, |
|
4894 rscriptType = /^$|\/(?:java|ecma)script/i, |
|
4895 rscriptTypeMasked = /^true\/(.*)/, |
|
4896 rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g, |
|
4897 |
|
4898 // We have to close these tags to support XHTML (#13200) |
|
4899 wrapMap = { |
|
4900 |
|
4901 // Support: IE 9 |
|
4902 option: [ 1, "<select multiple='multiple'>", "</select>" ], |
|
4903 |
|
4904 thead: [ 1, "<table>", "</table>" ], |
|
4905 col: [ 2, "<table><colgroup>", "</colgroup></table>" ], |
|
4906 tr: [ 2, "<table><tbody>", "</tbody></table>" ], |
|
4907 td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], |
|
4908 |
|
4909 _default: [ 0, "", "" ] |
|
4910 }; |
|
4911 |
|
4912 // Support: IE 9 |
|
4913 wrapMap.optgroup = wrapMap.option; |
|
4914 |
|
4915 wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; |
|
4916 wrapMap.th = wrapMap.td; |
|
4917 |
|
4918 // Support: 1.x compatibility |
|
4919 // Manipulating tables requires a tbody |
|
4920 function manipulationTarget( elem, content ) { |
|
4921 return jQuery.nodeName( elem, "table" ) && |
|
4922 jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? |
|
4923 |
|
4924 elem.getElementsByTagName("tbody")[0] || |
|
4925 elem.appendChild( elem.ownerDocument.createElement("tbody") ) : |
|
4926 elem; |
|
4927 } |
|
4928 |
|
4929 // Replace/restore the type attribute of script elements for safe DOM manipulation |
|
4930 function disableScript( elem ) { |
|
4931 elem.type = (elem.getAttribute("type") !== null) + "/" + elem.type; |
|
4932 return elem; |
|
4933 } |
|
4934 function restoreScript( elem ) { |
|
4935 var match = rscriptTypeMasked.exec( elem.type ); |
|
4936 |
|
4937 if ( match ) { |
|
4938 elem.type = match[ 1 ]; |
|
4939 } else { |
|
4940 elem.removeAttribute("type"); |
|
4941 } |
|
4942 |
|
4943 return elem; |
|
4944 } |
|
4945 |
|
4946 // Mark scripts as having already been evaluated |
|
4947 function setGlobalEval( elems, refElements ) { |
|
4948 var i = 0, |
|
4949 l = elems.length; |
|
4950 |
|
4951 for ( ; i < l; i++ ) { |
|
4952 data_priv.set( |
|
4953 elems[ i ], "globalEval", !refElements || data_priv.get( refElements[ i ], "globalEval" ) |
|
4954 ); |
|
4955 } |
|
4956 } |
|
4957 |
|
4958 function cloneCopyEvent( src, dest ) { |
|
4959 var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; |
|
4960 |
|
4961 if ( dest.nodeType !== 1 ) { |
|
4962 return; |
|
4963 } |
|
4964 |
|
4965 // 1. Copy private data: events, handlers, etc. |
|
4966 if ( data_priv.hasData( src ) ) { |
|
4967 pdataOld = data_priv.access( src ); |
|
4968 pdataCur = data_priv.set( dest, pdataOld ); |
|
4969 events = pdataOld.events; |
|
4970 |
|
4971 if ( events ) { |
|
4972 delete pdataCur.handle; |
|
4973 pdataCur.events = {}; |
|
4974 |
|
4975 for ( type in events ) { |
|
4976 for ( i = 0, l = events[ type ].length; i < l; i++ ) { |
|
4977 jQuery.event.add( dest, type, events[ type ][ i ] ); |
|
4978 } |
|
4979 } |
|
4980 } |
|
4981 } |
|
4982 |
|
4983 // 2. Copy user data |
|
4984 if ( data_user.hasData( src ) ) { |
|
4985 udataOld = data_user.access( src ); |
|
4986 udataCur = jQuery.extend( {}, udataOld ); |
|
4987 |
|
4988 data_user.set( dest, udataCur ); |
|
4989 } |
|
4990 } |
|
4991 |
|
4992 function getAll( context, tag ) { |
|
4993 var ret = context.getElementsByTagName ? context.getElementsByTagName( tag || "*" ) : |
|
4994 context.querySelectorAll ? context.querySelectorAll( tag || "*" ) : |
|
4995 []; |
|
4996 |
|
4997 return tag === undefined || tag && jQuery.nodeName( context, tag ) ? |
|
4998 jQuery.merge( [ context ], ret ) : |
|
4999 ret; |
|
5000 } |
|
5001 |
|
5002 // Support: IE >= 9 |
|
5003 function fixInput( src, dest ) { |
|
5004 var nodeName = dest.nodeName.toLowerCase(); |
|
5005 |
|
5006 // Fails to persist the checked state of a cloned checkbox or radio button. |
|
5007 if ( nodeName === "input" && rcheckableType.test( src.type ) ) { |
|
5008 dest.checked = src.checked; |
|
5009 |
|
5010 // Fails to return the selected option to the default selected state when cloning options |
|
5011 } else if ( nodeName === "input" || nodeName === "textarea" ) { |
|
5012 dest.defaultValue = src.defaultValue; |
|
5013 } |
|
5014 } |
|
5015 |
|
5016 jQuery.extend({ |
|
5017 clone: function( elem, dataAndEvents, deepDataAndEvents ) { |
|
5018 var i, l, srcElements, destElements, |
|
5019 clone = elem.cloneNode( true ), |
|
5020 inPage = jQuery.contains( elem.ownerDocument, elem ); |
|
5021 |
|
5022 // Support: IE >= 9 |
|
5023 // Fix Cloning issues |
|
5024 if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && |
|
5025 !jQuery.isXMLDoc( elem ) ) { |
|
5026 |
|
5027 // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 |
|
5028 destElements = getAll( clone ); |
|
5029 srcElements = getAll( elem ); |
|
5030 |
|
5031 for ( i = 0, l = srcElements.length; i < l; i++ ) { |
|
5032 fixInput( srcElements[ i ], destElements[ i ] ); |
|
5033 } |
|
5034 } |
|
5035 |
|
5036 // Copy the events from the original to the clone |
|
5037 if ( dataAndEvents ) { |
|
5038 if ( deepDataAndEvents ) { |
|
5039 srcElements = srcElements || getAll( elem ); |
|
5040 destElements = destElements || getAll( clone ); |
|
5041 |
|
5042 for ( i = 0, l = srcElements.length; i < l; i++ ) { |
|
5043 cloneCopyEvent( srcElements[ i ], destElements[ i ] ); |
|
5044 } |
|
5045 } else { |
|
5046 cloneCopyEvent( elem, clone ); |
|
5047 } |
|
5048 } |
|
5049 |
|
5050 // Preserve script evaluation history |
|
5051 destElements = getAll( clone, "script" ); |
|
5052 if ( destElements.length > 0 ) { |
|
5053 setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); |
|
5054 } |
|
5055 |
|
5056 // Return the cloned set |
|
5057 return clone; |
|
5058 }, |
|
5059 |
|
5060 buildFragment: function( elems, context, scripts, selection ) { |
|
5061 var elem, tmp, tag, wrap, contains, j, |
|
5062 fragment = context.createDocumentFragment(), |
|
5063 nodes = [], |
|
5064 i = 0, |
|
5065 l = elems.length; |
|
5066 |
|
5067 for ( ; i < l; i++ ) { |
|
5068 elem = elems[ i ]; |
|
5069 |
|
5070 if ( elem || elem === 0 ) { |
|
5071 |
|
5072 // Add nodes directly |
|
5073 if ( jQuery.type( elem ) === "object" ) { |
|
5074 // Support: QtWebKit |
|
5075 // jQuery.merge because push.apply(_, arraylike) throws |
|
5076 jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); |
|
5077 |
|
5078 // Convert non-html into a text node |
|
5079 } else if ( !rhtml.test( elem ) ) { |
|
5080 nodes.push( context.createTextNode( elem ) ); |
|
5081 |
|
5082 // Convert html into DOM nodes |
|
5083 } else { |
|
5084 tmp = tmp || fragment.appendChild( context.createElement("div") ); |
|
5085 |
|
5086 // Deserialize a standard representation |
|
5087 tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); |
|
5088 wrap = wrapMap[ tag ] || wrapMap._default; |
|
5089 tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[ 2 ]; |
|
5090 |
|
5091 // Descend through wrappers to the right content |
|
5092 j = wrap[ 0 ]; |
|
5093 while ( j-- ) { |
|
5094 tmp = tmp.lastChild; |
|
5095 } |
|
5096 |
|
5097 // Support: QtWebKit |
|
5098 // jQuery.merge because push.apply(_, arraylike) throws |
|
5099 jQuery.merge( nodes, tmp.childNodes ); |
|
5100 |
|
5101 // Remember the top-level container |
|
5102 tmp = fragment.firstChild; |
|
5103 |
|
5104 // Fixes #12346 |
|
5105 // Support: Webkit, IE |
|
5106 tmp.textContent = ""; |
|
5107 } |
|
5108 } |
|
5109 } |
|
5110 |
|
5111 // Remove wrapper from fragment |
|
5112 fragment.textContent = ""; |
|
5113 |
|
5114 i = 0; |
|
5115 while ( (elem = nodes[ i++ ]) ) { |
|
5116 |
|
5117 // #4087 - If origin and destination elements are the same, and this is |
|
5118 // that element, do not do anything |
|
5119 if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { |
|
5120 continue; |
|
5121 } |
|
5122 |
|
5123 contains = jQuery.contains( elem.ownerDocument, elem ); |
|
5124 |
|
5125 // Append to fragment |
|
5126 tmp = getAll( fragment.appendChild( elem ), "script" ); |
|
5127 |
|
5128 // Preserve script evaluation history |
|
5129 if ( contains ) { |
|
5130 setGlobalEval( tmp ); |
|
5131 } |
|
5132 |
|
5133 // Capture executables |
|
5134 if ( scripts ) { |
|
5135 j = 0; |
|
5136 while ( (elem = tmp[ j++ ]) ) { |
|
5137 if ( rscriptType.test( elem.type || "" ) ) { |
|
5138 scripts.push( elem ); |
|
5139 } |
|
5140 } |
|
5141 } |
|
5142 } |
|
5143 |
|
5144 return fragment; |
|
5145 }, |
|
5146 |
|
5147 cleanData: function( elems ) { |
|
5148 var data, elem, type, key, |
|
5149 special = jQuery.event.special, |
|
5150 i = 0; |
|
5151 |
|
5152 for ( ; (elem = elems[ i ]) !== undefined; i++ ) { |
|
5153 if ( jQuery.acceptData( elem ) ) { |
|
5154 key = elem[ data_priv.expando ]; |
|
5155 |
|
5156 if ( key && (data = data_priv.cache[ key ]) ) { |
|
5157 if ( data.events ) { |
|
5158 for ( type in data.events ) { |
|
5159 if ( special[ type ] ) { |
|
5160 jQuery.event.remove( elem, type ); |
|
5161 |
|
5162 // This is a shortcut to avoid jQuery.event.remove's overhead |
|
5163 } else { |
|
5164 jQuery.removeEvent( elem, type, data.handle ); |
|
5165 } |
|
5166 } |
|
5167 } |
|
5168 if ( data_priv.cache[ key ] ) { |
|
5169 // Discard any remaining `private` data |
|
5170 delete data_priv.cache[ key ]; |
|
5171 } |
|
5172 } |
|
5173 } |
|
5174 // Discard any remaining `user` data |
|
5175 delete data_user.cache[ elem[ data_user.expando ] ]; |
|
5176 } |
|
5177 } |
|
5178 }); |
|
5179 |
|
5180 jQuery.fn.extend({ |
|
5181 text: function( value ) { |
|
5182 return access( this, function( value ) { |
|
5183 return value === undefined ? |
|
5184 jQuery.text( this ) : |
|
5185 this.empty().each(function() { |
|
5186 if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { |
|
5187 this.textContent = value; |
|
5188 } |
|
5189 }); |
|
5190 }, null, value, arguments.length ); |
|
5191 }, |
|
5192 |
|
5193 append: function() { |
|
5194 return this.domManip( arguments, function( elem ) { |
|
5195 if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { |
|
5196 var target = manipulationTarget( this, elem ); |
|
5197 target.appendChild( elem ); |
|
5198 } |
|
5199 }); |
|
5200 }, |
|
5201 |
|
5202 prepend: function() { |
|
5203 return this.domManip( arguments, function( elem ) { |
|
5204 if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { |
|
5205 var target = manipulationTarget( this, elem ); |
|
5206 target.insertBefore( elem, target.firstChild ); |
|
5207 } |
|
5208 }); |
|
5209 }, |
|
5210 |
|
5211 before: function() { |
|
5212 return this.domManip( arguments, function( elem ) { |
|
5213 if ( this.parentNode ) { |
|
5214 this.parentNode.insertBefore( elem, this ); |
|
5215 } |
|
5216 }); |
|
5217 }, |
|
5218 |
|
5219 after: function() { |
|
5220 return this.domManip( arguments, function( elem ) { |
|
5221 if ( this.parentNode ) { |
|
5222 this.parentNode.insertBefore( elem, this.nextSibling ); |
|
5223 } |
|
5224 }); |
|
5225 }, |
|
5226 |
|
5227 remove: function( selector, keepData /* Internal Use Only */ ) { |
|
5228 var elem, |
|
5229 elems = selector ? jQuery.filter( selector, this ) : this, |
|
5230 i = 0; |
|
5231 |
|
5232 for ( ; (elem = elems[i]) != null; i++ ) { |
|
5233 if ( !keepData && elem.nodeType === 1 ) { |
|
5234 jQuery.cleanData( getAll( elem ) ); |
|
5235 } |
|
5236 |
|
5237 if ( elem.parentNode ) { |
|
5238 if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { |
|
5239 setGlobalEval( getAll( elem, "script" ) ); |
|
5240 } |
|
5241 elem.parentNode.removeChild( elem ); |
|
5242 } |
|
5243 } |
|
5244 |
|
5245 return this; |
|
5246 }, |
|
5247 |
|
5248 empty: function() { |
|
5249 var elem, |
|
5250 i = 0; |
|
5251 |
|
5252 for ( ; (elem = this[i]) != null; i++ ) { |
|
5253 if ( elem.nodeType === 1 ) { |
|
5254 |
|
5255 // Prevent memory leaks |
|
5256 jQuery.cleanData( getAll( elem, false ) ); |
|
5257 |
|
5258 // Remove any remaining nodes |
|
5259 elem.textContent = ""; |
|
5260 } |
|
5261 } |
|
5262 |
|
5263 return this; |
|
5264 }, |
|
5265 |
|
5266 clone: function( dataAndEvents, deepDataAndEvents ) { |
|
5267 dataAndEvents = dataAndEvents == null ? false : dataAndEvents; |
|
5268 deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; |
|
5269 |
|
5270 return this.map(function() { |
|
5271 return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); |
|
5272 }); |
|
5273 }, |
|
5274 |
|
5275 html: function( value ) { |
|
5276 return access( this, function( value ) { |
|
5277 var elem = this[ 0 ] || {}, |
|
5278 i = 0, |
|
5279 l = this.length; |
|
5280 |
|
5281 if ( value === undefined && elem.nodeType === 1 ) { |
|
5282 return elem.innerHTML; |
|
5283 } |
|
5284 |
|
5285 // See if we can take a shortcut and just use innerHTML |
|
5286 if ( typeof value === "string" && !rnoInnerhtml.test( value ) && |
|
5287 !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { |
|
5288 |
|
5289 value = value.replace( rxhtmlTag, "<$1></$2>" ); |
|
5290 |
|
5291 try { |
|
5292 for ( ; i < l; i++ ) { |
|
5293 elem = this[ i ] || {}; |
|
5294 |
|
5295 // Remove element nodes and prevent memory leaks |
|
5296 if ( elem.nodeType === 1 ) { |
|
5297 jQuery.cleanData( getAll( elem, false ) ); |
|
5298 elem.innerHTML = value; |
|
5299 } |
|
5300 } |
|
5301 |
|
5302 elem = 0; |
|
5303 |
|
5304 // If using innerHTML throws an exception, use the fallback method |
|
5305 } catch( e ) {} |
|
5306 } |
|
5307 |
|
5308 if ( elem ) { |
|
5309 this.empty().append( value ); |
|
5310 } |
|
5311 }, null, value, arguments.length ); |
|
5312 }, |
|
5313 |
|
5314 replaceWith: function() { |
|
5315 var arg = arguments[ 0 ]; |
|
5316 |
|
5317 // Make the changes, replacing each context element with the new content |
|
5318 this.domManip( arguments, function( elem ) { |
|
5319 arg = this.parentNode; |
|
5320 |
|
5321 jQuery.cleanData( getAll( this ) ); |
|
5322 |
|
5323 if ( arg ) { |
|
5324 arg.replaceChild( elem, this ); |
|
5325 } |
|
5326 }); |
|
5327 |
|
5328 // Force removal if there was no new content (e.g., from empty arguments) |
|
5329 return arg && (arg.length || arg.nodeType) ? this : this.remove(); |
|
5330 }, |
|
5331 |
|
5332 detach: function( selector ) { |
|
5333 return this.remove( selector, true ); |
|
5334 }, |
|
5335 |
|
5336 domManip: function( args, callback ) { |
|
5337 |
|
5338 // Flatten any nested arrays |
|
5339 args = concat.apply( [], args ); |
|
5340 |
|
5341 var fragment, first, scripts, hasScripts, node, doc, |
|
5342 i = 0, |
|
5343 l = this.length, |
|
5344 set = this, |
|
5345 iNoClone = l - 1, |
|
5346 value = args[ 0 ], |
|
5347 isFunction = jQuery.isFunction( value ); |
|
5348 |
|
5349 // We can't cloneNode fragments that contain checked, in WebKit |
|
5350 if ( isFunction || |
|
5351 ( l > 1 && typeof value === "string" && |
|
5352 !support.checkClone && rchecked.test( value ) ) ) { |
|
5353 return this.each(function( index ) { |
|
5354 var self = set.eq( index ); |
|
5355 if ( isFunction ) { |
|
5356 args[ 0 ] = value.call( this, index, self.html() ); |
|
5357 } |
|
5358 self.domManip( args, callback ); |
|
5359 }); |
|
5360 } |
|
5361 |
|
5362 if ( l ) { |
|
5363 fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); |
|
5364 first = fragment.firstChild; |
|
5365 |
|
5366 if ( fragment.childNodes.length === 1 ) { |
|
5367 fragment = first; |
|
5368 } |
|
5369 |
|
5370 if ( first ) { |
|
5371 scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); |
|
5372 hasScripts = scripts.length; |
|
5373 |
|
5374 // Use the original fragment for the last item instead of the first because it can end up |
|
5375 // being emptied incorrectly in certain situations (#8070). |
|
5376 for ( ; i < l; i++ ) { |
|
5377 node = fragment; |
|
5378 |
|
5379 if ( i !== iNoClone ) { |
|
5380 node = jQuery.clone( node, true, true ); |
|
5381 |
|
5382 // Keep references to cloned scripts for later restoration |
|
5383 if ( hasScripts ) { |
|
5384 // Support: QtWebKit |
|
5385 // jQuery.merge because push.apply(_, arraylike) throws |
|
5386 jQuery.merge( scripts, getAll( node, "script" ) ); |
|
5387 } |
|
5388 } |
|
5389 |
|
5390 callback.call( this[ i ], node, i ); |
|
5391 } |
|
5392 |
|
5393 if ( hasScripts ) { |
|
5394 doc = scripts[ scripts.length - 1 ].ownerDocument; |
|
5395 |
|
5396 // Reenable scripts |
|
5397 jQuery.map( scripts, restoreScript ); |
|
5398 |
|
5399 // Evaluate executable scripts on first document insertion |
|
5400 for ( i = 0; i < hasScripts; i++ ) { |
|
5401 node = scripts[ i ]; |
|
5402 if ( rscriptType.test( node.type || "" ) && |
|
5403 !data_priv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) { |
|
5404 |
|
5405 if ( node.src ) { |
|
5406 // Optional AJAX dependency, but won't run scripts if not present |
|
5407 if ( jQuery._evalUrl ) { |
|
5408 jQuery._evalUrl( node.src ); |
|
5409 } |
|
5410 } else { |
|
5411 jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); |
|
5412 } |
|
5413 } |
|
5414 } |
|
5415 } |
|
5416 } |
|
5417 } |
|
5418 |
|
5419 return this; |
|
5420 } |
|
5421 }); |
|
5422 |
|
5423 jQuery.each({ |
|
5424 appendTo: "append", |
|
5425 prependTo: "prepend", |
|
5426 insertBefore: "before", |
|
5427 insertAfter: "after", |
|
5428 replaceAll: "replaceWith" |
|
5429 }, function( name, original ) { |
|
5430 jQuery.fn[ name ] = function( selector ) { |
|
5431 var elems, |
|
5432 ret = [], |
|
5433 insert = jQuery( selector ), |
|
5434 last = insert.length - 1, |
|
5435 i = 0; |
|
5436 |
|
5437 for ( ; i <= last; i++ ) { |
|
5438 elems = i === last ? this : this.clone( true ); |
|
5439 jQuery( insert[ i ] )[ original ]( elems ); |
|
5440 |
|
5441 // Support: QtWebKit |
|
5442 // .get() because push.apply(_, arraylike) throws |
|
5443 push.apply( ret, elems.get() ); |
|
5444 } |
|
5445 |
|
5446 return this.pushStack( ret ); |
|
5447 }; |
|
5448 }); |
|
5449 |
|
5450 |
|
5451 var iframe, |
|
5452 elemdisplay = {}; |
|
5453 |
|
5454 /** |
|
5455 * Retrieve the actual display of a element |
|
5456 * @param {String} name nodeName of the element |
|
5457 * @param {Object} doc Document object |
|
5458 */ |
|
5459 // Called only from within defaultDisplay |
|
5460 function actualDisplay( name, doc ) { |
|
5461 var style, |
|
5462 elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), |
|
5463 |
|
5464 // getDefaultComputedStyle might be reliably used only on attached element |
|
5465 display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ? |
|
5466 |
|
5467 // Use of this method is a temporary fix (more like optmization) until something better comes along, |
|
5468 // since it was removed from specification and supported only in FF |
|
5469 style.display : jQuery.css( elem[ 0 ], "display" ); |
|
5470 |
|
5471 // We don't have any data stored on the element, |
|
5472 // so use "detach" method as fast way to get rid of the element |
|
5473 elem.detach(); |
|
5474 |
|
5475 return display; |
|
5476 } |
|
5477 |
|
5478 /** |
|
5479 * Try to determine the default display value of an element |
|
5480 * @param {String} nodeName |
|
5481 */ |
|
5482 function defaultDisplay( nodeName ) { |
|
5483 var doc = document, |
|
5484 display = elemdisplay[ nodeName ]; |
|
5485 |
|
5486 if ( !display ) { |
|
5487 display = actualDisplay( nodeName, doc ); |
|
5488 |
|
5489 // If the simple way fails, read from inside an iframe |
|
5490 if ( display === "none" || !display ) { |
|
5491 |
|
5492 // Use the already-created iframe if possible |
|
5493 iframe = (iframe || jQuery( "<iframe frameborder='0' width='0' height='0'/>" )).appendTo( doc.documentElement ); |
|
5494 |
|
5495 // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse |
|
5496 doc = iframe[ 0 ].contentDocument; |
|
5497 |
|
5498 // Support: IE |
|
5499 doc.write(); |
|
5500 doc.close(); |
|
5501 |
|
5502 display = actualDisplay( nodeName, doc ); |
|
5503 iframe.detach(); |
|
5504 } |
|
5505 |
|
5506 // Store the correct default display |
|
5507 elemdisplay[ nodeName ] = display; |
|
5508 } |
|
5509 |
|
5510 return display; |
|
5511 } |
|
5512 var rmargin = (/^margin/); |
|
5513 |
|
5514 var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); |
|
5515 |
|
5516 var getStyles = function( elem ) { |
|
5517 return elem.ownerDocument.defaultView.getComputedStyle( elem, null ); |
|
5518 }; |
|
5519 |
|
5520 |
|
5521 |
|
5522 function curCSS( elem, name, computed ) { |
|
5523 var width, minWidth, maxWidth, ret, |
|
5524 style = elem.style; |
|
5525 |
|
5526 computed = computed || getStyles( elem ); |
|
5527 |
|
5528 // Support: IE9 |
|
5529 // getPropertyValue is only needed for .css('filter') in IE9, see #12537 |
|
5530 if ( computed ) { |
|
5531 ret = computed.getPropertyValue( name ) || computed[ name ]; |
|
5532 } |
|
5533 |
|
5534 if ( computed ) { |
|
5535 |
|
5536 if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { |
|
5537 ret = jQuery.style( elem, name ); |
|
5538 } |
|
5539 |
|
5540 // Support: iOS < 6 |
|
5541 // A tribute to the "awesome hack by Dean Edwards" |
|
5542 // iOS < 6 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels |
|
5543 // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values |
|
5544 if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { |
|
5545 |
|
5546 // Remember the original values |
|
5547 width = style.width; |
|
5548 minWidth = style.minWidth; |
|
5549 maxWidth = style.maxWidth; |
|
5550 |
|
5551 // Put in the new values to get a computed value out |
|
5552 style.minWidth = style.maxWidth = style.width = ret; |
|
5553 ret = computed.width; |
|
5554 |
|
5555 // Revert the changed values |
|
5556 style.width = width; |
|
5557 style.minWidth = minWidth; |
|
5558 style.maxWidth = maxWidth; |
|
5559 } |
|
5560 } |
|
5561 |
|
5562 return ret !== undefined ? |
|
5563 // Support: IE |
|
5564 // IE returns zIndex value as an integer. |
|
5565 ret + "" : |
|
5566 ret; |
|
5567 } |
|
5568 |
|
5569 |
|
5570 function addGetHookIf( conditionFn, hookFn ) { |
|
5571 // Define the hook, we'll check on the first run if it's really needed. |
|
5572 return { |
|
5573 get: function() { |
|
5574 if ( conditionFn() ) { |
|
5575 // Hook not needed (or it's not possible to use it due to missing dependency), |
|
5576 // remove it. |
|
5577 // Since there are no other hooks for marginRight, remove the whole object. |
|
5578 delete this.get; |
|
5579 return; |
|
5580 } |
|
5581 |
|
5582 // Hook needed; redefine it so that the support test is not executed again. |
|
5583 |
|
5584 return (this.get = hookFn).apply( this, arguments ); |
|
5585 } |
|
5586 }; |
|
5587 } |
|
5588 |
|
5589 |
|
5590 (function() { |
|
5591 var pixelPositionVal, boxSizingReliableVal, |
|
5592 docElem = document.documentElement, |
|
5593 container = document.createElement( "div" ), |
|
5594 div = document.createElement( "div" ); |
|
5595 |
|
5596 if ( !div.style ) { |
|
5597 return; |
|
5598 } |
|
5599 |
|
5600 div.style.backgroundClip = "content-box"; |
|
5601 div.cloneNode( true ).style.backgroundClip = ""; |
|
5602 support.clearCloneStyle = div.style.backgroundClip === "content-box"; |
|
5603 |
|
5604 container.style.cssText = "border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;" + |
|
5605 "position:absolute"; |
|
5606 container.appendChild( div ); |
|
5607 |
|
5608 // Executing both pixelPosition & boxSizingReliable tests require only one layout |
|
5609 // so they're executed at the same time to save the second computation. |
|
5610 function computePixelPositionAndBoxSizingReliable() { |
|
5611 div.style.cssText = |
|
5612 // Support: Firefox<29, Android 2.3 |
|
5613 // Vendor-prefix box-sizing |
|
5614 "-webkit-box-sizing:border-box;-moz-box-sizing:border-box;" + |
|
5615 "box-sizing:border-box;display:block;margin-top:1%;top:1%;" + |
|
5616 "border:1px;padding:1px;width:4px;position:absolute"; |
|
5617 div.innerHTML = ""; |
|
5618 docElem.appendChild( container ); |
|
5619 |
|
5620 var divStyle = window.getComputedStyle( div, null ); |
|
5621 pixelPositionVal = divStyle.top !== "1%"; |
|
5622 boxSizingReliableVal = divStyle.width === "4px"; |
|
5623 |
|
5624 docElem.removeChild( container ); |
|
5625 } |
|
5626 |
|
5627 // Support: node.js jsdom |
|
5628 // Don't assume that getComputedStyle is a property of the global object |
|
5629 if ( window.getComputedStyle ) { |
|
5630 jQuery.extend( support, { |
|
5631 pixelPosition: function() { |
|
5632 // This test is executed only once but we still do memoizing |
|
5633 // since we can use the boxSizingReliable pre-computing. |
|
5634 // No need to check if the test was already performed, though. |
|
5635 computePixelPositionAndBoxSizingReliable(); |
|
5636 return pixelPositionVal; |
|
5637 }, |
|
5638 boxSizingReliable: function() { |
|
5639 if ( boxSizingReliableVal == null ) { |
|
5640 computePixelPositionAndBoxSizingReliable(); |
|
5641 } |
|
5642 return boxSizingReliableVal; |
|
5643 }, |
|
5644 reliableMarginRight: function() { |
|
5645 // Support: Android 2.3 |
|
5646 // Check if div with explicit width and no margin-right incorrectly |
|
5647 // gets computed margin-right based on width of container. (#3333) |
|
5648 // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right |
|
5649 // This support function is only executed once so no memoizing is needed. |
|
5650 var ret, |
|
5651 marginDiv = div.appendChild( document.createElement( "div" ) ); |
|
5652 |
|
5653 // Reset CSS: box-sizing; display; margin; border; padding |
|
5654 marginDiv.style.cssText = div.style.cssText = |
|
5655 // Support: Firefox<29, Android 2.3 |
|
5656 // Vendor-prefix box-sizing |
|
5657 "-webkit-box-sizing:content-box;-moz-box-sizing:content-box;" + |
|
5658 "box-sizing:content-box;display:block;margin:0;border:0;padding:0"; |
|
5659 marginDiv.style.marginRight = marginDiv.style.width = "0"; |
|
5660 div.style.width = "1px"; |
|
5661 docElem.appendChild( container ); |
|
5662 |
|
5663 ret = !parseFloat( window.getComputedStyle( marginDiv, null ).marginRight ); |
|
5664 |
|
5665 docElem.removeChild( container ); |
|
5666 |
|
5667 return ret; |
|
5668 } |
|
5669 }); |
|
5670 } |
|
5671 })(); |
|
5672 |
|
5673 |
|
5674 // A method for quickly swapping in/out CSS properties to get correct calculations. |
|
5675 jQuery.swap = function( elem, options, callback, args ) { |
|
5676 var ret, name, |
|
5677 old = {}; |
|
5678 |
|
5679 // Remember the old values, and insert the new ones |
|
5680 for ( name in options ) { |
|
5681 old[ name ] = elem.style[ name ]; |
|
5682 elem.style[ name ] = options[ name ]; |
|
5683 } |
|
5684 |
|
5685 ret = callback.apply( elem, args || [] ); |
|
5686 |
|
5687 // Revert the old values |
|
5688 for ( name in options ) { |
|
5689 elem.style[ name ] = old[ name ]; |
|
5690 } |
|
5691 |
|
5692 return ret; |
|
5693 }; |
|
5694 |
|
5695 |
|
5696 var |
|
5697 // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" |
|
5698 // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display |
|
5699 rdisplayswap = /^(none|table(?!-c[ea]).+)/, |
|
5700 rnumsplit = new RegExp( "^(" + pnum + ")(.*)$", "i" ), |
|
5701 rrelNum = new RegExp( "^([+-])=(" + pnum + ")", "i" ), |
|
5702 |
|
5703 cssShow = { position: "absolute", visibility: "hidden", display: "block" }, |
|
5704 cssNormalTransform = { |
|
5705 letterSpacing: "0", |
|
5706 fontWeight: "400" |
|
5707 }, |
|
5708 |
|
5709 cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; |
|
5710 |
|
5711 // return a css property mapped to a potentially vendor prefixed property |
|
5712 function vendorPropName( style, name ) { |
|
5713 |
|
5714 // shortcut for names that are not vendor prefixed |
|
5715 if ( name in style ) { |
|
5716 return name; |
|
5717 } |
|
5718 |
|
5719 // check for vendor prefixed names |
|
5720 var capName = name[0].toUpperCase() + name.slice(1), |
|
5721 origName = name, |
|
5722 i = cssPrefixes.length; |
|
5723 |
|
5724 while ( i-- ) { |
|
5725 name = cssPrefixes[ i ] + capName; |
|
5726 if ( name in style ) { |
|
5727 return name; |
|
5728 } |
|
5729 } |
|
5730 |
|
5731 return origName; |
|
5732 } |
|
5733 |
|
5734 function setPositiveNumber( elem, value, subtract ) { |
|
5735 var matches = rnumsplit.exec( value ); |
|
5736 return matches ? |
|
5737 // Guard against undefined "subtract", e.g., when used as in cssHooks |
|
5738 Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : |
|
5739 value; |
|
5740 } |
|
5741 |
|
5742 function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { |
|
5743 var i = extra === ( isBorderBox ? "border" : "content" ) ? |
|
5744 // If we already have the right measurement, avoid augmentation |
|
5745 4 : |
|
5746 // Otherwise initialize for horizontal or vertical properties |
|
5747 name === "width" ? 1 : 0, |
|
5748 |
|
5749 val = 0; |
|
5750 |
|
5751 for ( ; i < 4; i += 2 ) { |
|
5752 // both box models exclude margin, so add it if we want it |
|
5753 if ( extra === "margin" ) { |
|
5754 val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); |
|
5755 } |
|
5756 |
|
5757 if ( isBorderBox ) { |
|
5758 // border-box includes padding, so remove it if we want content |
|
5759 if ( extra === "content" ) { |
|
5760 val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); |
|
5761 } |
|
5762 |
|
5763 // at this point, extra isn't border nor margin, so remove border |
|
5764 if ( extra !== "margin" ) { |
|
5765 val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); |
|
5766 } |
|
5767 } else { |
|
5768 // at this point, extra isn't content, so add padding |
|
5769 val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); |
|
5770 |
|
5771 // at this point, extra isn't content nor padding, so add border |
|
5772 if ( extra !== "padding" ) { |
|
5773 val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); |
|
5774 } |
|
5775 } |
|
5776 } |
|
5777 |
|
5778 return val; |
|
5779 } |
|
5780 |
|
5781 function getWidthOrHeight( elem, name, extra ) { |
|
5782 |
|
5783 // Start with offset property, which is equivalent to the border-box value |
|
5784 var valueIsBorderBox = true, |
|
5785 val = name === "width" ? elem.offsetWidth : elem.offsetHeight, |
|
5786 styles = getStyles( elem ), |
|
5787 isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; |
|
5788 |
|
5789 // some non-html elements return undefined for offsetWidth, so check for null/undefined |
|
5790 // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 |
|
5791 // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 |
|
5792 if ( val <= 0 || val == null ) { |
|
5793 // Fall back to computed then uncomputed css if necessary |
|
5794 val = curCSS( elem, name, styles ); |
|
5795 if ( val < 0 || val == null ) { |
|
5796 val = elem.style[ name ]; |
|
5797 } |
|
5798 |
|
5799 // Computed unit is not pixels. Stop here and return. |
|
5800 if ( rnumnonpx.test(val) ) { |
|
5801 return val; |
|
5802 } |
|
5803 |
|
5804 // we need the check for style in case a browser which returns unreliable values |
|
5805 // for getComputedStyle silently falls back to the reliable elem.style |
|
5806 valueIsBorderBox = isBorderBox && |
|
5807 ( support.boxSizingReliable() || val === elem.style[ name ] ); |
|
5808 |
|
5809 // Normalize "", auto, and prepare for extra |
|
5810 val = parseFloat( val ) || 0; |
|
5811 } |
|
5812 |
|
5813 // use the active box-sizing model to add/subtract irrelevant styles |
|
5814 return ( val + |
|
5815 augmentWidthOrHeight( |
|
5816 elem, |
|
5817 name, |
|
5818 extra || ( isBorderBox ? "border" : "content" ), |
|
5819 valueIsBorderBox, |
|
5820 styles |
|
5821 ) |
|
5822 ) + "px"; |
|
5823 } |
|
5824 |
|
5825 function showHide( elements, show ) { |
|
5826 var display, elem, hidden, |
|
5827 values = [], |
|
5828 index = 0, |
|
5829 length = elements.length; |
|
5830 |
|
5831 for ( ; index < length; index++ ) { |
|
5832 elem = elements[ index ]; |
|
5833 if ( !elem.style ) { |
|
5834 continue; |
|
5835 } |
|
5836 |
|
5837 values[ index ] = data_priv.get( elem, "olddisplay" ); |
|
5838 display = elem.style.display; |
|
5839 if ( show ) { |
|
5840 // Reset the inline display of this element to learn if it is |
|
5841 // being hidden by cascaded rules or not |
|
5842 if ( !values[ index ] && display === "none" ) { |
|
5843 elem.style.display = ""; |
|
5844 } |
|
5845 |
|
5846 // Set elements which have been overridden with display: none |
|
5847 // in a stylesheet to whatever the default browser style is |
|
5848 // for such an element |
|
5849 if ( elem.style.display === "" && isHidden( elem ) ) { |
|
5850 values[ index ] = data_priv.access( elem, "olddisplay", defaultDisplay(elem.nodeName) ); |
|
5851 } |
|
5852 } else { |
|
5853 hidden = isHidden( elem ); |
|
5854 |
|
5855 if ( display !== "none" || !hidden ) { |
|
5856 data_priv.set( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); |
|
5857 } |
|
5858 } |
|
5859 } |
|
5860 |
|
5861 // Set the display of most of the elements in a second loop |
|
5862 // to avoid the constant reflow |
|
5863 for ( index = 0; index < length; index++ ) { |
|
5864 elem = elements[ index ]; |
|
5865 if ( !elem.style ) { |
|
5866 continue; |
|
5867 } |
|
5868 if ( !show || elem.style.display === "none" || elem.style.display === "" ) { |
|
5869 elem.style.display = show ? values[ index ] || "" : "none"; |
|
5870 } |
|
5871 } |
|
5872 |
|
5873 return elements; |
|
5874 } |
|
5875 |
|
5876 jQuery.extend({ |
|
5877 // Add in style property hooks for overriding the default |
|
5878 // behavior of getting and setting a style property |
|
5879 cssHooks: { |
|
5880 opacity: { |
|
5881 get: function( elem, computed ) { |
|
5882 if ( computed ) { |
|
5883 // We should always get a number back from opacity |
|
5884 var ret = curCSS( elem, "opacity" ); |
|
5885 return ret === "" ? "1" : ret; |
|
5886 } |
|
5887 } |
|
5888 } |
|
5889 }, |
|
5890 |
|
5891 // Don't automatically add "px" to these possibly-unitless properties |
|
5892 cssNumber: { |
|
5893 "columnCount": true, |
|
5894 "fillOpacity": true, |
|
5895 "flexGrow": true, |
|
5896 "flexShrink": true, |
|
5897 "fontWeight": true, |
|
5898 "lineHeight": true, |
|
5899 "opacity": true, |
|
5900 "order": true, |
|
5901 "orphans": true, |
|
5902 "widows": true, |
|
5903 "zIndex": true, |
|
5904 "zoom": true |
|
5905 }, |
|
5906 |
|
5907 // Add in properties whose names you wish to fix before |
|
5908 // setting or getting the value |
|
5909 cssProps: { |
|
5910 // normalize float css property |
|
5911 "float": "cssFloat" |
|
5912 }, |
|
5913 |
|
5914 // Get and set the style property on a DOM Node |
|
5915 style: function( elem, name, value, extra ) { |
|
5916 // Don't set styles on text and comment nodes |
|
5917 if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { |
|
5918 return; |
|
5919 } |
|
5920 |
|
5921 // Make sure that we're working with the right name |
|
5922 var ret, type, hooks, |
|
5923 origName = jQuery.camelCase( name ), |
|
5924 style = elem.style; |
|
5925 |
|
5926 name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); |
|
5927 |
|
5928 // gets hook for the prefixed version |
|
5929 // followed by the unprefixed version |
|
5930 hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; |
|
5931 |
|
5932 // Check if we're setting a value |
|
5933 if ( value !== undefined ) { |
|
5934 type = typeof value; |
|
5935 |
|
5936 // convert relative number strings (+= or -=) to relative numbers. #7345 |
|
5937 if ( type === "string" && (ret = rrelNum.exec( value )) ) { |
|
5938 value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); |
|
5939 // Fixes bug #9237 |
|
5940 type = "number"; |
|
5941 } |
|
5942 |
|
5943 // Make sure that null and NaN values aren't set. See: #7116 |
|
5944 if ( value == null || value !== value ) { |
|
5945 return; |
|
5946 } |
|
5947 |
|
5948 // If a number was passed in, add 'px' to the (except for certain CSS properties) |
|
5949 if ( type === "number" && !jQuery.cssNumber[ origName ] ) { |
|
5950 value += "px"; |
|
5951 } |
|
5952 |
|
5953 // Fixes #8908, it can be done more correctly by specifying setters in cssHooks, |
|
5954 // but it would mean to define eight (for every problematic property) identical functions |
|
5955 if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { |
|
5956 style[ name ] = "inherit"; |
|
5957 } |
|
5958 |
|
5959 // If a hook was provided, use that value, otherwise just set the specified value |
|
5960 if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { |
|
5961 style[ name ] = value; |
|
5962 } |
|
5963 |
|
5964 } else { |
|
5965 // If a hook was provided get the non-computed value from there |
|
5966 if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { |
|
5967 return ret; |
|
5968 } |
|
5969 |
|
5970 // Otherwise just get the value from the style object |
|
5971 return style[ name ]; |
|
5972 } |
|
5973 }, |
|
5974 |
|
5975 css: function( elem, name, extra, styles ) { |
|
5976 var val, num, hooks, |
|
5977 origName = jQuery.camelCase( name ); |
|
5978 |
|
5979 // Make sure that we're working with the right name |
|
5980 name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); |
|
5981 |
|
5982 // gets hook for the prefixed version |
|
5983 // followed by the unprefixed version |
|
5984 hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; |
|
5985 |
|
5986 // If a hook was provided get the computed value from there |
|
5987 if ( hooks && "get" in hooks ) { |
|
5988 val = hooks.get( elem, true, extra ); |
|
5989 } |
|
5990 |
|
5991 // Otherwise, if a way to get the computed value exists, use that |
|
5992 if ( val === undefined ) { |
|
5993 val = curCSS( elem, name, styles ); |
|
5994 } |
|
5995 |
|
5996 //convert "normal" to computed value |
|
5997 if ( val === "normal" && name in cssNormalTransform ) { |
|
5998 val = cssNormalTransform[ name ]; |
|
5999 } |
|
6000 |
|
6001 // Return, converting to number if forced or a qualifier was provided and val looks numeric |
|
6002 if ( extra === "" || extra ) { |
|
6003 num = parseFloat( val ); |
|
6004 return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; |
|
6005 } |
|
6006 return val; |
|
6007 } |
|
6008 }); |
|
6009 |
|
6010 jQuery.each([ "height", "width" ], function( i, name ) { |
|
6011 jQuery.cssHooks[ name ] = { |
|
6012 get: function( elem, computed, extra ) { |
|
6013 if ( computed ) { |
|
6014 // certain elements can have dimension info if we invisibly show them |
|
6015 // however, it must have a current display style that would benefit from this |
|
6016 return rdisplayswap.test( jQuery.css( elem, "display" ) ) && elem.offsetWidth === 0 ? |
|
6017 jQuery.swap( elem, cssShow, function() { |
|
6018 return getWidthOrHeight( elem, name, extra ); |
|
6019 }) : |
|
6020 getWidthOrHeight( elem, name, extra ); |
|
6021 } |
|
6022 }, |
|
6023 |
|
6024 set: function( elem, value, extra ) { |
|
6025 var styles = extra && getStyles( elem ); |
|
6026 return setPositiveNumber( elem, value, extra ? |
|
6027 augmentWidthOrHeight( |
|
6028 elem, |
|
6029 name, |
|
6030 extra, |
|
6031 jQuery.css( elem, "boxSizing", false, styles ) === "border-box", |
|
6032 styles |
|
6033 ) : 0 |
|
6034 ); |
|
6035 } |
|
6036 }; |
|
6037 }); |
|
6038 |
|
6039 // Support: Android 2.3 |
|
6040 jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight, |
|
6041 function( elem, computed ) { |
|
6042 if ( computed ) { |
|
6043 // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right |
|
6044 // Work around by temporarily setting element display to inline-block |
|
6045 return jQuery.swap( elem, { "display": "inline-block" }, |
|
6046 curCSS, [ elem, "marginRight" ] ); |
|
6047 } |
|
6048 } |
|
6049 ); |
|
6050 |
|
6051 // These hooks are used by animate to expand properties |
|
6052 jQuery.each({ |
|
6053 margin: "", |
|
6054 padding: "", |
|
6055 border: "Width" |
|
6056 }, function( prefix, suffix ) { |
|
6057 jQuery.cssHooks[ prefix + suffix ] = { |
|
6058 expand: function( value ) { |
|
6059 var i = 0, |
|
6060 expanded = {}, |
|
6061 |
|
6062 // assumes a single number if not a string |
|
6063 parts = typeof value === "string" ? value.split(" ") : [ value ]; |
|
6064 |
|
6065 for ( ; i < 4; i++ ) { |
|
6066 expanded[ prefix + cssExpand[ i ] + suffix ] = |
|
6067 parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; |
|
6068 } |
|
6069 |
|
6070 return expanded; |
|
6071 } |
|
6072 }; |
|
6073 |
|
6074 if ( !rmargin.test( prefix ) ) { |
|
6075 jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; |
|
6076 } |
|
6077 }); |
|
6078 |
|
6079 jQuery.fn.extend({ |
|
6080 css: function( name, value ) { |
|
6081 return access( this, function( elem, name, value ) { |
|
6082 var styles, len, |
|
6083 map = {}, |
|
6084 i = 0; |
|
6085 |
|
6086 if ( jQuery.isArray( name ) ) { |
|
6087 styles = getStyles( elem ); |
|
6088 len = name.length; |
|
6089 |
|
6090 for ( ; i < len; i++ ) { |
|
6091 map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); |
|
6092 } |
|
6093 |
|
6094 return map; |
|
6095 } |
|
6096 |
|
6097 return value !== undefined ? |
|
6098 jQuery.style( elem, name, value ) : |
|
6099 jQuery.css( elem, name ); |
|
6100 }, name, value, arguments.length > 1 ); |
|
6101 }, |
|
6102 show: function() { |
|
6103 return showHide( this, true ); |
|
6104 }, |
|
6105 hide: function() { |
|
6106 return showHide( this ); |
|
6107 }, |
|
6108 toggle: function( state ) { |
|
6109 if ( typeof state === "boolean" ) { |
|
6110 return state ? this.show() : this.hide(); |
|
6111 } |
|
6112 |
|
6113 return this.each(function() { |
|
6114 if ( isHidden( this ) ) { |
|
6115 jQuery( this ).show(); |
|
6116 } else { |
|
6117 jQuery( this ).hide(); |
|
6118 } |
|
6119 }); |
|
6120 } |
|
6121 }); |
|
6122 |
|
6123 |
|
6124 function Tween( elem, options, prop, end, easing ) { |
|
6125 return new Tween.prototype.init( elem, options, prop, end, easing ); |
|
6126 } |
|
6127 jQuery.Tween = Tween; |
|
6128 |
|
6129 Tween.prototype = { |
|
6130 constructor: Tween, |
|
6131 init: function( elem, options, prop, end, easing, unit ) { |
|
6132 this.elem = elem; |
|
6133 this.prop = prop; |
|
6134 this.easing = easing || "swing"; |
|
6135 this.options = options; |
|
6136 this.start = this.now = this.cur(); |
|
6137 this.end = end; |
|
6138 this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); |
|
6139 }, |
|
6140 cur: function() { |
|
6141 var hooks = Tween.propHooks[ this.prop ]; |
|
6142 |
|
6143 return hooks && hooks.get ? |
|
6144 hooks.get( this ) : |
|
6145 Tween.propHooks._default.get( this ); |
|
6146 }, |
|
6147 run: function( percent ) { |
|
6148 var eased, |
|
6149 hooks = Tween.propHooks[ this.prop ]; |
|
6150 |
|
6151 if ( this.options.duration ) { |
|
6152 this.pos = eased = jQuery.easing[ this.easing ]( |
|
6153 percent, this.options.duration * percent, 0, 1, this.options.duration |
|
6154 ); |
|
6155 } else { |
|
6156 this.pos = eased = percent; |
|
6157 } |
|
6158 this.now = ( this.end - this.start ) * eased + this.start; |
|
6159 |
|
6160 if ( this.options.step ) { |
|
6161 this.options.step.call( this.elem, this.now, this ); |
|
6162 } |
|
6163 |
|
6164 if ( hooks && hooks.set ) { |
|
6165 hooks.set( this ); |
|
6166 } else { |
|
6167 Tween.propHooks._default.set( this ); |
|
6168 } |
|
6169 return this; |
|
6170 } |
|
6171 }; |
|
6172 |
|
6173 Tween.prototype.init.prototype = Tween.prototype; |
|
6174 |
|
6175 Tween.propHooks = { |
|
6176 _default: { |
|
6177 get: function( tween ) { |
|
6178 var result; |
|
6179 |
|
6180 if ( tween.elem[ tween.prop ] != null && |
|
6181 (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) { |
|
6182 return tween.elem[ tween.prop ]; |
|
6183 } |
|
6184 |
|
6185 // passing an empty string as a 3rd parameter to .css will automatically |
|
6186 // attempt a parseFloat and fallback to a string if the parse fails |
|
6187 // so, simple values such as "10px" are parsed to Float. |
|
6188 // complex values such as "rotate(1rad)" are returned as is. |
|
6189 result = jQuery.css( tween.elem, tween.prop, "" ); |
|
6190 // Empty strings, null, undefined and "auto" are converted to 0. |
|
6191 return !result || result === "auto" ? 0 : result; |
|
6192 }, |
|
6193 set: function( tween ) { |
|
6194 // use step hook for back compat - use cssHook if its there - use .style if its |
|
6195 // available and use plain properties where available |
|
6196 if ( jQuery.fx.step[ tween.prop ] ) { |
|
6197 jQuery.fx.step[ tween.prop ]( tween ); |
|
6198 } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) { |
|
6199 jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); |
|
6200 } else { |
|
6201 tween.elem[ tween.prop ] = tween.now; |
|
6202 } |
|
6203 } |
|
6204 } |
|
6205 }; |
|
6206 |
|
6207 // Support: IE9 |
|
6208 // Panic based approach to setting things on disconnected nodes |
|
6209 |
|
6210 Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { |
|
6211 set: function( tween ) { |
|
6212 if ( tween.elem.nodeType && tween.elem.parentNode ) { |
|
6213 tween.elem[ tween.prop ] = tween.now; |
|
6214 } |
|
6215 } |
|
6216 }; |
|
6217 |
|
6218 jQuery.easing = { |
|
6219 linear: function( p ) { |
|
6220 return p; |
|
6221 }, |
|
6222 swing: function( p ) { |
|
6223 return 0.5 - Math.cos( p * Math.PI ) / 2; |
|
6224 } |
|
6225 }; |
|
6226 |
|
6227 jQuery.fx = Tween.prototype.init; |
|
6228 |
|
6229 // Back Compat <1.8 extension point |
|
6230 jQuery.fx.step = {}; |
|
6231 |
|
6232 |
|
6233 |
|
6234 |
|
6235 var |
|
6236 fxNow, timerId, |
|
6237 rfxtypes = /^(?:toggle|show|hide)$/, |
|
6238 rfxnum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ), |
|
6239 rrun = /queueHooks$/, |
|
6240 animationPrefilters = [ defaultPrefilter ], |
|
6241 tweeners = { |
|
6242 "*": [ function( prop, value ) { |
|
6243 var tween = this.createTween( prop, value ), |
|
6244 target = tween.cur(), |
|
6245 parts = rfxnum.exec( value ), |
|
6246 unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), |
|
6247 |
|
6248 // Starting value computation is required for potential unit mismatches |
|
6249 start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) && |
|
6250 rfxnum.exec( jQuery.css( tween.elem, prop ) ), |
|
6251 scale = 1, |
|
6252 maxIterations = 20; |
|
6253 |
|
6254 if ( start && start[ 3 ] !== unit ) { |
|
6255 // Trust units reported by jQuery.css |
|
6256 unit = unit || start[ 3 ]; |
|
6257 |
|
6258 // Make sure we update the tween properties later on |
|
6259 parts = parts || []; |
|
6260 |
|
6261 // Iteratively approximate from a nonzero starting point |
|
6262 start = +target || 1; |
|
6263 |
|
6264 do { |
|
6265 // If previous iteration zeroed out, double until we get *something* |
|
6266 // Use a string for doubling factor so we don't accidentally see scale as unchanged below |
|
6267 scale = scale || ".5"; |
|
6268 |
|
6269 // Adjust and apply |
|
6270 start = start / scale; |
|
6271 jQuery.style( tween.elem, prop, start + unit ); |
|
6272 |
|
6273 // Update scale, tolerating zero or NaN from tween.cur() |
|
6274 // And breaking the loop if scale is unchanged or perfect, or if we've just had enough |
|
6275 } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); |
|
6276 } |
|
6277 |
|
6278 // Update tween properties |
|
6279 if ( parts ) { |
|
6280 start = tween.start = +start || +target || 0; |
|
6281 tween.unit = unit; |
|
6282 // If a +=/-= token was provided, we're doing a relative animation |
|
6283 tween.end = parts[ 1 ] ? |
|
6284 start + ( parts[ 1 ] + 1 ) * parts[ 2 ] : |
|
6285 +parts[ 2 ]; |
|
6286 } |
|
6287 |
|
6288 return tween; |
|
6289 } ] |
|
6290 }; |
|
6291 |
|
6292 // Animations created synchronously will run synchronously |
|
6293 function createFxNow() { |
|
6294 setTimeout(function() { |
|
6295 fxNow = undefined; |
|
6296 }); |
|
6297 return ( fxNow = jQuery.now() ); |
|
6298 } |
|
6299 |
|
6300 // Generate parameters to create a standard animation |
|
6301 function genFx( type, includeWidth ) { |
|
6302 var which, |
|
6303 i = 0, |
|
6304 attrs = { height: type }; |
|
6305 |
|
6306 // if we include width, step value is 1 to do all cssExpand values, |
|
6307 // if we don't include width, step value is 2 to skip over Left and Right |
|
6308 includeWidth = includeWidth ? 1 : 0; |
|
6309 for ( ; i < 4 ; i += 2 - includeWidth ) { |
|
6310 which = cssExpand[ i ]; |
|
6311 attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; |
|
6312 } |
|
6313 |
|
6314 if ( includeWidth ) { |
|
6315 attrs.opacity = attrs.width = type; |
|
6316 } |
|
6317 |
|
6318 return attrs; |
|
6319 } |
|
6320 |
|
6321 function createTween( value, prop, animation ) { |
|
6322 var tween, |
|
6323 collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), |
|
6324 index = 0, |
|
6325 length = collection.length; |
|
6326 for ( ; index < length; index++ ) { |
|
6327 if ( (tween = collection[ index ].call( animation, prop, value )) ) { |
|
6328 |
|
6329 // we're done with this property |
|
6330 return tween; |
|
6331 } |
|
6332 } |
|
6333 } |
|
6334 |
|
6335 function defaultPrefilter( elem, props, opts ) { |
|
6336 /* jshint validthis: true */ |
|
6337 var prop, value, toggle, tween, hooks, oldfire, display, checkDisplay, |
|
6338 anim = this, |
|
6339 orig = {}, |
|
6340 style = elem.style, |
|
6341 hidden = elem.nodeType && isHidden( elem ), |
|
6342 dataShow = data_priv.get( elem, "fxshow" ); |
|
6343 |
|
6344 // handle queue: false promises |
|
6345 if ( !opts.queue ) { |
|
6346 hooks = jQuery._queueHooks( elem, "fx" ); |
|
6347 if ( hooks.unqueued == null ) { |
|
6348 hooks.unqueued = 0; |
|
6349 oldfire = hooks.empty.fire; |
|
6350 hooks.empty.fire = function() { |
|
6351 if ( !hooks.unqueued ) { |
|
6352 oldfire(); |
|
6353 } |
|
6354 }; |
|
6355 } |
|
6356 hooks.unqueued++; |
|
6357 |
|
6358 anim.always(function() { |
|
6359 // doing this makes sure that the complete handler will be called |
|
6360 // before this completes |
|
6361 anim.always(function() { |
|
6362 hooks.unqueued--; |
|
6363 if ( !jQuery.queue( elem, "fx" ).length ) { |
|
6364 hooks.empty.fire(); |
|
6365 } |
|
6366 }); |
|
6367 }); |
|
6368 } |
|
6369 |
|
6370 // height/width overflow pass |
|
6371 if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) { |
|
6372 // Make sure that nothing sneaks out |
|
6373 // Record all 3 overflow attributes because IE9-10 do not |
|
6374 // change the overflow attribute when overflowX and |
|
6375 // overflowY are set to the same value |
|
6376 opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; |
|
6377 |
|
6378 // Set display property to inline-block for height/width |
|
6379 // animations on inline elements that are having width/height animated |
|
6380 display = jQuery.css( elem, "display" ); |
|
6381 |
|
6382 // Test default display if display is currently "none" |
|
6383 checkDisplay = display === "none" ? |
|
6384 data_priv.get( elem, "olddisplay" ) || defaultDisplay( elem.nodeName ) : display; |
|
6385 |
|
6386 if ( checkDisplay === "inline" && jQuery.css( elem, "float" ) === "none" ) { |
|
6387 style.display = "inline-block"; |
|
6388 } |
|
6389 } |
|
6390 |
|
6391 if ( opts.overflow ) { |
|
6392 style.overflow = "hidden"; |
|
6393 anim.always(function() { |
|
6394 style.overflow = opts.overflow[ 0 ]; |
|
6395 style.overflowX = opts.overflow[ 1 ]; |
|
6396 style.overflowY = opts.overflow[ 2 ]; |
|
6397 }); |
|
6398 } |
|
6399 |
|
6400 // show/hide pass |
|
6401 for ( prop in props ) { |
|
6402 value = props[ prop ]; |
|
6403 if ( rfxtypes.exec( value ) ) { |
|
6404 delete props[ prop ]; |
|
6405 toggle = toggle || value === "toggle"; |
|
6406 if ( value === ( hidden ? "hide" : "show" ) ) { |
|
6407 |
|
6408 // If there is dataShow left over from a stopped hide or show and we are going to proceed with show, we should pretend to be hidden |
|
6409 if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { |
|
6410 hidden = true; |
|
6411 } else { |
|
6412 continue; |
|
6413 } |
|
6414 } |
|
6415 orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); |
|
6416 |
|
6417 // Any non-fx value stops us from restoring the original display value |
|
6418 } else { |
|
6419 display = undefined; |
|
6420 } |
|
6421 } |
|
6422 |
|
6423 if ( !jQuery.isEmptyObject( orig ) ) { |
|
6424 if ( dataShow ) { |
|
6425 if ( "hidden" in dataShow ) { |
|
6426 hidden = dataShow.hidden; |
|
6427 } |
|
6428 } else { |
|
6429 dataShow = data_priv.access( elem, "fxshow", {} ); |
|
6430 } |
|
6431 |
|
6432 // store state if its toggle - enables .stop().toggle() to "reverse" |
|
6433 if ( toggle ) { |
|
6434 dataShow.hidden = !hidden; |
|
6435 } |
|
6436 if ( hidden ) { |
|
6437 jQuery( elem ).show(); |
|
6438 } else { |
|
6439 anim.done(function() { |
|
6440 jQuery( elem ).hide(); |
|
6441 }); |
|
6442 } |
|
6443 anim.done(function() { |
|
6444 var prop; |
|
6445 |
|
6446 data_priv.remove( elem, "fxshow" ); |
|
6447 for ( prop in orig ) { |
|
6448 jQuery.style( elem, prop, orig[ prop ] ); |
|
6449 } |
|
6450 }); |
|
6451 for ( prop in orig ) { |
|
6452 tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); |
|
6453 |
|
6454 if ( !( prop in dataShow ) ) { |
|
6455 dataShow[ prop ] = tween.start; |
|
6456 if ( hidden ) { |
|
6457 tween.end = tween.start; |
|
6458 tween.start = prop === "width" || prop === "height" ? 1 : 0; |
|
6459 } |
|
6460 } |
|
6461 } |
|
6462 |
|
6463 // If this is a noop like .hide().hide(), restore an overwritten display value |
|
6464 } else if ( (display === "none" ? defaultDisplay( elem.nodeName ) : display) === "inline" ) { |
|
6465 style.display = display; |
|
6466 } |
|
6467 } |
|
6468 |
|
6469 function propFilter( props, specialEasing ) { |
|
6470 var index, name, easing, value, hooks; |
|
6471 |
|
6472 // camelCase, specialEasing and expand cssHook pass |
|
6473 for ( index in props ) { |
|
6474 name = jQuery.camelCase( index ); |
|
6475 easing = specialEasing[ name ]; |
|
6476 value = props[ index ]; |
|
6477 if ( jQuery.isArray( value ) ) { |
|
6478 easing = value[ 1 ]; |
|
6479 value = props[ index ] = value[ 0 ]; |
|
6480 } |
|
6481 |
|
6482 if ( index !== name ) { |
|
6483 props[ name ] = value; |
|
6484 delete props[ index ]; |
|
6485 } |
|
6486 |
|
6487 hooks = jQuery.cssHooks[ name ]; |
|
6488 if ( hooks && "expand" in hooks ) { |
|
6489 value = hooks.expand( value ); |
|
6490 delete props[ name ]; |
|
6491 |
|
6492 // not quite $.extend, this wont overwrite keys already present. |
|
6493 // also - reusing 'index' from above because we have the correct "name" |
|
6494 for ( index in value ) { |
|
6495 if ( !( index in props ) ) { |
|
6496 props[ index ] = value[ index ]; |
|
6497 specialEasing[ index ] = easing; |
|
6498 } |
|
6499 } |
|
6500 } else { |
|
6501 specialEasing[ name ] = easing; |
|
6502 } |
|
6503 } |
|
6504 } |
|
6505 |
|
6506 function Animation( elem, properties, options ) { |
|
6507 var result, |
|
6508 stopped, |
|
6509 index = 0, |
|
6510 length = animationPrefilters.length, |
|
6511 deferred = jQuery.Deferred().always( function() { |
|
6512 // don't match elem in the :animated selector |
|
6513 delete tick.elem; |
|
6514 }), |
|
6515 tick = function() { |
|
6516 if ( stopped ) { |
|
6517 return false; |
|
6518 } |
|
6519 var currentTime = fxNow || createFxNow(), |
|
6520 remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), |
|
6521 // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) |
|
6522 temp = remaining / animation.duration || 0, |
|
6523 percent = 1 - temp, |
|
6524 index = 0, |
|
6525 length = animation.tweens.length; |
|
6526 |
|
6527 for ( ; index < length ; index++ ) { |
|
6528 animation.tweens[ index ].run( percent ); |
|
6529 } |
|
6530 |
|
6531 deferred.notifyWith( elem, [ animation, percent, remaining ]); |
|
6532 |
|
6533 if ( percent < 1 && length ) { |
|
6534 return remaining; |
|
6535 } else { |
|
6536 deferred.resolveWith( elem, [ animation ] ); |
|
6537 return false; |
|
6538 } |
|
6539 }, |
|
6540 animation = deferred.promise({ |
|
6541 elem: elem, |
|
6542 props: jQuery.extend( {}, properties ), |
|
6543 opts: jQuery.extend( true, { specialEasing: {} }, options ), |
|
6544 originalProperties: properties, |
|
6545 originalOptions: options, |
|
6546 startTime: fxNow || createFxNow(), |
|
6547 duration: options.duration, |
|
6548 tweens: [], |
|
6549 createTween: function( prop, end ) { |
|
6550 var tween = jQuery.Tween( elem, animation.opts, prop, end, |
|
6551 animation.opts.specialEasing[ prop ] || animation.opts.easing ); |
|
6552 animation.tweens.push( tween ); |
|
6553 return tween; |
|
6554 }, |
|
6555 stop: function( gotoEnd ) { |
|
6556 var index = 0, |
|
6557 // if we are going to the end, we want to run all the tweens |
|
6558 // otherwise we skip this part |
|
6559 length = gotoEnd ? animation.tweens.length : 0; |
|
6560 if ( stopped ) { |
|
6561 return this; |
|
6562 } |
|
6563 stopped = true; |
|
6564 for ( ; index < length ; index++ ) { |
|
6565 animation.tweens[ index ].run( 1 ); |
|
6566 } |
|
6567 |
|
6568 // resolve when we played the last frame |
|
6569 // otherwise, reject |
|
6570 if ( gotoEnd ) { |
|
6571 deferred.resolveWith( elem, [ animation, gotoEnd ] ); |
|
6572 } else { |
|
6573 deferred.rejectWith( elem, [ animation, gotoEnd ] ); |
|
6574 } |
|
6575 return this; |
|
6576 } |
|
6577 }), |
|
6578 props = animation.props; |
|
6579 |
|
6580 propFilter( props, animation.opts.specialEasing ); |
|
6581 |
|
6582 for ( ; index < length ; index++ ) { |
|
6583 result = animationPrefilters[ index ].call( animation, elem, props, animation.opts ); |
|
6584 if ( result ) { |
|
6585 return result; |
|
6586 } |
|
6587 } |
|
6588 |
|
6589 jQuery.map( props, createTween, animation ); |
|
6590 |
|
6591 if ( jQuery.isFunction( animation.opts.start ) ) { |
|
6592 animation.opts.start.call( elem, animation ); |
|
6593 } |
|
6594 |
|
6595 jQuery.fx.timer( |
|
6596 jQuery.extend( tick, { |
|
6597 elem: elem, |
|
6598 anim: animation, |
|
6599 queue: animation.opts.queue |
|
6600 }) |
|
6601 ); |
|
6602 |
|
6603 // attach callbacks from options |
|
6604 return animation.progress( animation.opts.progress ) |
|
6605 .done( animation.opts.done, animation.opts.complete ) |
|
6606 .fail( animation.opts.fail ) |
|
6607 .always( animation.opts.always ); |
|
6608 } |
|
6609 |
|
6610 jQuery.Animation = jQuery.extend( Animation, { |
|
6611 |
|
6612 tweener: function( props, callback ) { |
|
6613 if ( jQuery.isFunction( props ) ) { |
|
6614 callback = props; |
|
6615 props = [ "*" ]; |
|
6616 } else { |
|
6617 props = props.split(" "); |
|
6618 } |
|
6619 |
|
6620 var prop, |
|
6621 index = 0, |
|
6622 length = props.length; |
|
6623 |
|
6624 for ( ; index < length ; index++ ) { |
|
6625 prop = props[ index ]; |
|
6626 tweeners[ prop ] = tweeners[ prop ] || []; |
|
6627 tweeners[ prop ].unshift( callback ); |
|
6628 } |
|
6629 }, |
|
6630 |
|
6631 prefilter: function( callback, prepend ) { |
|
6632 if ( prepend ) { |
|
6633 animationPrefilters.unshift( callback ); |
|
6634 } else { |
|
6635 animationPrefilters.push( callback ); |
|
6636 } |
|
6637 } |
|
6638 }); |
|
6639 |
|
6640 jQuery.speed = function( speed, easing, fn ) { |
|
6641 var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { |
|
6642 complete: fn || !fn && easing || |
|
6643 jQuery.isFunction( speed ) && speed, |
|
6644 duration: speed, |
|
6645 easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing |
|
6646 }; |
|
6647 |
|
6648 opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : |
|
6649 opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; |
|
6650 |
|
6651 // normalize opt.queue - true/undefined/null -> "fx" |
|
6652 if ( opt.queue == null || opt.queue === true ) { |
|
6653 opt.queue = "fx"; |
|
6654 } |
|
6655 |
|
6656 // Queueing |
|
6657 opt.old = opt.complete; |
|
6658 |
|
6659 opt.complete = function() { |
|
6660 if ( jQuery.isFunction( opt.old ) ) { |
|
6661 opt.old.call( this ); |
|
6662 } |
|
6663 |
|
6664 if ( opt.queue ) { |
|
6665 jQuery.dequeue( this, opt.queue ); |
|
6666 } |
|
6667 }; |
|
6668 |
|
6669 return opt; |
|
6670 }; |
|
6671 |
|
6672 jQuery.fn.extend({ |
|
6673 fadeTo: function( speed, to, easing, callback ) { |
|
6674 |
|
6675 // show any hidden elements after setting opacity to 0 |
|
6676 return this.filter( isHidden ).css( "opacity", 0 ).show() |
|
6677 |
|
6678 // animate to the value specified |
|
6679 .end().animate({ opacity: to }, speed, easing, callback ); |
|
6680 }, |
|
6681 animate: function( prop, speed, easing, callback ) { |
|
6682 var empty = jQuery.isEmptyObject( prop ), |
|
6683 optall = jQuery.speed( speed, easing, callback ), |
|
6684 doAnimation = function() { |
|
6685 // Operate on a copy of prop so per-property easing won't be lost |
|
6686 var anim = Animation( this, jQuery.extend( {}, prop ), optall ); |
|
6687 |
|
6688 // Empty animations, or finishing resolves immediately |
|
6689 if ( empty || data_priv.get( this, "finish" ) ) { |
|
6690 anim.stop( true ); |
|
6691 } |
|
6692 }; |
|
6693 doAnimation.finish = doAnimation; |
|
6694 |
|
6695 return empty || optall.queue === false ? |
|
6696 this.each( doAnimation ) : |
|
6697 this.queue( optall.queue, doAnimation ); |
|
6698 }, |
|
6699 stop: function( type, clearQueue, gotoEnd ) { |
|
6700 var stopQueue = function( hooks ) { |
|
6701 var stop = hooks.stop; |
|
6702 delete hooks.stop; |
|
6703 stop( gotoEnd ); |
|
6704 }; |
|
6705 |
|
6706 if ( typeof type !== "string" ) { |
|
6707 gotoEnd = clearQueue; |
|
6708 clearQueue = type; |
|
6709 type = undefined; |
|
6710 } |
|
6711 if ( clearQueue && type !== false ) { |
|
6712 this.queue( type || "fx", [] ); |
|
6713 } |
|
6714 |
|
6715 return this.each(function() { |
|
6716 var dequeue = true, |
|
6717 index = type != null && type + "queueHooks", |
|
6718 timers = jQuery.timers, |
|
6719 data = data_priv.get( this ); |
|
6720 |
|
6721 if ( index ) { |
|
6722 if ( data[ index ] && data[ index ].stop ) { |
|
6723 stopQueue( data[ index ] ); |
|
6724 } |
|
6725 } else { |
|
6726 for ( index in data ) { |
|
6727 if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { |
|
6728 stopQueue( data[ index ] ); |
|
6729 } |
|
6730 } |
|
6731 } |
|
6732 |
|
6733 for ( index = timers.length; index--; ) { |
|
6734 if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { |
|
6735 timers[ index ].anim.stop( gotoEnd ); |
|
6736 dequeue = false; |
|
6737 timers.splice( index, 1 ); |
|
6738 } |
|
6739 } |
|
6740 |
|
6741 // start the next in the queue if the last step wasn't forced |
|
6742 // timers currently will call their complete callbacks, which will dequeue |
|
6743 // but only if they were gotoEnd |
|
6744 if ( dequeue || !gotoEnd ) { |
|
6745 jQuery.dequeue( this, type ); |
|
6746 } |
|
6747 }); |
|
6748 }, |
|
6749 finish: function( type ) { |
|
6750 if ( type !== false ) { |
|
6751 type = type || "fx"; |
|
6752 } |
|
6753 return this.each(function() { |
|
6754 var index, |
|
6755 data = data_priv.get( this ), |
|
6756 queue = data[ type + "queue" ], |
|
6757 hooks = data[ type + "queueHooks" ], |
|
6758 timers = jQuery.timers, |
|
6759 length = queue ? queue.length : 0; |
|
6760 |
|
6761 // enable finishing flag on private data |
|
6762 data.finish = true; |
|
6763 |
|
6764 // empty the queue first |
|
6765 jQuery.queue( this, type, [] ); |
|
6766 |
|
6767 if ( hooks && hooks.stop ) { |
|
6768 hooks.stop.call( this, true ); |
|
6769 } |
|
6770 |
|
6771 // look for any active animations, and finish them |
|
6772 for ( index = timers.length; index--; ) { |
|
6773 if ( timers[ index ].elem === this && timers[ index ].queue === type ) { |
|
6774 timers[ index ].anim.stop( true ); |
|
6775 timers.splice( index, 1 ); |
|
6776 } |
|
6777 } |
|
6778 |
|
6779 // look for any animations in the old queue and finish them |
|
6780 for ( index = 0; index < length; index++ ) { |
|
6781 if ( queue[ index ] && queue[ index ].finish ) { |
|
6782 queue[ index ].finish.call( this ); |
|
6783 } |
|
6784 } |
|
6785 |
|
6786 // turn off finishing flag |
|
6787 delete data.finish; |
|
6788 }); |
|
6789 } |
|
6790 }); |
|
6791 |
|
6792 jQuery.each([ "toggle", "show", "hide" ], function( i, name ) { |
|
6793 var cssFn = jQuery.fn[ name ]; |
|
6794 jQuery.fn[ name ] = function( speed, easing, callback ) { |
|
6795 return speed == null || typeof speed === "boolean" ? |
|
6796 cssFn.apply( this, arguments ) : |
|
6797 this.animate( genFx( name, true ), speed, easing, callback ); |
|
6798 }; |
|
6799 }); |
|
6800 |
|
6801 // Generate shortcuts for custom animations |
|
6802 jQuery.each({ |
|
6803 slideDown: genFx("show"), |
|
6804 slideUp: genFx("hide"), |
|
6805 slideToggle: genFx("toggle"), |
|
6806 fadeIn: { opacity: "show" }, |
|
6807 fadeOut: { opacity: "hide" }, |
|
6808 fadeToggle: { opacity: "toggle" } |
|
6809 }, function( name, props ) { |
|
6810 jQuery.fn[ name ] = function( speed, easing, callback ) { |
|
6811 return this.animate( props, speed, easing, callback ); |
|
6812 }; |
|
6813 }); |
|
6814 |
|
6815 jQuery.timers = []; |
|
6816 jQuery.fx.tick = function() { |
|
6817 var timer, |
|
6818 i = 0, |
|
6819 timers = jQuery.timers; |
|
6820 |
|
6821 fxNow = jQuery.now(); |
|
6822 |
|
6823 for ( ; i < timers.length; i++ ) { |
|
6824 timer = timers[ i ]; |
|
6825 // Checks the timer has not already been removed |
|
6826 if ( !timer() && timers[ i ] === timer ) { |
|
6827 timers.splice( i--, 1 ); |
|
6828 } |
|
6829 } |
|
6830 |
|
6831 if ( !timers.length ) { |
|
6832 jQuery.fx.stop(); |
|
6833 } |
|
6834 fxNow = undefined; |
|
6835 }; |
|
6836 |
|
6837 jQuery.fx.timer = function( timer ) { |
|
6838 jQuery.timers.push( timer ); |
|
6839 if ( timer() ) { |
|
6840 jQuery.fx.start(); |
|
6841 } else { |
|
6842 jQuery.timers.pop(); |
|
6843 } |
|
6844 }; |
|
6845 |
|
6846 jQuery.fx.interval = 13; |
|
6847 |
|
6848 jQuery.fx.start = function() { |
|
6849 if ( !timerId ) { |
|
6850 timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); |
|
6851 } |
|
6852 }; |
|
6853 |
|
6854 jQuery.fx.stop = function() { |
|
6855 clearInterval( timerId ); |
|
6856 timerId = null; |
|
6857 }; |
|
6858 |
|
6859 jQuery.fx.speeds = { |
|
6860 slow: 600, |
|
6861 fast: 200, |
|
6862 // Default speed |
|
6863 _default: 400 |
|
6864 }; |
|
6865 |
|
6866 |
|
6867 // Based off of the plugin by Clint Helfers, with permission. |
|
6868 // http://blindsignals.com/index.php/2009/07/jquery-delay/ |
|
6869 jQuery.fn.delay = function( time, type ) { |
|
6870 time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; |
|
6871 type = type || "fx"; |
|
6872 |
|
6873 return this.queue( type, function( next, hooks ) { |
|
6874 var timeout = setTimeout( next, time ); |
|
6875 hooks.stop = function() { |
|
6876 clearTimeout( timeout ); |
|
6877 }; |
|
6878 }); |
|
6879 }; |
|
6880 |
|
6881 |
|
6882 (function() { |
|
6883 var input = document.createElement( "input" ), |
|
6884 select = document.createElement( "select" ), |
|
6885 opt = select.appendChild( document.createElement( "option" ) ); |
|
6886 |
|
6887 input.type = "checkbox"; |
|
6888 |
|
6889 // Support: iOS 5.1, Android 4.x, Android 2.3 |
|
6890 // Check the default checkbox/radio value ("" on old WebKit; "on" elsewhere) |
|
6891 support.checkOn = input.value !== ""; |
|
6892 |
|
6893 // Must access the parent to make an option select properly |
|
6894 // Support: IE9, IE10 |
|
6895 support.optSelected = opt.selected; |
|
6896 |
|
6897 // Make sure that the options inside disabled selects aren't marked as disabled |
|
6898 // (WebKit marks them as disabled) |
|
6899 select.disabled = true; |
|
6900 support.optDisabled = !opt.disabled; |
|
6901 |
|
6902 // Check if an input maintains its value after becoming a radio |
|
6903 // Support: IE9, IE10 |
|
6904 input = document.createElement( "input" ); |
|
6905 input.value = "t"; |
|
6906 input.type = "radio"; |
|
6907 support.radioValue = input.value === "t"; |
|
6908 })(); |
|
6909 |
|
6910 |
|
6911 var nodeHook, boolHook, |
|
6912 attrHandle = jQuery.expr.attrHandle; |
|
6913 |
|
6914 jQuery.fn.extend({ |
|
6915 attr: function( name, value ) { |
|
6916 return access( this, jQuery.attr, name, value, arguments.length > 1 ); |
|
6917 }, |
|
6918 |
|
6919 removeAttr: function( name ) { |
|
6920 return this.each(function() { |
|
6921 jQuery.removeAttr( this, name ); |
|
6922 }); |
|
6923 } |
|
6924 }); |
|
6925 |
|
6926 jQuery.extend({ |
|
6927 attr: function( elem, name, value ) { |
|
6928 var hooks, ret, |
|
6929 nType = elem.nodeType; |
|
6930 |
|
6931 // don't get/set attributes on text, comment and attribute nodes |
|
6932 if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { |
|
6933 return; |
|
6934 } |
|
6935 |
|
6936 // Fallback to prop when attributes are not supported |
|
6937 if ( typeof elem.getAttribute === strundefined ) { |
|
6938 return jQuery.prop( elem, name, value ); |
|
6939 } |
|
6940 |
|
6941 // All attributes are lowercase |
|
6942 // Grab necessary hook if one is defined |
|
6943 if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { |
|
6944 name = name.toLowerCase(); |
|
6945 hooks = jQuery.attrHooks[ name ] || |
|
6946 ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); |
|
6947 } |
|
6948 |
|
6949 if ( value !== undefined ) { |
|
6950 |
|
6951 if ( value === null ) { |
|
6952 jQuery.removeAttr( elem, name ); |
|
6953 |
|
6954 } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { |
|
6955 return ret; |
|
6956 |
|
6957 } else { |
|
6958 elem.setAttribute( name, value + "" ); |
|
6959 return value; |
|
6960 } |
|
6961 |
|
6962 } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { |
|
6963 return ret; |
|
6964 |
|
6965 } else { |
|
6966 ret = jQuery.find.attr( elem, name ); |
|
6967 |
|
6968 // Non-existent attributes return null, we normalize to undefined |
|
6969 return ret == null ? |
|
6970 undefined : |
|
6971 ret; |
|
6972 } |
|
6973 }, |
|
6974 |
|
6975 removeAttr: function( elem, value ) { |
|
6976 var name, propName, |
|
6977 i = 0, |
|
6978 attrNames = value && value.match( rnotwhite ); |
|
6979 |
|
6980 if ( attrNames && elem.nodeType === 1 ) { |
|
6981 while ( (name = attrNames[i++]) ) { |
|
6982 propName = jQuery.propFix[ name ] || name; |
|
6983 |
|
6984 // Boolean attributes get special treatment (#10870) |
|
6985 if ( jQuery.expr.match.bool.test( name ) ) { |
|
6986 // Set corresponding property to false |
|
6987 elem[ propName ] = false; |
|
6988 } |
|
6989 |
|
6990 elem.removeAttribute( name ); |
|
6991 } |
|
6992 } |
|
6993 }, |
|
6994 |
|
6995 attrHooks: { |
|
6996 type: { |
|
6997 set: function( elem, value ) { |
|
6998 if ( !support.radioValue && value === "radio" && |
|
6999 jQuery.nodeName( elem, "input" ) ) { |
|
7000 // Setting the type on a radio button after the value resets the value in IE6-9 |
|
7001 // Reset value to default in case type is set after value during creation |
|
7002 var val = elem.value; |
|
7003 elem.setAttribute( "type", value ); |
|
7004 if ( val ) { |
|
7005 elem.value = val; |
|
7006 } |
|
7007 return value; |
|
7008 } |
|
7009 } |
|
7010 } |
|
7011 } |
|
7012 }); |
|
7013 |
|
7014 // Hooks for boolean attributes |
|
7015 boolHook = { |
|
7016 set: function( elem, value, name ) { |
|
7017 if ( value === false ) { |
|
7018 // Remove boolean attributes when set to false |
|
7019 jQuery.removeAttr( elem, name ); |
|
7020 } else { |
|
7021 elem.setAttribute( name, name ); |
|
7022 } |
|
7023 return name; |
|
7024 } |
|
7025 }; |
|
7026 jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { |
|
7027 var getter = attrHandle[ name ] || jQuery.find.attr; |
|
7028 |
|
7029 attrHandle[ name ] = function( elem, name, isXML ) { |
|
7030 var ret, handle; |
|
7031 if ( !isXML ) { |
|
7032 // Avoid an infinite loop by temporarily removing this function from the getter |
|
7033 handle = attrHandle[ name ]; |
|
7034 attrHandle[ name ] = ret; |
|
7035 ret = getter( elem, name, isXML ) != null ? |
|
7036 name.toLowerCase() : |
|
7037 null; |
|
7038 attrHandle[ name ] = handle; |
|
7039 } |
|
7040 return ret; |
|
7041 }; |
|
7042 }); |
|
7043 |
|
7044 |
|
7045 |
|
7046 |
|
7047 var rfocusable = /^(?:input|select|textarea|button)$/i; |
|
7048 |
|
7049 jQuery.fn.extend({ |
|
7050 prop: function( name, value ) { |
|
7051 return access( this, jQuery.prop, name, value, arguments.length > 1 ); |
|
7052 }, |
|
7053 |
|
7054 removeProp: function( name ) { |
|
7055 return this.each(function() { |
|
7056 delete this[ jQuery.propFix[ name ] || name ]; |
|
7057 }); |
|
7058 } |
|
7059 }); |
|
7060 |
|
7061 jQuery.extend({ |
|
7062 propFix: { |
|
7063 "for": "htmlFor", |
|
7064 "class": "className" |
|
7065 }, |
|
7066 |
|
7067 prop: function( elem, name, value ) { |
|
7068 var ret, hooks, notxml, |
|
7069 nType = elem.nodeType; |
|
7070 |
|
7071 // don't get/set properties on text, comment and attribute nodes |
|
7072 if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { |
|
7073 return; |
|
7074 } |
|
7075 |
|
7076 notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); |
|
7077 |
|
7078 if ( notxml ) { |
|
7079 // Fix name and attach hooks |
|
7080 name = jQuery.propFix[ name ] || name; |
|
7081 hooks = jQuery.propHooks[ name ]; |
|
7082 } |
|
7083 |
|
7084 if ( value !== undefined ) { |
|
7085 return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? |
|
7086 ret : |
|
7087 ( elem[ name ] = value ); |
|
7088 |
|
7089 } else { |
|
7090 return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? |
|
7091 ret : |
|
7092 elem[ name ]; |
|
7093 } |
|
7094 }, |
|
7095 |
|
7096 propHooks: { |
|
7097 tabIndex: { |
|
7098 get: function( elem ) { |
|
7099 return elem.hasAttribute( "tabindex" ) || rfocusable.test( elem.nodeName ) || elem.href ? |
|
7100 elem.tabIndex : |
|
7101 -1; |
|
7102 } |
|
7103 } |
|
7104 } |
|
7105 }); |
|
7106 |
|
7107 // Support: IE9+ |
|
7108 // Selectedness for an option in an optgroup can be inaccurate |
|
7109 if ( !support.optSelected ) { |
|
7110 jQuery.propHooks.selected = { |
|
7111 get: function( elem ) { |
|
7112 var parent = elem.parentNode; |
|
7113 if ( parent && parent.parentNode ) { |
|
7114 parent.parentNode.selectedIndex; |
|
7115 } |
|
7116 return null; |
|
7117 } |
|
7118 }; |
|
7119 } |
|
7120 |
|
7121 jQuery.each([ |
|
7122 "tabIndex", |
|
7123 "readOnly", |
|
7124 "maxLength", |
|
7125 "cellSpacing", |
|
7126 "cellPadding", |
|
7127 "rowSpan", |
|
7128 "colSpan", |
|
7129 "useMap", |
|
7130 "frameBorder", |
|
7131 "contentEditable" |
|
7132 ], function() { |
|
7133 jQuery.propFix[ this.toLowerCase() ] = this; |
|
7134 }); |
|
7135 |
|
7136 |
|
7137 |
|
7138 |
|
7139 var rclass = /[\t\r\n\f]/g; |
|
7140 |
|
7141 jQuery.fn.extend({ |
|
7142 addClass: function( value ) { |
|
7143 var classes, elem, cur, clazz, j, finalValue, |
|
7144 proceed = typeof value === "string" && value, |
|
7145 i = 0, |
|
7146 len = this.length; |
|
7147 |
|
7148 if ( jQuery.isFunction( value ) ) { |
|
7149 return this.each(function( j ) { |
|
7150 jQuery( this ).addClass( value.call( this, j, this.className ) ); |
|
7151 }); |
|
7152 } |
|
7153 |
|
7154 if ( proceed ) { |
|
7155 // The disjunction here is for better compressibility (see removeClass) |
|
7156 classes = ( value || "" ).match( rnotwhite ) || []; |
|
7157 |
|
7158 for ( ; i < len; i++ ) { |
|
7159 elem = this[ i ]; |
|
7160 cur = elem.nodeType === 1 && ( elem.className ? |
|
7161 ( " " + elem.className + " " ).replace( rclass, " " ) : |
|
7162 " " |
|
7163 ); |
|
7164 |
|
7165 if ( cur ) { |
|
7166 j = 0; |
|
7167 while ( (clazz = classes[j++]) ) { |
|
7168 if ( cur.indexOf( " " + clazz + " " ) < 0 ) { |
|
7169 cur += clazz + " "; |
|
7170 } |
|
7171 } |
|
7172 |
|
7173 // only assign if different to avoid unneeded rendering. |
|
7174 finalValue = jQuery.trim( cur ); |
|
7175 if ( elem.className !== finalValue ) { |
|
7176 elem.className = finalValue; |
|
7177 } |
|
7178 } |
|
7179 } |
|
7180 } |
|
7181 |
|
7182 return this; |
|
7183 }, |
|
7184 |
|
7185 removeClass: function( value ) { |
|
7186 var classes, elem, cur, clazz, j, finalValue, |
|
7187 proceed = arguments.length === 0 || typeof value === "string" && value, |
|
7188 i = 0, |
|
7189 len = this.length; |
|
7190 |
|
7191 if ( jQuery.isFunction( value ) ) { |
|
7192 return this.each(function( j ) { |
|
7193 jQuery( this ).removeClass( value.call( this, j, this.className ) ); |
|
7194 }); |
|
7195 } |
|
7196 if ( proceed ) { |
|
7197 classes = ( value || "" ).match( rnotwhite ) || []; |
|
7198 |
|
7199 for ( ; i < len; i++ ) { |
|
7200 elem = this[ i ]; |
|
7201 // This expression is here for better compressibility (see addClass) |
|
7202 cur = elem.nodeType === 1 && ( elem.className ? |
|
7203 ( " " + elem.className + " " ).replace( rclass, " " ) : |
|
7204 "" |
|
7205 ); |
|
7206 |
|
7207 if ( cur ) { |
|
7208 j = 0; |
|
7209 while ( (clazz = classes[j++]) ) { |
|
7210 // Remove *all* instances |
|
7211 while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { |
|
7212 cur = cur.replace( " " + clazz + " ", " " ); |
|
7213 } |
|
7214 } |
|
7215 |
|
7216 // only assign if different to avoid unneeded rendering. |
|
7217 finalValue = value ? jQuery.trim( cur ) : ""; |
|
7218 if ( elem.className !== finalValue ) { |
|
7219 elem.className = finalValue; |
|
7220 } |
|
7221 } |
|
7222 } |
|
7223 } |
|
7224 |
|
7225 return this; |
|
7226 }, |
|
7227 |
|
7228 toggleClass: function( value, stateVal ) { |
|
7229 var type = typeof value; |
|
7230 |
|
7231 if ( typeof stateVal === "boolean" && type === "string" ) { |
|
7232 return stateVal ? this.addClass( value ) : this.removeClass( value ); |
|
7233 } |
|
7234 |
|
7235 if ( jQuery.isFunction( value ) ) { |
|
7236 return this.each(function( i ) { |
|
7237 jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); |
|
7238 }); |
|
7239 } |
|
7240 |
|
7241 return this.each(function() { |
|
7242 if ( type === "string" ) { |
|
7243 // toggle individual class names |
|
7244 var className, |
|
7245 i = 0, |
|
7246 self = jQuery( this ), |
|
7247 classNames = value.match( rnotwhite ) || []; |
|
7248 |
|
7249 while ( (className = classNames[ i++ ]) ) { |
|
7250 // check each className given, space separated list |
|
7251 if ( self.hasClass( className ) ) { |
|
7252 self.removeClass( className ); |
|
7253 } else { |
|
7254 self.addClass( className ); |
|
7255 } |
|
7256 } |
|
7257 |
|
7258 // Toggle whole class name |
|
7259 } else if ( type === strundefined || type === "boolean" ) { |
|
7260 if ( this.className ) { |
|
7261 // store className if set |
|
7262 data_priv.set( this, "__className__", this.className ); |
|
7263 } |
|
7264 |
|
7265 // If the element has a class name or if we're passed "false", |
|
7266 // then remove the whole classname (if there was one, the above saved it). |
|
7267 // Otherwise bring back whatever was previously saved (if anything), |
|
7268 // falling back to the empty string if nothing was stored. |
|
7269 this.className = this.className || value === false ? "" : data_priv.get( this, "__className__" ) || ""; |
|
7270 } |
|
7271 }); |
|
7272 }, |
|
7273 |
|
7274 hasClass: function( selector ) { |
|
7275 var className = " " + selector + " ", |
|
7276 i = 0, |
|
7277 l = this.length; |
|
7278 for ( ; i < l; i++ ) { |
|
7279 if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { |
|
7280 return true; |
|
7281 } |
|
7282 } |
|
7283 |
|
7284 return false; |
|
7285 } |
|
7286 }); |
|
7287 |
|
7288 |
|
7289 |
|
7290 |
|
7291 var rreturn = /\r/g; |
|
7292 |
|
7293 jQuery.fn.extend({ |
|
7294 val: function( value ) { |
|
7295 var hooks, ret, isFunction, |
|
7296 elem = this[0]; |
|
7297 |
|
7298 if ( !arguments.length ) { |
|
7299 if ( elem ) { |
|
7300 hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; |
|
7301 |
|
7302 if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { |
|
7303 return ret; |
|
7304 } |
|
7305 |
|
7306 ret = elem.value; |
|
7307 |
|
7308 return typeof ret === "string" ? |
|
7309 // handle most common string cases |
|
7310 ret.replace(rreturn, "") : |
|
7311 // handle cases where value is null/undef or number |
|
7312 ret == null ? "" : ret; |
|
7313 } |
|
7314 |
|
7315 return; |
|
7316 } |
|
7317 |
|
7318 isFunction = jQuery.isFunction( value ); |
|
7319 |
|
7320 return this.each(function( i ) { |
|
7321 var val; |
|
7322 |
|
7323 if ( this.nodeType !== 1 ) { |
|
7324 return; |
|
7325 } |
|
7326 |
|
7327 if ( isFunction ) { |
|
7328 val = value.call( this, i, jQuery( this ).val() ); |
|
7329 } else { |
|
7330 val = value; |
|
7331 } |
|
7332 |
|
7333 // Treat null/undefined as ""; convert numbers to string |
|
7334 if ( val == null ) { |
|
7335 val = ""; |
|
7336 |
|
7337 } else if ( typeof val === "number" ) { |
|
7338 val += ""; |
|
7339 |
|
7340 } else if ( jQuery.isArray( val ) ) { |
|
7341 val = jQuery.map( val, function( value ) { |
|
7342 return value == null ? "" : value + ""; |
|
7343 }); |
|
7344 } |
|
7345 |
|
7346 hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; |
|
7347 |
|
7348 // If set returns undefined, fall back to normal setting |
|
7349 if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { |
|
7350 this.value = val; |
|
7351 } |
|
7352 }); |
|
7353 } |
|
7354 }); |
|
7355 |
|
7356 jQuery.extend({ |
|
7357 valHooks: { |
|
7358 option: { |
|
7359 get: function( elem ) { |
|
7360 var val = jQuery.find.attr( elem, "value" ); |
|
7361 return val != null ? |
|
7362 val : |
|
7363 // Support: IE10-11+ |
|
7364 // option.text throws exceptions (#14686, #14858) |
|
7365 jQuery.trim( jQuery.text( elem ) ); |
|
7366 } |
|
7367 }, |
|
7368 select: { |
|
7369 get: function( elem ) { |
|
7370 var value, option, |
|
7371 options = elem.options, |
|
7372 index = elem.selectedIndex, |
|
7373 one = elem.type === "select-one" || index < 0, |
|
7374 values = one ? null : [], |
|
7375 max = one ? index + 1 : options.length, |
|
7376 i = index < 0 ? |
|
7377 max : |
|
7378 one ? index : 0; |
|
7379 |
|
7380 // Loop through all the selected options |
|
7381 for ( ; i < max; i++ ) { |
|
7382 option = options[ i ]; |
|
7383 |
|
7384 // IE6-9 doesn't update selected after form reset (#2551) |
|
7385 if ( ( option.selected || i === index ) && |
|
7386 // Don't return options that are disabled or in a disabled optgroup |
|
7387 ( support.optDisabled ? !option.disabled : option.getAttribute( "disabled" ) === null ) && |
|
7388 ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { |
|
7389 |
|
7390 // Get the specific value for the option |
|
7391 value = jQuery( option ).val(); |
|
7392 |
|
7393 // We don't need an array for one selects |
|
7394 if ( one ) { |
|
7395 return value; |
|
7396 } |
|
7397 |
|
7398 // Multi-Selects return an array |
|
7399 values.push( value ); |
|
7400 } |
|
7401 } |
|
7402 |
|
7403 return values; |
|
7404 }, |
|
7405 |
|
7406 set: function( elem, value ) { |
|
7407 var optionSet, option, |
|
7408 options = elem.options, |
|
7409 values = jQuery.makeArray( value ), |
|
7410 i = options.length; |
|
7411 |
|
7412 while ( i-- ) { |
|
7413 option = options[ i ]; |
|
7414 if ( (option.selected = jQuery.inArray( option.value, values ) >= 0) ) { |
|
7415 optionSet = true; |
|
7416 } |
|
7417 } |
|
7418 |
|
7419 // force browsers to behave consistently when non-matching value is set |
|
7420 if ( !optionSet ) { |
|
7421 elem.selectedIndex = -1; |
|
7422 } |
|
7423 return values; |
|
7424 } |
|
7425 } |
|
7426 } |
|
7427 }); |
|
7428 |
|
7429 // Radios and checkboxes getter/setter |
|
7430 jQuery.each([ "radio", "checkbox" ], function() { |
|
7431 jQuery.valHooks[ this ] = { |
|
7432 set: function( elem, value ) { |
|
7433 if ( jQuery.isArray( value ) ) { |
|
7434 return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); |
|
7435 } |
|
7436 } |
|
7437 }; |
|
7438 if ( !support.checkOn ) { |
|
7439 jQuery.valHooks[ this ].get = function( elem ) { |
|
7440 // Support: Webkit |
|
7441 // "" is returned instead of "on" if a value isn't specified |
|
7442 return elem.getAttribute("value") === null ? "on" : elem.value; |
|
7443 }; |
|
7444 } |
|
7445 }); |
|
7446 |
|
7447 |
|
7448 |
|
7449 |
|
7450 // Return jQuery for attributes-only inclusion |
|
7451 |
|
7452 |
|
7453 jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + |
|
7454 "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + |
|
7455 "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { |
|
7456 |
|
7457 // Handle event binding |
|
7458 jQuery.fn[ name ] = function( data, fn ) { |
|
7459 return arguments.length > 0 ? |
|
7460 this.on( name, null, data, fn ) : |
|
7461 this.trigger( name ); |
|
7462 }; |
|
7463 }); |
|
7464 |
|
7465 jQuery.fn.extend({ |
|
7466 hover: function( fnOver, fnOut ) { |
|
7467 return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); |
|
7468 }, |
|
7469 |
|
7470 bind: function( types, data, fn ) { |
|
7471 return this.on( types, null, data, fn ); |
|
7472 }, |
|
7473 unbind: function( types, fn ) { |
|
7474 return this.off( types, null, fn ); |
|
7475 }, |
|
7476 |
|
7477 delegate: function( selector, types, data, fn ) { |
|
7478 return this.on( types, selector, data, fn ); |
|
7479 }, |
|
7480 undelegate: function( selector, types, fn ) { |
|
7481 // ( namespace ) or ( selector, types [, fn] ) |
|
7482 return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); |
|
7483 } |
|
7484 }); |
|
7485 |
|
7486 |
|
7487 var nonce = jQuery.now(); |
|
7488 |
|
7489 var rquery = (/\?/); |
|
7490 |
|
7491 |
|
7492 |
|
7493 // Support: Android 2.3 |
|
7494 // Workaround failure to string-cast null input |
|
7495 jQuery.parseJSON = function( data ) { |
|
7496 return JSON.parse( data + "" ); |
|
7497 }; |
|
7498 |
|
7499 |
|
7500 // Cross-browser xml parsing |
|
7501 jQuery.parseXML = function( data ) { |
|
7502 var xml, tmp; |
|
7503 if ( !data || typeof data !== "string" ) { |
|
7504 return null; |
|
7505 } |
|
7506 |
|
7507 // Support: IE9 |
|
7508 try { |
|
7509 tmp = new DOMParser(); |
|
7510 xml = tmp.parseFromString( data, "text/xml" ); |
|
7511 } catch ( e ) { |
|
7512 xml = undefined; |
|
7513 } |
|
7514 |
|
7515 if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { |
|
7516 jQuery.error( "Invalid XML: " + data ); |
|
7517 } |
|
7518 return xml; |
|
7519 }; |
|
7520 |
|
7521 |
|
7522 var |
|
7523 // Document location |
|
7524 ajaxLocParts, |
|
7525 ajaxLocation, |
|
7526 |
|
7527 rhash = /#.*$/, |
|
7528 rts = /([?&])_=[^&]*/, |
|
7529 rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, |
|
7530 // #7653, #8125, #8152: local protocol detection |
|
7531 rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, |
|
7532 rnoContent = /^(?:GET|HEAD)$/, |
|
7533 rprotocol = /^\/\//, |
|
7534 rurl = /^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/, |
|
7535 |
|
7536 /* Prefilters |
|
7537 * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) |
|
7538 * 2) These are called: |
|
7539 * - BEFORE asking for a transport |
|
7540 * - AFTER param serialization (s.data is a string if s.processData is true) |
|
7541 * 3) key is the dataType |
|
7542 * 4) the catchall symbol "*" can be used |
|
7543 * 5) execution will start with transport dataType and THEN continue down to "*" if needed |
|
7544 */ |
|
7545 prefilters = {}, |
|
7546 |
|
7547 /* Transports bindings |
|
7548 * 1) key is the dataType |
|
7549 * 2) the catchall symbol "*" can be used |
|
7550 * 3) selection will start with transport dataType and THEN go to "*" if needed |
|
7551 */ |
|
7552 transports = {}, |
|
7553 |
|
7554 // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression |
|
7555 allTypes = "*/".concat("*"); |
|
7556 |
|
7557 // #8138, IE may throw an exception when accessing |
|
7558 // a field from window.location if document.domain has been set |
|
7559 try { |
|
7560 ajaxLocation = location.href; |
|
7561 } catch( e ) { |
|
7562 // Use the href attribute of an A element |
|
7563 // since IE will modify it given document.location |
|
7564 ajaxLocation = document.createElement( "a" ); |
|
7565 ajaxLocation.href = ""; |
|
7566 ajaxLocation = ajaxLocation.href; |
|
7567 } |
|
7568 |
|
7569 // Segment location into parts |
|
7570 ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; |
|
7571 |
|
7572 // Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport |
|
7573 function addToPrefiltersOrTransports( structure ) { |
|
7574 |
|
7575 // dataTypeExpression is optional and defaults to "*" |
|
7576 return function( dataTypeExpression, func ) { |
|
7577 |
|
7578 if ( typeof dataTypeExpression !== "string" ) { |
|
7579 func = dataTypeExpression; |
|
7580 dataTypeExpression = "*"; |
|
7581 } |
|
7582 |
|
7583 var dataType, |
|
7584 i = 0, |
|
7585 dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || []; |
|
7586 |
|
7587 if ( jQuery.isFunction( func ) ) { |
|
7588 // For each dataType in the dataTypeExpression |
|
7589 while ( (dataType = dataTypes[i++]) ) { |
|
7590 // Prepend if requested |
|
7591 if ( dataType[0] === "+" ) { |
|
7592 dataType = dataType.slice( 1 ) || "*"; |
|
7593 (structure[ dataType ] = structure[ dataType ] || []).unshift( func ); |
|
7594 |
|
7595 // Otherwise append |
|
7596 } else { |
|
7597 (structure[ dataType ] = structure[ dataType ] || []).push( func ); |
|
7598 } |
|
7599 } |
|
7600 } |
|
7601 }; |
|
7602 } |
|
7603 |
|
7604 // Base inspection function for prefilters and transports |
|
7605 function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { |
|
7606 |
|
7607 var inspected = {}, |
|
7608 seekingTransport = ( structure === transports ); |
|
7609 |
|
7610 function inspect( dataType ) { |
|
7611 var selected; |
|
7612 inspected[ dataType ] = true; |
|
7613 jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { |
|
7614 var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); |
|
7615 if ( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) { |
|
7616 options.dataTypes.unshift( dataTypeOrTransport ); |
|
7617 inspect( dataTypeOrTransport ); |
|
7618 return false; |
|
7619 } else if ( seekingTransport ) { |
|
7620 return !( selected = dataTypeOrTransport ); |
|
7621 } |
|
7622 }); |
|
7623 return selected; |
|
7624 } |
|
7625 |
|
7626 return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); |
|
7627 } |
|
7628 |
|
7629 // A special extend for ajax options |
|
7630 // that takes "flat" options (not to be deep extended) |
|
7631 // Fixes #9887 |
|
7632 function ajaxExtend( target, src ) { |
|
7633 var key, deep, |
|
7634 flatOptions = jQuery.ajaxSettings.flatOptions || {}; |
|
7635 |
|
7636 for ( key in src ) { |
|
7637 if ( src[ key ] !== undefined ) { |
|
7638 ( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ]; |
|
7639 } |
|
7640 } |
|
7641 if ( deep ) { |
|
7642 jQuery.extend( true, target, deep ); |
|
7643 } |
|
7644 |
|
7645 return target; |
|
7646 } |
|
7647 |
|
7648 /* Handles responses to an ajax request: |
|
7649 * - finds the right dataType (mediates between content-type and expected dataType) |
|
7650 * - returns the corresponding response |
|
7651 */ |
|
7652 function ajaxHandleResponses( s, jqXHR, responses ) { |
|
7653 |
|
7654 var ct, type, finalDataType, firstDataType, |
|
7655 contents = s.contents, |
|
7656 dataTypes = s.dataTypes; |
|
7657 |
|
7658 // Remove auto dataType and get content-type in the process |
|
7659 while ( dataTypes[ 0 ] === "*" ) { |
|
7660 dataTypes.shift(); |
|
7661 if ( ct === undefined ) { |
|
7662 ct = s.mimeType || jqXHR.getResponseHeader("Content-Type"); |
|
7663 } |
|
7664 } |
|
7665 |
|
7666 // Check if we're dealing with a known content-type |
|
7667 if ( ct ) { |
|
7668 for ( type in contents ) { |
|
7669 if ( contents[ type ] && contents[ type ].test( ct ) ) { |
|
7670 dataTypes.unshift( type ); |
|
7671 break; |
|
7672 } |
|
7673 } |
|
7674 } |
|
7675 |
|
7676 // Check to see if we have a response for the expected dataType |
|
7677 if ( dataTypes[ 0 ] in responses ) { |
|
7678 finalDataType = dataTypes[ 0 ]; |
|
7679 } else { |
|
7680 // Try convertible dataTypes |
|
7681 for ( type in responses ) { |
|
7682 if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { |
|
7683 finalDataType = type; |
|
7684 break; |
|
7685 } |
|
7686 if ( !firstDataType ) { |
|
7687 firstDataType = type; |
|
7688 } |
|
7689 } |
|
7690 // Or just use first one |
|
7691 finalDataType = finalDataType || firstDataType; |
|
7692 } |
|
7693 |
|
7694 // If we found a dataType |
|
7695 // We add the dataType to the list if needed |
|
7696 // and return the corresponding response |
|
7697 if ( finalDataType ) { |
|
7698 if ( finalDataType !== dataTypes[ 0 ] ) { |
|
7699 dataTypes.unshift( finalDataType ); |
|
7700 } |
|
7701 return responses[ finalDataType ]; |
|
7702 } |
|
7703 } |
|
7704 |
|
7705 /* Chain conversions given the request and the original response |
|
7706 * Also sets the responseXXX fields on the jqXHR instance |
|
7707 */ |
|
7708 function ajaxConvert( s, response, jqXHR, isSuccess ) { |
|
7709 var conv2, current, conv, tmp, prev, |
|
7710 converters = {}, |
|
7711 // Work with a copy of dataTypes in case we need to modify it for conversion |
|
7712 dataTypes = s.dataTypes.slice(); |
|
7713 |
|
7714 // Create converters map with lowercased keys |
|
7715 if ( dataTypes[ 1 ] ) { |
|
7716 for ( conv in s.converters ) { |
|
7717 converters[ conv.toLowerCase() ] = s.converters[ conv ]; |
|
7718 } |
|
7719 } |
|
7720 |
|
7721 current = dataTypes.shift(); |
|
7722 |
|
7723 // Convert to each sequential dataType |
|
7724 while ( current ) { |
|
7725 |
|
7726 if ( s.responseFields[ current ] ) { |
|
7727 jqXHR[ s.responseFields[ current ] ] = response; |
|
7728 } |
|
7729 |
|
7730 // Apply the dataFilter if provided |
|
7731 if ( !prev && isSuccess && s.dataFilter ) { |
|
7732 response = s.dataFilter( response, s.dataType ); |
|
7733 } |
|
7734 |
|
7735 prev = current; |
|
7736 current = dataTypes.shift(); |
|
7737 |
|
7738 if ( current ) { |
|
7739 |
|
7740 // There's only work to do if current dataType is non-auto |
|
7741 if ( current === "*" ) { |
|
7742 |
|
7743 current = prev; |
|
7744 |
|
7745 // Convert response if prev dataType is non-auto and differs from current |
|
7746 } else if ( prev !== "*" && prev !== current ) { |
|
7747 |
|
7748 // Seek a direct converter |
|
7749 conv = converters[ prev + " " + current ] || converters[ "* " + current ]; |
|
7750 |
|
7751 // If none found, seek a pair |
|
7752 if ( !conv ) { |
|
7753 for ( conv2 in converters ) { |
|
7754 |
|
7755 // If conv2 outputs current |
|
7756 tmp = conv2.split( " " ); |
|
7757 if ( tmp[ 1 ] === current ) { |
|
7758 |
|
7759 // If prev can be converted to accepted input |
|
7760 conv = converters[ prev + " " + tmp[ 0 ] ] || |
|
7761 converters[ "* " + tmp[ 0 ] ]; |
|
7762 if ( conv ) { |
|
7763 // Condense equivalence converters |
|
7764 if ( conv === true ) { |
|
7765 conv = converters[ conv2 ]; |
|
7766 |
|
7767 // Otherwise, insert the intermediate dataType |
|
7768 } else if ( converters[ conv2 ] !== true ) { |
|
7769 current = tmp[ 0 ]; |
|
7770 dataTypes.unshift( tmp[ 1 ] ); |
|
7771 } |
|
7772 break; |
|
7773 } |
|
7774 } |
|
7775 } |
|
7776 } |
|
7777 |
|
7778 // Apply converter (if not an equivalence) |
|
7779 if ( conv !== true ) { |
|
7780 |
|
7781 // Unless errors are allowed to bubble, catch and return them |
|
7782 if ( conv && s[ "throws" ] ) { |
|
7783 response = conv( response ); |
|
7784 } else { |
|
7785 try { |
|
7786 response = conv( response ); |
|
7787 } catch ( e ) { |
|
7788 return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current }; |
|
7789 } |
|
7790 } |
|
7791 } |
|
7792 } |
|
7793 } |
|
7794 } |
|
7795 |
|
7796 return { state: "success", data: response }; |
|
7797 } |
|
7798 |
|
7799 jQuery.extend({ |
|
7800 |
|
7801 // Counter for holding the number of active queries |
|
7802 active: 0, |
|
7803 |
|
7804 // Last-Modified header cache for next request |
|
7805 lastModified: {}, |
|
7806 etag: {}, |
|
7807 |
|
7808 ajaxSettings: { |
|
7809 url: ajaxLocation, |
|
7810 type: "GET", |
|
7811 isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), |
|
7812 global: true, |
|
7813 processData: true, |
|
7814 async: true, |
|
7815 contentType: "application/x-www-form-urlencoded; charset=UTF-8", |
|
7816 /* |
|
7817 timeout: 0, |
|
7818 data: null, |
|
7819 dataType: null, |
|
7820 username: null, |
|
7821 password: null, |
|
7822 cache: null, |
|
7823 throws: false, |
|
7824 traditional: false, |
|
7825 headers: {}, |
|
7826 */ |
|
7827 |
|
7828 accepts: { |
|
7829 "*": allTypes, |
|
7830 text: "text/plain", |
|
7831 html: "text/html", |
|
7832 xml: "application/xml, text/xml", |
|
7833 json: "application/json, text/javascript" |
|
7834 }, |
|
7835 |
|
7836 contents: { |
|
7837 xml: /xml/, |
|
7838 html: /html/, |
|
7839 json: /json/ |
|
7840 }, |
|
7841 |
|
7842 responseFields: { |
|
7843 xml: "responseXML", |
|
7844 text: "responseText", |
|
7845 json: "responseJSON" |
|
7846 }, |
|
7847 |
|
7848 // Data converters |
|
7849 // Keys separate source (or catchall "*") and destination types with a single space |
|
7850 converters: { |
|
7851 |
|
7852 // Convert anything to text |
|
7853 "* text": String, |
|
7854 |
|
7855 // Text to html (true = no transformation) |
|
7856 "text html": true, |
|
7857 |
|
7858 // Evaluate text as a json expression |
|
7859 "text json": jQuery.parseJSON, |
|
7860 |
|
7861 // Parse text as xml |
|
7862 "text xml": jQuery.parseXML |
|
7863 }, |
|
7864 |
|
7865 // For options that shouldn't be deep extended: |
|
7866 // you can add your own custom options here if |
|
7867 // and when you create one that shouldn't be |
|
7868 // deep extended (see ajaxExtend) |
|
7869 flatOptions: { |
|
7870 url: true, |
|
7871 context: true |
|
7872 } |
|
7873 }, |
|
7874 |
|
7875 // Creates a full fledged settings object into target |
|
7876 // with both ajaxSettings and settings fields. |
|
7877 // If target is omitted, writes into ajaxSettings. |
|
7878 ajaxSetup: function( target, settings ) { |
|
7879 return settings ? |
|
7880 |
|
7881 // Building a settings object |
|
7882 ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : |
|
7883 |
|
7884 // Extending ajaxSettings |
|
7885 ajaxExtend( jQuery.ajaxSettings, target ); |
|
7886 }, |
|
7887 |
|
7888 ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), |
|
7889 ajaxTransport: addToPrefiltersOrTransports( transports ), |
|
7890 |
|
7891 // Main method |
|
7892 ajax: function( url, options ) { |
|
7893 |
|
7894 // If url is an object, simulate pre-1.5 signature |
|
7895 if ( typeof url === "object" ) { |
|
7896 options = url; |
|
7897 url = undefined; |
|
7898 } |
|
7899 |
|
7900 // Force options to be an object |
|
7901 options = options || {}; |
|
7902 |
|
7903 var transport, |
|
7904 // URL without anti-cache param |
|
7905 cacheURL, |
|
7906 // Response headers |
|
7907 responseHeadersString, |
|
7908 responseHeaders, |
|
7909 // timeout handle |
|
7910 timeoutTimer, |
|
7911 // Cross-domain detection vars |
|
7912 parts, |
|
7913 // To know if global events are to be dispatched |
|
7914 fireGlobals, |
|
7915 // Loop variable |
|
7916 i, |
|
7917 // Create the final options object |
|
7918 s = jQuery.ajaxSetup( {}, options ), |
|
7919 // Callbacks context |
|
7920 callbackContext = s.context || s, |
|
7921 // Context for global events is callbackContext if it is a DOM node or jQuery collection |
|
7922 globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ? |
|
7923 jQuery( callbackContext ) : |
|
7924 jQuery.event, |
|
7925 // Deferreds |
|
7926 deferred = jQuery.Deferred(), |
|
7927 completeDeferred = jQuery.Callbacks("once memory"), |
|
7928 // Status-dependent callbacks |
|
7929 statusCode = s.statusCode || {}, |
|
7930 // Headers (they are sent all at once) |
|
7931 requestHeaders = {}, |
|
7932 requestHeadersNames = {}, |
|
7933 // The jqXHR state |
|
7934 state = 0, |
|
7935 // Default abort message |
|
7936 strAbort = "canceled", |
|
7937 // Fake xhr |
|
7938 jqXHR = { |
|
7939 readyState: 0, |
|
7940 |
|
7941 // Builds headers hashtable if needed |
|
7942 getResponseHeader: function( key ) { |
|
7943 var match; |
|
7944 if ( state === 2 ) { |
|
7945 if ( !responseHeaders ) { |
|
7946 responseHeaders = {}; |
|
7947 while ( (match = rheaders.exec( responseHeadersString )) ) { |
|
7948 responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; |
|
7949 } |
|
7950 } |
|
7951 match = responseHeaders[ key.toLowerCase() ]; |
|
7952 } |
|
7953 return match == null ? null : match; |
|
7954 }, |
|
7955 |
|
7956 // Raw string |
|
7957 getAllResponseHeaders: function() { |
|
7958 return state === 2 ? responseHeadersString : null; |
|
7959 }, |
|
7960 |
|
7961 // Caches the header |
|
7962 setRequestHeader: function( name, value ) { |
|
7963 var lname = name.toLowerCase(); |
|
7964 if ( !state ) { |
|
7965 name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; |
|
7966 requestHeaders[ name ] = value; |
|
7967 } |
|
7968 return this; |
|
7969 }, |
|
7970 |
|
7971 // Overrides response content-type header |
|
7972 overrideMimeType: function( type ) { |
|
7973 if ( !state ) { |
|
7974 s.mimeType = type; |
|
7975 } |
|
7976 return this; |
|
7977 }, |
|
7978 |
|
7979 // Status-dependent callbacks |
|
7980 statusCode: function( map ) { |
|
7981 var code; |
|
7982 if ( map ) { |
|
7983 if ( state < 2 ) { |
|
7984 for ( code in map ) { |
|
7985 // Lazy-add the new callback in a way that preserves old ones |
|
7986 statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; |
|
7987 } |
|
7988 } else { |
|
7989 // Execute the appropriate callbacks |
|
7990 jqXHR.always( map[ jqXHR.status ] ); |
|
7991 } |
|
7992 } |
|
7993 return this; |
|
7994 }, |
|
7995 |
|
7996 // Cancel the request |
|
7997 abort: function( statusText ) { |
|
7998 var finalText = statusText || strAbort; |
|
7999 if ( transport ) { |
|
8000 transport.abort( finalText ); |
|
8001 } |
|
8002 done( 0, finalText ); |
|
8003 return this; |
|
8004 } |
|
8005 }; |
|
8006 |
|
8007 // Attach deferreds |
|
8008 deferred.promise( jqXHR ).complete = completeDeferred.add; |
|
8009 jqXHR.success = jqXHR.done; |
|
8010 jqXHR.error = jqXHR.fail; |
|
8011 |
|
8012 // Remove hash character (#7531: and string promotion) |
|
8013 // Add protocol if not provided (prefilters might expect it) |
|
8014 // Handle falsy url in the settings object (#10093: consistency with old signature) |
|
8015 // We also use the url parameter if available |
|
8016 s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ) |
|
8017 .replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); |
|
8018 |
|
8019 // Alias method option to type as per ticket #12004 |
|
8020 s.type = options.method || options.type || s.method || s.type; |
|
8021 |
|
8022 // Extract dataTypes list |
|
8023 s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ]; |
|
8024 |
|
8025 // A cross-domain request is in order when we have a protocol:host:port mismatch |
|
8026 if ( s.crossDomain == null ) { |
|
8027 parts = rurl.exec( s.url.toLowerCase() ); |
|
8028 s.crossDomain = !!( parts && |
|
8029 ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] || |
|
8030 ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? "80" : "443" ) ) !== |
|
8031 ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? "80" : "443" ) ) ) |
|
8032 ); |
|
8033 } |
|
8034 |
|
8035 // Convert data if not already a string |
|
8036 if ( s.data && s.processData && typeof s.data !== "string" ) { |
|
8037 s.data = jQuery.param( s.data, s.traditional ); |
|
8038 } |
|
8039 |
|
8040 // Apply prefilters |
|
8041 inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); |
|
8042 |
|
8043 // If request was aborted inside a prefilter, stop there |
|
8044 if ( state === 2 ) { |
|
8045 return jqXHR; |
|
8046 } |
|
8047 |
|
8048 // We can fire global events as of now if asked to |
|
8049 fireGlobals = s.global; |
|
8050 |
|
8051 // Watch for a new set of requests |
|
8052 if ( fireGlobals && jQuery.active++ === 0 ) { |
|
8053 jQuery.event.trigger("ajaxStart"); |
|
8054 } |
|
8055 |
|
8056 // Uppercase the type |
|
8057 s.type = s.type.toUpperCase(); |
|
8058 |
|
8059 // Determine if request has content |
|
8060 s.hasContent = !rnoContent.test( s.type ); |
|
8061 |
|
8062 // Save the URL in case we're toying with the If-Modified-Since |
|
8063 // and/or If-None-Match header later on |
|
8064 cacheURL = s.url; |
|
8065 |
|
8066 // More options handling for requests with no content |
|
8067 if ( !s.hasContent ) { |
|
8068 |
|
8069 // If data is available, append data to url |
|
8070 if ( s.data ) { |
|
8071 cacheURL = ( s.url += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data ); |
|
8072 // #9682: remove data so that it's not used in an eventual retry |
|
8073 delete s.data; |
|
8074 } |
|
8075 |
|
8076 // Add anti-cache in url if needed |
|
8077 if ( s.cache === false ) { |
|
8078 s.url = rts.test( cacheURL ) ? |
|
8079 |
|
8080 // If there is already a '_' parameter, set its value |
|
8081 cacheURL.replace( rts, "$1_=" + nonce++ ) : |
|
8082 |
|
8083 // Otherwise add one to the end |
|
8084 cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++; |
|
8085 } |
|
8086 } |
|
8087 |
|
8088 // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. |
|
8089 if ( s.ifModified ) { |
|
8090 if ( jQuery.lastModified[ cacheURL ] ) { |
|
8091 jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); |
|
8092 } |
|
8093 if ( jQuery.etag[ cacheURL ] ) { |
|
8094 jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); |
|
8095 } |
|
8096 } |
|
8097 |
|
8098 // Set the correct header, if data is being sent |
|
8099 if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { |
|
8100 jqXHR.setRequestHeader( "Content-Type", s.contentType ); |
|
8101 } |
|
8102 |
|
8103 // Set the Accepts header for the server, depending on the dataType |
|
8104 jqXHR.setRequestHeader( |
|
8105 "Accept", |
|
8106 s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? |
|
8107 s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : |
|
8108 s.accepts[ "*" ] |
|
8109 ); |
|
8110 |
|
8111 // Check for headers option |
|
8112 for ( i in s.headers ) { |
|
8113 jqXHR.setRequestHeader( i, s.headers[ i ] ); |
|
8114 } |
|
8115 |
|
8116 // Allow custom headers/mimetypes and early abort |
|
8117 if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { |
|
8118 // Abort if not done already and return |
|
8119 return jqXHR.abort(); |
|
8120 } |
|
8121 |
|
8122 // aborting is no longer a cancellation |
|
8123 strAbort = "abort"; |
|
8124 |
|
8125 // Install callbacks on deferreds |
|
8126 for ( i in { success: 1, error: 1, complete: 1 } ) { |
|
8127 jqXHR[ i ]( s[ i ] ); |
|
8128 } |
|
8129 |
|
8130 // Get transport |
|
8131 transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); |
|
8132 |
|
8133 // If no transport, we auto-abort |
|
8134 if ( !transport ) { |
|
8135 done( -1, "No Transport" ); |
|
8136 } else { |
|
8137 jqXHR.readyState = 1; |
|
8138 |
|
8139 // Send global event |
|
8140 if ( fireGlobals ) { |
|
8141 globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); |
|
8142 } |
|
8143 // Timeout |
|
8144 if ( s.async && s.timeout > 0 ) { |
|
8145 timeoutTimer = setTimeout(function() { |
|
8146 jqXHR.abort("timeout"); |
|
8147 }, s.timeout ); |
|
8148 } |
|
8149 |
|
8150 try { |
|
8151 state = 1; |
|
8152 transport.send( requestHeaders, done ); |
|
8153 } catch ( e ) { |
|
8154 // Propagate exception as error if not done |
|
8155 if ( state < 2 ) { |
|
8156 done( -1, e ); |
|
8157 // Simply rethrow otherwise |
|
8158 } else { |
|
8159 throw e; |
|
8160 } |
|
8161 } |
|
8162 } |
|
8163 |
|
8164 // Callback for when everything is done |
|
8165 function done( status, nativeStatusText, responses, headers ) { |
|
8166 var isSuccess, success, error, response, modified, |
|
8167 statusText = nativeStatusText; |
|
8168 |
|
8169 // Called once |
|
8170 if ( state === 2 ) { |
|
8171 return; |
|
8172 } |
|
8173 |
|
8174 // State is "done" now |
|
8175 state = 2; |
|
8176 |
|
8177 // Clear timeout if it exists |
|
8178 if ( timeoutTimer ) { |
|
8179 clearTimeout( timeoutTimer ); |
|
8180 } |
|
8181 |
|
8182 // Dereference transport for early garbage collection |
|
8183 // (no matter how long the jqXHR object will be used) |
|
8184 transport = undefined; |
|
8185 |
|
8186 // Cache response headers |
|
8187 responseHeadersString = headers || ""; |
|
8188 |
|
8189 // Set readyState |
|
8190 jqXHR.readyState = status > 0 ? 4 : 0; |
|
8191 |
|
8192 // Determine if successful |
|
8193 isSuccess = status >= 200 && status < 300 || status === 304; |
|
8194 |
|
8195 // Get response data |
|
8196 if ( responses ) { |
|
8197 response = ajaxHandleResponses( s, jqXHR, responses ); |
|
8198 } |
|
8199 |
|
8200 // Convert no matter what (that way responseXXX fields are always set) |
|
8201 response = ajaxConvert( s, response, jqXHR, isSuccess ); |
|
8202 |
|
8203 // If successful, handle type chaining |
|
8204 if ( isSuccess ) { |
|
8205 |
|
8206 // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. |
|
8207 if ( s.ifModified ) { |
|
8208 modified = jqXHR.getResponseHeader("Last-Modified"); |
|
8209 if ( modified ) { |
|
8210 jQuery.lastModified[ cacheURL ] = modified; |
|
8211 } |
|
8212 modified = jqXHR.getResponseHeader("etag"); |
|
8213 if ( modified ) { |
|
8214 jQuery.etag[ cacheURL ] = modified; |
|
8215 } |
|
8216 } |
|
8217 |
|
8218 // if no content |
|
8219 if ( status === 204 || s.type === "HEAD" ) { |
|
8220 statusText = "nocontent"; |
|
8221 |
|
8222 // if not modified |
|
8223 } else if ( status === 304 ) { |
|
8224 statusText = "notmodified"; |
|
8225 |
|
8226 // If we have data, let's convert it |
|
8227 } else { |
|
8228 statusText = response.state; |
|
8229 success = response.data; |
|
8230 error = response.error; |
|
8231 isSuccess = !error; |
|
8232 } |
|
8233 } else { |
|
8234 // We extract error from statusText |
|
8235 // then normalize statusText and status for non-aborts |
|
8236 error = statusText; |
|
8237 if ( status || !statusText ) { |
|
8238 statusText = "error"; |
|
8239 if ( status < 0 ) { |
|
8240 status = 0; |
|
8241 } |
|
8242 } |
|
8243 } |
|
8244 |
|
8245 // Set data for the fake xhr object |
|
8246 jqXHR.status = status; |
|
8247 jqXHR.statusText = ( nativeStatusText || statusText ) + ""; |
|
8248 |
|
8249 // Success/Error |
|
8250 if ( isSuccess ) { |
|
8251 deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); |
|
8252 } else { |
|
8253 deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); |
|
8254 } |
|
8255 |
|
8256 // Status-dependent callbacks |
|
8257 jqXHR.statusCode( statusCode ); |
|
8258 statusCode = undefined; |
|
8259 |
|
8260 if ( fireGlobals ) { |
|
8261 globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", |
|
8262 [ jqXHR, s, isSuccess ? success : error ] ); |
|
8263 } |
|
8264 |
|
8265 // Complete |
|
8266 completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); |
|
8267 |
|
8268 if ( fireGlobals ) { |
|
8269 globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); |
|
8270 // Handle the global AJAX counter |
|
8271 if ( !( --jQuery.active ) ) { |
|
8272 jQuery.event.trigger("ajaxStop"); |
|
8273 } |
|
8274 } |
|
8275 } |
|
8276 |
|
8277 return jqXHR; |
|
8278 }, |
|
8279 |
|
8280 getJSON: function( url, data, callback ) { |
|
8281 return jQuery.get( url, data, callback, "json" ); |
|
8282 }, |
|
8283 |
|
8284 getScript: function( url, callback ) { |
|
8285 return jQuery.get( url, undefined, callback, "script" ); |
|
8286 } |
|
8287 }); |
|
8288 |
|
8289 jQuery.each( [ "get", "post" ], function( i, method ) { |
|
8290 jQuery[ method ] = function( url, data, callback, type ) { |
|
8291 // shift arguments if data argument was omitted |
|
8292 if ( jQuery.isFunction( data ) ) { |
|
8293 type = type || callback; |
|
8294 callback = data; |
|
8295 data = undefined; |
|
8296 } |
|
8297 |
|
8298 return jQuery.ajax({ |
|
8299 url: url, |
|
8300 type: method, |
|
8301 dataType: type, |
|
8302 data: data, |
|
8303 success: callback |
|
8304 }); |
|
8305 }; |
|
8306 }); |
|
8307 |
|
8308 // Attach a bunch of functions for handling common AJAX events |
|
8309 jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ) { |
|
8310 jQuery.fn[ type ] = function( fn ) { |
|
8311 return this.on( type, fn ); |
|
8312 }; |
|
8313 }); |
|
8314 |
|
8315 |
|
8316 jQuery._evalUrl = function( url ) { |
|
8317 return jQuery.ajax({ |
|
8318 url: url, |
|
8319 type: "GET", |
|
8320 dataType: "script", |
|
8321 async: false, |
|
8322 global: false, |
|
8323 "throws": true |
|
8324 }); |
|
8325 }; |
|
8326 |
|
8327 |
|
8328 jQuery.fn.extend({ |
|
8329 wrapAll: function( html ) { |
|
8330 var wrap; |
|
8331 |
|
8332 if ( jQuery.isFunction( html ) ) { |
|
8333 return this.each(function( i ) { |
|
8334 jQuery( this ).wrapAll( html.call(this, i) ); |
|
8335 }); |
|
8336 } |
|
8337 |
|
8338 if ( this[ 0 ] ) { |
|
8339 |
|
8340 // The elements to wrap the target around |
|
8341 wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); |
|
8342 |
|
8343 if ( this[ 0 ].parentNode ) { |
|
8344 wrap.insertBefore( this[ 0 ] ); |
|
8345 } |
|
8346 |
|
8347 wrap.map(function() { |
|
8348 var elem = this; |
|
8349 |
|
8350 while ( elem.firstElementChild ) { |
|
8351 elem = elem.firstElementChild; |
|
8352 } |
|
8353 |
|
8354 return elem; |
|
8355 }).append( this ); |
|
8356 } |
|
8357 |
|
8358 return this; |
|
8359 }, |
|
8360 |
|
8361 wrapInner: function( html ) { |
|
8362 if ( jQuery.isFunction( html ) ) { |
|
8363 return this.each(function( i ) { |
|
8364 jQuery( this ).wrapInner( html.call(this, i) ); |
|
8365 }); |
|
8366 } |
|
8367 |
|
8368 return this.each(function() { |
|
8369 var self = jQuery( this ), |
|
8370 contents = self.contents(); |
|
8371 |
|
8372 if ( contents.length ) { |
|
8373 contents.wrapAll( html ); |
|
8374 |
|
8375 } else { |
|
8376 self.append( html ); |
|
8377 } |
|
8378 }); |
|
8379 }, |
|
8380 |
|
8381 wrap: function( html ) { |
|
8382 var isFunction = jQuery.isFunction( html ); |
|
8383 |
|
8384 return this.each(function( i ) { |
|
8385 jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); |
|
8386 }); |
|
8387 }, |
|
8388 |
|
8389 unwrap: function() { |
|
8390 return this.parent().each(function() { |
|
8391 if ( !jQuery.nodeName( this, "body" ) ) { |
|
8392 jQuery( this ).replaceWith( this.childNodes ); |
|
8393 } |
|
8394 }).end(); |
|
8395 } |
|
8396 }); |
|
8397 |
|
8398 |
|
8399 jQuery.expr.filters.hidden = function( elem ) { |
|
8400 // Support: Opera <= 12.12 |
|
8401 // Opera reports offsetWidths and offsetHeights less than zero on some elements |
|
8402 return elem.offsetWidth <= 0 && elem.offsetHeight <= 0; |
|
8403 }; |
|
8404 jQuery.expr.filters.visible = function( elem ) { |
|
8405 return !jQuery.expr.filters.hidden( elem ); |
|
8406 }; |
|
8407 |
|
8408 |
|
8409 |
|
8410 |
|
8411 var r20 = /%20/g, |
|
8412 rbracket = /\[\]$/, |
|
8413 rCRLF = /\r?\n/g, |
|
8414 rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, |
|
8415 rsubmittable = /^(?:input|select|textarea|keygen)/i; |
|
8416 |
|
8417 function buildParams( prefix, obj, traditional, add ) { |
|
8418 var name; |
|
8419 |
|
8420 if ( jQuery.isArray( obj ) ) { |
|
8421 // Serialize array item. |
|
8422 jQuery.each( obj, function( i, v ) { |
|
8423 if ( traditional || rbracket.test( prefix ) ) { |
|
8424 // Treat each array item as a scalar. |
|
8425 add( prefix, v ); |
|
8426 |
|
8427 } else { |
|
8428 // Item is non-scalar (array or object), encode its numeric index. |
|
8429 buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add ); |
|
8430 } |
|
8431 }); |
|
8432 |
|
8433 } else if ( !traditional && jQuery.type( obj ) === "object" ) { |
|
8434 // Serialize object item. |
|
8435 for ( name in obj ) { |
|
8436 buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); |
|
8437 } |
|
8438 |
|
8439 } else { |
|
8440 // Serialize scalar item. |
|
8441 add( prefix, obj ); |
|
8442 } |
|
8443 } |
|
8444 |
|
8445 // Serialize an array of form elements or a set of |
|
8446 // key/values into a query string |
|
8447 jQuery.param = function( a, traditional ) { |
|
8448 var prefix, |
|
8449 s = [], |
|
8450 add = function( key, value ) { |
|
8451 // If value is a function, invoke it and return its value |
|
8452 value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value ); |
|
8453 s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); |
|
8454 }; |
|
8455 |
|
8456 // Set traditional to true for jQuery <= 1.3.2 behavior. |
|
8457 if ( traditional === undefined ) { |
|
8458 traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional; |
|
8459 } |
|
8460 |
|
8461 // If an array was passed in, assume that it is an array of form elements. |
|
8462 if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { |
|
8463 // Serialize the form elements |
|
8464 jQuery.each( a, function() { |
|
8465 add( this.name, this.value ); |
|
8466 }); |
|
8467 |
|
8468 } else { |
|
8469 // If traditional, encode the "old" way (the way 1.3.2 or older |
|
8470 // did it), otherwise encode params recursively. |
|
8471 for ( prefix in a ) { |
|
8472 buildParams( prefix, a[ prefix ], traditional, add ); |
|
8473 } |
|
8474 } |
|
8475 |
|
8476 // Return the resulting serialization |
|
8477 return s.join( "&" ).replace( r20, "+" ); |
|
8478 }; |
|
8479 |
|
8480 jQuery.fn.extend({ |
|
8481 serialize: function() { |
|
8482 return jQuery.param( this.serializeArray() ); |
|
8483 }, |
|
8484 serializeArray: function() { |
|
8485 return this.map(function() { |
|
8486 // Can add propHook for "elements" to filter or add form elements |
|
8487 var elements = jQuery.prop( this, "elements" ); |
|
8488 return elements ? jQuery.makeArray( elements ) : this; |
|
8489 }) |
|
8490 .filter(function() { |
|
8491 var type = this.type; |
|
8492 |
|
8493 // Use .is( ":disabled" ) so that fieldset[disabled] works |
|
8494 return this.name && !jQuery( this ).is( ":disabled" ) && |
|
8495 rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && |
|
8496 ( this.checked || !rcheckableType.test( type ) ); |
|
8497 }) |
|
8498 .map(function( i, elem ) { |
|
8499 var val = jQuery( this ).val(); |
|
8500 |
|
8501 return val == null ? |
|
8502 null : |
|
8503 jQuery.isArray( val ) ? |
|
8504 jQuery.map( val, function( val ) { |
|
8505 return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; |
|
8506 }) : |
|
8507 { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; |
|
8508 }).get(); |
|
8509 } |
|
8510 }); |
|
8511 |
|
8512 |
|
8513 jQuery.ajaxSettings.xhr = function() { |
|
8514 try { |
|
8515 return new XMLHttpRequest(); |
|
8516 } catch( e ) {} |
|
8517 }; |
|
8518 |
|
8519 var xhrId = 0, |
|
8520 xhrCallbacks = {}, |
|
8521 xhrSuccessStatus = { |
|
8522 // file protocol always yields status code 0, assume 200 |
|
8523 0: 200, |
|
8524 // Support: IE9 |
|
8525 // #1450: sometimes IE returns 1223 when it should be 204 |
|
8526 1223: 204 |
|
8527 }, |
|
8528 xhrSupported = jQuery.ajaxSettings.xhr(); |
|
8529 |
|
8530 // Support: IE9 |
|
8531 // Open requests must be manually aborted on unload (#5280) |
|
8532 if ( window.ActiveXObject ) { |
|
8533 jQuery( window ).on( "unload", function() { |
|
8534 for ( var key in xhrCallbacks ) { |
|
8535 xhrCallbacks[ key ](); |
|
8536 } |
|
8537 }); |
|
8538 } |
|
8539 |
|
8540 support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); |
|
8541 support.ajax = xhrSupported = !!xhrSupported; |
|
8542 |
|
8543 jQuery.ajaxTransport(function( options ) { |
|
8544 var callback; |
|
8545 |
|
8546 // Cross domain only allowed if supported through XMLHttpRequest |
|
8547 if ( support.cors || xhrSupported && !options.crossDomain ) { |
|
8548 return { |
|
8549 send: function( headers, complete ) { |
|
8550 var i, |
|
8551 xhr = options.xhr(), |
|
8552 id = ++xhrId; |
|
8553 |
|
8554 xhr.open( options.type, options.url, options.async, options.username, options.password ); |
|
8555 |
|
8556 // Apply custom fields if provided |
|
8557 if ( options.xhrFields ) { |
|
8558 for ( i in options.xhrFields ) { |
|
8559 xhr[ i ] = options.xhrFields[ i ]; |
|
8560 } |
|
8561 } |
|
8562 |
|
8563 // Override mime type if needed |
|
8564 if ( options.mimeType && xhr.overrideMimeType ) { |
|
8565 xhr.overrideMimeType( options.mimeType ); |
|
8566 } |
|
8567 |
|
8568 // X-Requested-With header |
|
8569 // For cross-domain requests, seeing as conditions for a preflight are |
|
8570 // akin to a jigsaw puzzle, we simply never set it to be sure. |
|
8571 // (it can always be set on a per-request basis or even using ajaxSetup) |
|
8572 // For same-domain requests, won't change header if already provided. |
|
8573 if ( !options.crossDomain && !headers["X-Requested-With"] ) { |
|
8574 headers["X-Requested-With"] = "XMLHttpRequest"; |
|
8575 } |
|
8576 |
|
8577 // Set headers |
|
8578 for ( i in headers ) { |
|
8579 xhr.setRequestHeader( i, headers[ i ] ); |
|
8580 } |
|
8581 |
|
8582 // Callback |
|
8583 callback = function( type ) { |
|
8584 return function() { |
|
8585 if ( callback ) { |
|
8586 delete xhrCallbacks[ id ]; |
|
8587 callback = xhr.onload = xhr.onerror = null; |
|
8588 |
|
8589 if ( type === "abort" ) { |
|
8590 xhr.abort(); |
|
8591 } else if ( type === "error" ) { |
|
8592 complete( |
|
8593 // file: protocol always yields status 0; see #8605, #14207 |
|
8594 xhr.status, |
|
8595 xhr.statusText |
|
8596 ); |
|
8597 } else { |
|
8598 complete( |
|
8599 xhrSuccessStatus[ xhr.status ] || xhr.status, |
|
8600 xhr.statusText, |
|
8601 // Support: IE9 |
|
8602 // Accessing binary-data responseText throws an exception |
|
8603 // (#11426) |
|
8604 typeof xhr.responseText === "string" ? { |
|
8605 text: xhr.responseText |
|
8606 } : undefined, |
|
8607 xhr.getAllResponseHeaders() |
|
8608 ); |
|
8609 } |
|
8610 } |
|
8611 }; |
|
8612 }; |
|
8613 |
|
8614 // Listen to events |
|
8615 xhr.onload = callback(); |
|
8616 xhr.onerror = callback("error"); |
|
8617 |
|
8618 // Create the abort callback |
|
8619 callback = xhrCallbacks[ id ] = callback("abort"); |
|
8620 |
|
8621 try { |
|
8622 // Do send the request (this may raise an exception) |
|
8623 xhr.send( options.hasContent && options.data || null ); |
|
8624 } catch ( e ) { |
|
8625 // #14683: Only rethrow if this hasn't been notified as an error yet |
|
8626 if ( callback ) { |
|
8627 throw e; |
|
8628 } |
|
8629 } |
|
8630 }, |
|
8631 |
|
8632 abort: function() { |
|
8633 if ( callback ) { |
|
8634 callback(); |
|
8635 } |
|
8636 } |
|
8637 }; |
|
8638 } |
|
8639 }); |
|
8640 |
|
8641 |
|
8642 |
|
8643 |
|
8644 // Install script dataType |
|
8645 jQuery.ajaxSetup({ |
|
8646 accepts: { |
|
8647 script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" |
|
8648 }, |
|
8649 contents: { |
|
8650 script: /(?:java|ecma)script/ |
|
8651 }, |
|
8652 converters: { |
|
8653 "text script": function( text ) { |
|
8654 jQuery.globalEval( text ); |
|
8655 return text; |
|
8656 } |
|
8657 } |
|
8658 }); |
|
8659 |
|
8660 // Handle cache's special case and crossDomain |
|
8661 jQuery.ajaxPrefilter( "script", function( s ) { |
|
8662 if ( s.cache === undefined ) { |
|
8663 s.cache = false; |
|
8664 } |
|
8665 if ( s.crossDomain ) { |
|
8666 s.type = "GET"; |
|
8667 } |
|
8668 }); |
|
8669 |
|
8670 // Bind script tag hack transport |
|
8671 jQuery.ajaxTransport( "script", function( s ) { |
|
8672 // This transport only deals with cross domain requests |
|
8673 if ( s.crossDomain ) { |
|
8674 var script, callback; |
|
8675 return { |
|
8676 send: function( _, complete ) { |
|
8677 script = jQuery("<script>").prop({ |
|
8678 async: true, |
|
8679 charset: s.scriptCharset, |
|
8680 src: s.url |
|
8681 }).on( |
|
8682 "load error", |
|
8683 callback = function( evt ) { |
|
8684 script.remove(); |
|
8685 callback = null; |
|
8686 if ( evt ) { |
|
8687 complete( evt.type === "error" ? 404 : 200, evt.type ); |
|
8688 } |
|
8689 } |
|
8690 ); |
|
8691 document.head.appendChild( script[ 0 ] ); |
|
8692 }, |
|
8693 abort: function() { |
|
8694 if ( callback ) { |
|
8695 callback(); |
|
8696 } |
|
8697 } |
|
8698 }; |
|
8699 } |
|
8700 }); |
|
8701 |
|
8702 |
|
8703 |
|
8704 |
|
8705 var oldCallbacks = [], |
|
8706 rjsonp = /(=)\?(?=&|$)|\?\?/; |
|
8707 |
|
8708 // Default jsonp settings |
|
8709 jQuery.ajaxSetup({ |
|
8710 jsonp: "callback", |
|
8711 jsonpCallback: function() { |
|
8712 var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) ); |
|
8713 this[ callback ] = true; |
|
8714 return callback; |
|
8715 } |
|
8716 }); |
|
8717 |
|
8718 // Detect, normalize options and install callbacks for jsonp requests |
|
8719 jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { |
|
8720 |
|
8721 var callbackName, overwritten, responseContainer, |
|
8722 jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ? |
|
8723 "url" : |
|
8724 typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data" |
|
8725 ); |
|
8726 |
|
8727 // Handle iff the expected data type is "jsonp" or we have a parameter to set |
|
8728 if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) { |
|
8729 |
|
8730 // Get callback name, remembering preexisting value associated with it |
|
8731 callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ? |
|
8732 s.jsonpCallback() : |
|
8733 s.jsonpCallback; |
|
8734 |
|
8735 // Insert callback into url or form data |
|
8736 if ( jsonProp ) { |
|
8737 s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName ); |
|
8738 } else if ( s.jsonp !== false ) { |
|
8739 s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; |
|
8740 } |
|
8741 |
|
8742 // Use data converter to retrieve json after script execution |
|
8743 s.converters["script json"] = function() { |
|
8744 if ( !responseContainer ) { |
|
8745 jQuery.error( callbackName + " was not called" ); |
|
8746 } |
|
8747 return responseContainer[ 0 ]; |
|
8748 }; |
|
8749 |
|
8750 // force json dataType |
|
8751 s.dataTypes[ 0 ] = "json"; |
|
8752 |
|
8753 // Install callback |
|
8754 overwritten = window[ callbackName ]; |
|
8755 window[ callbackName ] = function() { |
|
8756 responseContainer = arguments; |
|
8757 }; |
|
8758 |
|
8759 // Clean-up function (fires after converters) |
|
8760 jqXHR.always(function() { |
|
8761 // Restore preexisting value |
|
8762 window[ callbackName ] = overwritten; |
|
8763 |
|
8764 // Save back as free |
|
8765 if ( s[ callbackName ] ) { |
|
8766 // make sure that re-using the options doesn't screw things around |
|
8767 s.jsonpCallback = originalSettings.jsonpCallback; |
|
8768 |
|
8769 // save the callback name for future use |
|
8770 oldCallbacks.push( callbackName ); |
|
8771 } |
|
8772 |
|
8773 // Call if it was a function and we have a response |
|
8774 if ( responseContainer && jQuery.isFunction( overwritten ) ) { |
|
8775 overwritten( responseContainer[ 0 ] ); |
|
8776 } |
|
8777 |
|
8778 responseContainer = overwritten = undefined; |
|
8779 }); |
|
8780 |
|
8781 // Delegate to script |
|
8782 return "script"; |
|
8783 } |
|
8784 }); |
|
8785 |
|
8786 |
|
8787 |
|
8788 |
|
8789 // data: string of html |
|
8790 // context (optional): If specified, the fragment will be created in this context, defaults to document |
|
8791 // keepScripts (optional): If true, will include scripts passed in the html string |
|
8792 jQuery.parseHTML = function( data, context, keepScripts ) { |
|
8793 if ( !data || typeof data !== "string" ) { |
|
8794 return null; |
|
8795 } |
|
8796 if ( typeof context === "boolean" ) { |
|
8797 keepScripts = context; |
|
8798 context = false; |
|
8799 } |
|
8800 context = context || document; |
|
8801 |
|
8802 var parsed = rsingleTag.exec( data ), |
|
8803 scripts = !keepScripts && []; |
|
8804 |
|
8805 // Single tag |
|
8806 if ( parsed ) { |
|
8807 return [ context.createElement( parsed[1] ) ]; |
|
8808 } |
|
8809 |
|
8810 parsed = jQuery.buildFragment( [ data ], context, scripts ); |
|
8811 |
|
8812 if ( scripts && scripts.length ) { |
|
8813 jQuery( scripts ).remove(); |
|
8814 } |
|
8815 |
|
8816 return jQuery.merge( [], parsed.childNodes ); |
|
8817 }; |
|
8818 |
|
8819 |
|
8820 // Keep a copy of the old load method |
|
8821 var _load = jQuery.fn.load; |
|
8822 |
|
8823 /** |
|
8824 * Load a url into a page |
|
8825 */ |
|
8826 jQuery.fn.load = function( url, params, callback ) { |
|
8827 if ( typeof url !== "string" && _load ) { |
|
8828 return _load.apply( this, arguments ); |
|
8829 } |
|
8830 |
|
8831 var selector, type, response, |
|
8832 self = this, |
|
8833 off = url.indexOf(" "); |
|
8834 |
|
8835 if ( off >= 0 ) { |
|
8836 selector = jQuery.trim( url.slice( off ) ); |
|
8837 url = url.slice( 0, off ); |
|
8838 } |
|
8839 |
|
8840 // If it's a function |
|
8841 if ( jQuery.isFunction( params ) ) { |
|
8842 |
|
8843 // We assume that it's the callback |
|
8844 callback = params; |
|
8845 params = undefined; |
|
8846 |
|
8847 // Otherwise, build a param string |
|
8848 } else if ( params && typeof params === "object" ) { |
|
8849 type = "POST"; |
|
8850 } |
|
8851 |
|
8852 // If we have elements to modify, make the request |
|
8853 if ( self.length > 0 ) { |
|
8854 jQuery.ajax({ |
|
8855 url: url, |
|
8856 |
|
8857 // if "type" variable is undefined, then "GET" method will be used |
|
8858 type: type, |
|
8859 dataType: "html", |
|
8860 data: params |
|
8861 }).done(function( responseText ) { |
|
8862 |
|
8863 // Save response for use in complete callback |
|
8864 response = arguments; |
|
8865 |
|
8866 self.html( selector ? |
|
8867 |
|
8868 // If a selector was specified, locate the right elements in a dummy div |
|
8869 // Exclude scripts to avoid IE 'Permission Denied' errors |
|
8870 jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) : |
|
8871 |
|
8872 // Otherwise use the full result |
|
8873 responseText ); |
|
8874 |
|
8875 }).complete( callback && function( jqXHR, status ) { |
|
8876 self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] ); |
|
8877 }); |
|
8878 } |
|
8879 |
|
8880 return this; |
|
8881 }; |
|
8882 |
|
8883 |
|
8884 |
|
8885 |
|
8886 jQuery.expr.filters.animated = function( elem ) { |
|
8887 return jQuery.grep(jQuery.timers, function( fn ) { |
|
8888 return elem === fn.elem; |
|
8889 }).length; |
|
8890 }; |
|
8891 |
|
8892 |
|
8893 |
|
8894 |
|
8895 var docElem = window.document.documentElement; |
|
8896 |
|
8897 /** |
|
8898 * Gets a window from an element |
|
8899 */ |
|
8900 function getWindow( elem ) { |
|
8901 return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 && elem.defaultView; |
|
8902 } |
|
8903 |
|
8904 jQuery.offset = { |
|
8905 setOffset: function( elem, options, i ) { |
|
8906 var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition, |
|
8907 position = jQuery.css( elem, "position" ), |
|
8908 curElem = jQuery( elem ), |
|
8909 props = {}; |
|
8910 |
|
8911 // Set position first, in-case top/left are set even on static elem |
|
8912 if ( position === "static" ) { |
|
8913 elem.style.position = "relative"; |
|
8914 } |
|
8915 |
|
8916 curOffset = curElem.offset(); |
|
8917 curCSSTop = jQuery.css( elem, "top" ); |
|
8918 curCSSLeft = jQuery.css( elem, "left" ); |
|
8919 calculatePosition = ( position === "absolute" || position === "fixed" ) && |
|
8920 ( curCSSTop + curCSSLeft ).indexOf("auto") > -1; |
|
8921 |
|
8922 // Need to be able to calculate position if either top or left is auto and position is either absolute or fixed |
|
8923 if ( calculatePosition ) { |
|
8924 curPosition = curElem.position(); |
|
8925 curTop = curPosition.top; |
|
8926 curLeft = curPosition.left; |
|
8927 |
|
8928 } else { |
|
8929 curTop = parseFloat( curCSSTop ) || 0; |
|
8930 curLeft = parseFloat( curCSSLeft ) || 0; |
|
8931 } |
|
8932 |
|
8933 if ( jQuery.isFunction( options ) ) { |
|
8934 options = options.call( elem, i, curOffset ); |
|
8935 } |
|
8936 |
|
8937 if ( options.top != null ) { |
|
8938 props.top = ( options.top - curOffset.top ) + curTop; |
|
8939 } |
|
8940 if ( options.left != null ) { |
|
8941 props.left = ( options.left - curOffset.left ) + curLeft; |
|
8942 } |
|
8943 |
|
8944 if ( "using" in options ) { |
|
8945 options.using.call( elem, props ); |
|
8946 |
|
8947 } else { |
|
8948 curElem.css( props ); |
|
8949 } |
|
8950 } |
|
8951 }; |
|
8952 |
|
8953 jQuery.fn.extend({ |
|
8954 offset: function( options ) { |
|
8955 if ( arguments.length ) { |
|
8956 return options === undefined ? |
|
8957 this : |
|
8958 this.each(function( i ) { |
|
8959 jQuery.offset.setOffset( this, options, i ); |
|
8960 }); |
|
8961 } |
|
8962 |
|
8963 var docElem, win, |
|
8964 elem = this[ 0 ], |
|
8965 box = { top: 0, left: 0 }, |
|
8966 doc = elem && elem.ownerDocument; |
|
8967 |
|
8968 if ( !doc ) { |
|
8969 return; |
|
8970 } |
|
8971 |
|
8972 docElem = doc.documentElement; |
|
8973 |
|
8974 // Make sure it's not a disconnected DOM node |
|
8975 if ( !jQuery.contains( docElem, elem ) ) { |
|
8976 return box; |
|
8977 } |
|
8978 |
|
8979 // If we don't have gBCR, just use 0,0 rather than error |
|
8980 // BlackBerry 5, iOS 3 (original iPhone) |
|
8981 if ( typeof elem.getBoundingClientRect !== strundefined ) { |
|
8982 box = elem.getBoundingClientRect(); |
|
8983 } |
|
8984 win = getWindow( doc ); |
|
8985 return { |
|
8986 top: box.top + win.pageYOffset - docElem.clientTop, |
|
8987 left: box.left + win.pageXOffset - docElem.clientLeft |
|
8988 }; |
|
8989 }, |
|
8990 |
|
8991 position: function() { |
|
8992 if ( !this[ 0 ] ) { |
|
8993 return; |
|
8994 } |
|
8995 |
|
8996 var offsetParent, offset, |
|
8997 elem = this[ 0 ], |
|
8998 parentOffset = { top: 0, left: 0 }; |
|
8999 |
|
9000 // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is its only offset parent |
|
9001 if ( jQuery.css( elem, "position" ) === "fixed" ) { |
|
9002 // We assume that getBoundingClientRect is available when computed position is fixed |
|
9003 offset = elem.getBoundingClientRect(); |
|
9004 |
|
9005 } else { |
|
9006 // Get *real* offsetParent |
|
9007 offsetParent = this.offsetParent(); |
|
9008 |
|
9009 // Get correct offsets |
|
9010 offset = this.offset(); |
|
9011 if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) { |
|
9012 parentOffset = offsetParent.offset(); |
|
9013 } |
|
9014 |
|
9015 // Add offsetParent borders |
|
9016 parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ); |
|
9017 parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true ); |
|
9018 } |
|
9019 |
|
9020 // Subtract parent offsets and element margins |
|
9021 return { |
|
9022 top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ), |
|
9023 left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true ) |
|
9024 }; |
|
9025 }, |
|
9026 |
|
9027 offsetParent: function() { |
|
9028 return this.map(function() { |
|
9029 var offsetParent = this.offsetParent || docElem; |
|
9030 |
|
9031 while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position" ) === "static" ) ) { |
|
9032 offsetParent = offsetParent.offsetParent; |
|
9033 } |
|
9034 |
|
9035 return offsetParent || docElem; |
|
9036 }); |
|
9037 } |
|
9038 }); |
|
9039 |
|
9040 // Create scrollLeft and scrollTop methods |
|
9041 jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) { |
|
9042 var top = "pageYOffset" === prop; |
|
9043 |
|
9044 jQuery.fn[ method ] = function( val ) { |
|
9045 return access( this, function( elem, method, val ) { |
|
9046 var win = getWindow( elem ); |
|
9047 |
|
9048 if ( val === undefined ) { |
|
9049 return win ? win[ prop ] : elem[ method ]; |
|
9050 } |
|
9051 |
|
9052 if ( win ) { |
|
9053 win.scrollTo( |
|
9054 !top ? val : window.pageXOffset, |
|
9055 top ? val : window.pageYOffset |
|
9056 ); |
|
9057 |
|
9058 } else { |
|
9059 elem[ method ] = val; |
|
9060 } |
|
9061 }, method, val, arguments.length, null ); |
|
9062 }; |
|
9063 }); |
|
9064 |
|
9065 // Add the top/left cssHooks using jQuery.fn.position |
|
9066 // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 |
|
9067 // getComputedStyle returns percent when specified for top/left/bottom/right |
|
9068 // rather than make the css module depend on the offset module, we just check for it here |
|
9069 jQuery.each( [ "top", "left" ], function( i, prop ) { |
|
9070 jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition, |
|
9071 function( elem, computed ) { |
|
9072 if ( computed ) { |
|
9073 computed = curCSS( elem, prop ); |
|
9074 // if curCSS returns percentage, fallback to offset |
|
9075 return rnumnonpx.test( computed ) ? |
|
9076 jQuery( elem ).position()[ prop ] + "px" : |
|
9077 computed; |
|
9078 } |
|
9079 } |
|
9080 ); |
|
9081 }); |
|
9082 |
|
9083 |
|
9084 // Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods |
|
9085 jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { |
|
9086 jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) { |
|
9087 // margin is only for outerHeight, outerWidth |
|
9088 jQuery.fn[ funcName ] = function( margin, value ) { |
|
9089 var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), |
|
9090 extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); |
|
9091 |
|
9092 return access( this, function( elem, type, value ) { |
|
9093 var doc; |
|
9094 |
|
9095 if ( jQuery.isWindow( elem ) ) { |
|
9096 // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there |
|
9097 // isn't a whole lot we can do. See pull request at this URL for discussion: |
|
9098 // https://github.com/jquery/jquery/pull/764 |
|
9099 return elem.document.documentElement[ "client" + name ]; |
|
9100 } |
|
9101 |
|
9102 // Get document width or height |
|
9103 if ( elem.nodeType === 9 ) { |
|
9104 doc = elem.documentElement; |
|
9105 |
|
9106 // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], |
|
9107 // whichever is greatest |
|
9108 return Math.max( |
|
9109 elem.body[ "scroll" + name ], doc[ "scroll" + name ], |
|
9110 elem.body[ "offset" + name ], doc[ "offset" + name ], |
|
9111 doc[ "client" + name ] |
|
9112 ); |
|
9113 } |
|
9114 |
|
9115 return value === undefined ? |
|
9116 // Get width or height on the element, requesting but not forcing parseFloat |
|
9117 jQuery.css( elem, type, extra ) : |
|
9118 |
|
9119 // Set width or height on the element |
|
9120 jQuery.style( elem, type, value, extra ); |
|
9121 }, type, chainable ? margin : undefined, chainable, null ); |
|
9122 }; |
|
9123 }); |
|
9124 }); |
|
9125 |
|
9126 |
|
9127 // The number of elements contained in the matched element set |
|
9128 jQuery.fn.size = function() { |
|
9129 return this.length; |
|
9130 }; |
|
9131 |
|
9132 jQuery.fn.andSelf = jQuery.fn.addBack; |
|
9133 |
|
9134 |
|
9135 |
|
9136 |
|
9137 // Register as a named AMD module, since jQuery can be concatenated with other |
|
9138 // files that may use define, but not via a proper concatenation script that |
|
9139 // understands anonymous AMD modules. A named AMD is safest and most robust |
|
9140 // way to register. Lowercase jquery is used because AMD module names are |
|
9141 // derived from file names, and jQuery is normally delivered in a lowercase |
|
9142 // file name. Do this after creating the global so that if an AMD module wants |
|
9143 // to call noConflict to hide this version of jQuery, it will work. |
|
9144 |
|
9145 // Note that for maximum portability, libraries that are not jQuery should |
|
9146 // declare themselves as anonymous modules, and avoid setting a global if an |
|
9147 // AMD loader is present. jQuery is a special case. For more information, see |
|
9148 // https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon |
|
9149 |
|
9150 if ( typeof define === "function" && define.amd ) { |
|
9151 define( "jquery", [], function() { |
|
9152 return jQuery; |
|
9153 }); |
|
9154 } |
|
9155 |
|
9156 |
|
9157 |
|
9158 |
|
9159 var |
|
9160 // Map over jQuery in case of overwrite |
|
9161 _jQuery = window.jQuery, |
|
9162 |
|
9163 // Map over the $ in case of overwrite |
|
9164 _$ = window.$; |
|
9165 |
|
9166 jQuery.noConflict = function( deep ) { |
|
9167 if ( window.$ === jQuery ) { |
|
9168 window.$ = _$; |
|
9169 } |
|
9170 |
|
9171 if ( deep && window.jQuery === jQuery ) { |
|
9172 window.jQuery = _jQuery; |
|
9173 } |
|
9174 |
|
9175 return jQuery; |
|
9176 }; |
|
9177 |
|
9178 // Expose jQuery and $ identifiers, even in |
|
9179 // AMD (#7102#comment:10, https://github.com/jquery/jquery/pull/557) |
|
9180 // and CommonJS for browser emulators (#13566) |
|
9181 if ( typeof noGlobal === strundefined ) { |
|
9182 window.jQuery = window.$ = jQuery; |
|
9183 } |
|
9184 |
|
9185 |
|
9186 |
|
9187 |
|
9188 return jQuery; |
|
9189 |
|
9190 })); |
|
9191 |
|
9192 /** |
|
9193 * @license AngularJS v1.3.0-rc.5 |
|
9194 * (c) 2010-2014 Google, Inc. http://angularjs.org |
|
9195 * License: MIT |
|
9196 */ |
|
9197 (function(window, document, undefined) {'use strict'; |
|
9198 |
|
9199 /** |
|
9200 * @description |
|
9201 * |
|
9202 * This object provides a utility for producing rich Error messages within |
|
9203 * Angular. It can be called as follows: |
|
9204 * |
|
9205 * var exampleMinErr = minErr('example'); |
|
9206 * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); |
|
9207 * |
|
9208 * The above creates an instance of minErr in the example namespace. The |
|
9209 * resulting error will have a namespaced error code of example.one. The |
|
9210 * resulting error will replace {0} with the value of foo, and {1} with the |
|
9211 * value of bar. The object is not restricted in the number of arguments it can |
|
9212 * take. |
|
9213 * |
|
9214 * If fewer arguments are specified than necessary for interpolation, the extra |
|
9215 * interpolation markers will be preserved in the final string. |
|
9216 * |
|
9217 * Since data will be parsed statically during a build step, some restrictions |
|
9218 * are applied with respect to how minErr instances are created and called. |
|
9219 * Instances should have names of the form namespaceMinErr for a minErr created |
|
9220 * using minErr('namespace') . Error codes, namespaces and template strings |
|
9221 * should all be static strings, not variables or general expressions. |
|
9222 * |
|
9223 * @param {string} module The namespace to use for the new minErr instance. |
|
9224 * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning |
|
9225 * error from returned function, for cases when a particular type of error is useful. |
|
9226 * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance |
|
9227 */ |
|
9228 |
|
9229 function minErr(module, ErrorConstructor) { |
|
9230 ErrorConstructor = ErrorConstructor || Error; |
|
9231 return function () { |
|
9232 var code = arguments[0], |
|
9233 prefix = '[' + (module ? module + ':' : '') + code + '] ', |
|
9234 template = arguments[1], |
|
9235 templateArgs = arguments, |
|
9236 stringify = function (obj) { |
|
9237 if (typeof obj === 'function') { |
|
9238 return obj.toString().replace(/ \{[\s\S]*$/, ''); |
|
9239 } else if (typeof obj === 'undefined') { |
|
9240 return 'undefined'; |
|
9241 } else if (typeof obj !== 'string') { |
|
9242 return JSON.stringify(obj); |
|
9243 } |
|
9244 return obj; |
|
9245 }, |
|
9246 message, i; |
|
9247 |
|
9248 message = prefix + template.replace(/\{\d+\}/g, function (match) { |
|
9249 var index = +match.slice(1, -1), arg; |
|
9250 |
|
9251 if (index + 2 < templateArgs.length) { |
|
9252 arg = templateArgs[index + 2]; |
|
9253 if (typeof arg === 'function') { |
|
9254 return arg.toString().replace(/ ?\{[\s\S]*$/, ''); |
|
9255 } else if (typeof arg === 'undefined') { |
|
9256 return 'undefined'; |
|
9257 } else if (typeof arg !== 'string') { |
|
9258 return toJson(arg); |
|
9259 } |
|
9260 return arg; |
|
9261 } |
|
9262 return match; |
|
9263 }); |
|
9264 |
|
9265 message = message + '\nhttp://errors.angularjs.org/1.3.0-rc.5/' + |
|
9266 (module ? module + '/' : '') + code; |
|
9267 for (i = 2; i < arguments.length; i++) { |
|
9268 message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + |
|
9269 encodeURIComponent(stringify(arguments[i])); |
|
9270 } |
|
9271 return new ErrorConstructor(message); |
|
9272 }; |
|
9273 } |
|
9274 |
|
9275 /* We need to tell jshint what variables are being exported */ |
|
9276 /* global angular: true, |
|
9277 msie: true, |
|
9278 jqLite: true, |
|
9279 jQuery: true, |
|
9280 slice: true, |
|
9281 splice: true, |
|
9282 push: true, |
|
9283 toString: true, |
|
9284 ngMinErr: true, |
|
9285 angularModule: true, |
|
9286 uid: true, |
|
9287 REGEX_STRING_REGEXP: true, |
|
9288 VALIDITY_STATE_PROPERTY: true, |
|
9289 |
|
9290 lowercase: true, |
|
9291 uppercase: true, |
|
9292 manualLowercase: true, |
|
9293 manualUppercase: true, |
|
9294 nodeName_: true, |
|
9295 isArrayLike: true, |
|
9296 forEach: true, |
|
9297 sortedKeys: true, |
|
9298 forEachSorted: true, |
|
9299 reverseParams: true, |
|
9300 nextUid: true, |
|
9301 setHashKey: true, |
|
9302 extend: true, |
|
9303 int: true, |
|
9304 inherit: true, |
|
9305 noop: true, |
|
9306 identity: true, |
|
9307 valueFn: true, |
|
9308 isUndefined: true, |
|
9309 isDefined: true, |
|
9310 isObject: true, |
|
9311 isString: true, |
|
9312 isNumber: true, |
|
9313 isDate: true, |
|
9314 isArray: true, |
|
9315 isFunction: true, |
|
9316 isRegExp: true, |
|
9317 isWindow: true, |
|
9318 isScope: true, |
|
9319 isFile: true, |
|
9320 isBlob: true, |
|
9321 isBoolean: true, |
|
9322 isPromiseLike: true, |
|
9323 trim: true, |
|
9324 isElement: true, |
|
9325 makeMap: true, |
|
9326 size: true, |
|
9327 includes: true, |
|
9328 arrayRemove: true, |
|
9329 isLeafNode: true, |
|
9330 copy: true, |
|
9331 shallowCopy: true, |
|
9332 equals: true, |
|
9333 csp: true, |
|
9334 concat: true, |
|
9335 sliceArgs: true, |
|
9336 bind: true, |
|
9337 toJsonReplacer: true, |
|
9338 toJson: true, |
|
9339 fromJson: true, |
|
9340 startingTag: true, |
|
9341 tryDecodeURIComponent: true, |
|
9342 parseKeyValue: true, |
|
9343 toKeyValue: true, |
|
9344 encodeUriSegment: true, |
|
9345 encodeUriQuery: true, |
|
9346 angularInit: true, |
|
9347 bootstrap: true, |
|
9348 getTestability: true, |
|
9349 snake_case: true, |
|
9350 bindJQuery: true, |
|
9351 assertArg: true, |
|
9352 assertArgFn: true, |
|
9353 assertNotHasOwnProperty: true, |
|
9354 getter: true, |
|
9355 getBlockNodes: true, |
|
9356 hasOwnProperty: true, |
|
9357 createMap: true, |
|
9358 |
|
9359 NODE_TYPE_ELEMENT: true, |
|
9360 NODE_TYPE_TEXT: true, |
|
9361 NODE_TYPE_COMMENT: true, |
|
9362 NODE_TYPE_DOCUMENT: true, |
|
9363 NODE_TYPE_DOCUMENT_FRAGMENT: true, |
|
9364 */ |
|
9365 |
|
9366 //////////////////////////////////// |
|
9367 |
|
9368 /** |
|
9369 * @ngdoc module |
|
9370 * @name ng |
|
9371 * @module ng |
|
9372 * @description |
|
9373 * |
|
9374 * # ng (core module) |
|
9375 * The ng module is loaded by default when an AngularJS application is started. The module itself |
|
9376 * contains the essential components for an AngularJS application to function. The table below |
|
9377 * lists a high level breakdown of each of the services/factories, filters, directives and testing |
|
9378 * components available within this core module. |
|
9379 * |
|
9380 * <div doc-module-components="ng"></div> |
|
9381 */ |
|
9382 |
|
9383 var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; |
|
9384 |
|
9385 // The name of a form control's ValidityState property. |
|
9386 // This is used so that it's possible for internal tests to create mock ValidityStates. |
|
9387 var VALIDITY_STATE_PROPERTY = 'validity'; |
|
9388 |
|
9389 /** |
|
9390 * @ngdoc function |
|
9391 * @name angular.lowercase |
|
9392 * @module ng |
|
9393 * @kind function |
|
9394 * |
|
9395 * @description Converts the specified string to lowercase. |
|
9396 * @param {string} string String to be converted to lowercase. |
|
9397 * @returns {string} Lowercased string. |
|
9398 */ |
|
9399 var lowercase = function(string){return isString(string) ? string.toLowerCase() : string;}; |
|
9400 var hasOwnProperty = Object.prototype.hasOwnProperty; |
|
9401 |
|
9402 /** |
|
9403 * @ngdoc function |
|
9404 * @name angular.uppercase |
|
9405 * @module ng |
|
9406 * @kind function |
|
9407 * |
|
9408 * @description Converts the specified string to uppercase. |
|
9409 * @param {string} string String to be converted to uppercase. |
|
9410 * @returns {string} Uppercased string. |
|
9411 */ |
|
9412 var uppercase = function(string){return isString(string) ? string.toUpperCase() : string;}; |
|
9413 |
|
9414 |
|
9415 var manualLowercase = function(s) { |
|
9416 /* jshint bitwise: false */ |
|
9417 return isString(s) |
|
9418 ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);}) |
|
9419 : s; |
|
9420 }; |
|
9421 var manualUppercase = function(s) { |
|
9422 /* jshint bitwise: false */ |
|
9423 return isString(s) |
|
9424 ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) |
|
9425 : s; |
|
9426 }; |
|
9427 |
|
9428 |
|
9429 // String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish |
|
9430 // locale, for this reason we need to detect this case and redefine lowercase/uppercase methods |
|
9431 // with correct but slower alternatives. |
|
9432 if ('i' !== 'I'.toLowerCase()) { |
|
9433 lowercase = manualLowercase; |
|
9434 uppercase = manualUppercase; |
|
9435 } |
|
9436 |
|
9437 |
|
9438 var /** holds major version number for IE or NaN for real browsers */ |
|
9439 msie, |
|
9440 jqLite, // delay binding since jQuery could be loaded after us. |
|
9441 jQuery, // delay binding |
|
9442 slice = [].slice, |
|
9443 splice = [].splice, |
|
9444 push = [].push, |
|
9445 toString = Object.prototype.toString, |
|
9446 ngMinErr = minErr('ng'), |
|
9447 |
|
9448 /** @name angular */ |
|
9449 angular = window.angular || (window.angular = {}), |
|
9450 angularModule, |
|
9451 uid = 0; |
|
9452 |
|
9453 /** |
|
9454 * documentMode is an IE-only property |
|
9455 * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx |
|
9456 */ |
|
9457 msie = document.documentMode; |
|
9458 |
|
9459 |
|
9460 /** |
|
9461 * @private |
|
9462 * @param {*} obj |
|
9463 * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, |
|
9464 * String ...) |
|
9465 */ |
|
9466 function isArrayLike(obj) { |
|
9467 if (obj == null || isWindow(obj)) { |
|
9468 return false; |
|
9469 } |
|
9470 |
|
9471 var length = obj.length; |
|
9472 |
|
9473 if (obj.nodeType === NODE_TYPE_ELEMENT && length) { |
|
9474 return true; |
|
9475 } |
|
9476 |
|
9477 return isString(obj) || isArray(obj) || length === 0 || |
|
9478 typeof length === 'number' && length > 0 && (length - 1) in obj; |
|
9479 } |
|
9480 |
|
9481 /** |
|
9482 * @ngdoc function |
|
9483 * @name angular.forEach |
|
9484 * @module ng |
|
9485 * @kind function |
|
9486 * |
|
9487 * @description |
|
9488 * Invokes the `iterator` function once for each item in `obj` collection, which can be either an |
|
9489 * object or an array. The `iterator` function is invoked with `iterator(value, key, obj)`, where `value` |
|
9490 * is the value of an object property or an array element, `key` is the object property key or |
|
9491 * array element index and obj is the `obj` itself. Specifying a `context` for the function is optional. |
|
9492 * |
|
9493 * It is worth noting that `.forEach` does not iterate over inherited properties because it filters |
|
9494 * using the `hasOwnProperty` method. |
|
9495 * |
|
9496 ```js |
|
9497 var values = {name: 'misko', gender: 'male'}; |
|
9498 var log = []; |
|
9499 angular.forEach(values, function(value, key) { |
|
9500 this.push(key + ': ' + value); |
|
9501 }, log); |
|
9502 expect(log).toEqual(['name: misko', 'gender: male']); |
|
9503 ``` |
|
9504 * |
|
9505 * @param {Object|Array} obj Object to iterate over. |
|
9506 * @param {Function} iterator Iterator function. |
|
9507 * @param {Object=} context Object to become context (`this`) for the iterator function. |
|
9508 * @returns {Object|Array} Reference to `obj`. |
|
9509 */ |
|
9510 |
|
9511 function forEach(obj, iterator, context) { |
|
9512 var key, length; |
|
9513 if (obj) { |
|
9514 if (isFunction(obj)) { |
|
9515 for (key in obj) { |
|
9516 // Need to check if hasOwnProperty exists, |
|
9517 // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function |
|
9518 if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { |
|
9519 iterator.call(context, obj[key], key, obj); |
|
9520 } |
|
9521 } |
|
9522 } else if (isArray(obj) || isArrayLike(obj)) { |
|
9523 var isPrimitive = typeof obj !== 'object'; |
|
9524 for (key = 0, length = obj.length; key < length; key++) { |
|
9525 if (isPrimitive || key in obj) { |
|
9526 iterator.call(context, obj[key], key, obj); |
|
9527 } |
|
9528 } |
|
9529 } else if (obj.forEach && obj.forEach !== forEach) { |
|
9530 obj.forEach(iterator, context, obj); |
|
9531 } else { |
|
9532 for (key in obj) { |
|
9533 if (obj.hasOwnProperty(key)) { |
|
9534 iterator.call(context, obj[key], key, obj); |
|
9535 } |
|
9536 } |
|
9537 } |
|
9538 } |
|
9539 return obj; |
|
9540 } |
|
9541 |
|
9542 function sortedKeys(obj) { |
|
9543 var keys = []; |
|
9544 for (var key in obj) { |
|
9545 if (obj.hasOwnProperty(key)) { |
|
9546 keys.push(key); |
|
9547 } |
|
9548 } |
|
9549 return keys.sort(); |
|
9550 } |
|
9551 |
|
9552 function forEachSorted(obj, iterator, context) { |
|
9553 var keys = sortedKeys(obj); |
|
9554 for ( var i = 0; i < keys.length; i++) { |
|
9555 iterator.call(context, obj[keys[i]], keys[i]); |
|
9556 } |
|
9557 return keys; |
|
9558 } |
|
9559 |
|
9560 |
|
9561 /** |
|
9562 * when using forEach the params are value, key, but it is often useful to have key, value. |
|
9563 * @param {function(string, *)} iteratorFn |
|
9564 * @returns {function(*, string)} |
|
9565 */ |
|
9566 function reverseParams(iteratorFn) { |
|
9567 return function(value, key) { iteratorFn(key, value); }; |
|
9568 } |
|
9569 |
|
9570 /** |
|
9571 * A consistent way of creating unique IDs in angular. |
|
9572 * |
|
9573 * Using simple numbers allows us to generate 28.6 million unique ids per second for 10 years before |
|
9574 * we hit number precision issues in JavaScript. |
|
9575 * |
|
9576 * Math.pow(2,53) / 60 / 60 / 24 / 365 / 10 = 28.6M |
|
9577 * |
|
9578 * @returns {number} an unique alpha-numeric string |
|
9579 */ |
|
9580 function nextUid() { |
|
9581 return ++uid; |
|
9582 } |
|
9583 |
|
9584 |
|
9585 /** |
|
9586 * Set or clear the hashkey for an object. |
|
9587 * @param obj object |
|
9588 * @param h the hashkey (!truthy to delete the hashkey) |
|
9589 */ |
|
9590 function setHashKey(obj, h) { |
|
9591 if (h) { |
|
9592 obj.$$hashKey = h; |
|
9593 } |
|
9594 else { |
|
9595 delete obj.$$hashKey; |
|
9596 } |
|
9597 } |
|
9598 |
|
9599 /** |
|
9600 * @ngdoc function |
|
9601 * @name angular.extend |
|
9602 * @module ng |
|
9603 * @kind function |
|
9604 * |
|
9605 * @description |
|
9606 * Extends the destination object `dst` by copying own enumerable properties from the `src` object(s) |
|
9607 * to `dst`. You can specify multiple `src` objects. |
|
9608 * |
|
9609 * @param {Object} dst Destination object. |
|
9610 * @param {...Object} src Source object(s). |
|
9611 * @returns {Object} Reference to `dst`. |
|
9612 */ |
|
9613 function extend(dst) { |
|
9614 var h = dst.$$hashKey; |
|
9615 |
|
9616 for (var i = 1, ii = arguments.length; i < ii; i++) { |
|
9617 var obj = arguments[i]; |
|
9618 if (obj) { |
|
9619 var keys = Object.keys(obj); |
|
9620 for (var j = 0, jj = keys.length; j < jj; j++) { |
|
9621 var key = keys[j]; |
|
9622 dst[key] = obj[key]; |
|
9623 } |
|
9624 } |
|
9625 } |
|
9626 |
|
9627 setHashKey(dst, h); |
|
9628 return dst; |
|
9629 } |
|
9630 |
|
9631 function int(str) { |
|
9632 return parseInt(str, 10); |
|
9633 } |
|
9634 |
|
9635 |
|
9636 function inherit(parent, extra) { |
|
9637 return extend(new (extend(function() {}, {prototype:parent}))(), extra); |
|
9638 } |
|
9639 |
|
9640 /** |
|
9641 * @ngdoc function |
|
9642 * @name angular.noop |
|
9643 * @module ng |
|
9644 * @kind function |
|
9645 * |
|
9646 * @description |
|
9647 * A function that performs no operations. This function can be useful when writing code in the |
|
9648 * functional style. |
|
9649 ```js |
|
9650 function foo(callback) { |
|
9651 var result = calculateResult(); |
|
9652 (callback || angular.noop)(result); |
|
9653 } |
|
9654 ``` |
|
9655 */ |
|
9656 function noop() {} |
|
9657 noop.$inject = []; |
|
9658 |
|
9659 |
|
9660 /** |
|
9661 * @ngdoc function |
|
9662 * @name angular.identity |
|
9663 * @module ng |
|
9664 * @kind function |
|
9665 * |
|
9666 * @description |
|
9667 * A function that returns its first argument. This function is useful when writing code in the |
|
9668 * functional style. |
|
9669 * |
|
9670 ```js |
|
9671 function transformer(transformationFn, value) { |
|
9672 return (transformationFn || angular.identity)(value); |
|
9673 }; |
|
9674 ``` |
|
9675 */ |
|
9676 function identity($) {return $;} |
|
9677 identity.$inject = []; |
|
9678 |
|
9679 |
|
9680 function valueFn(value) {return function() {return value;};} |
|
9681 |
|
9682 /** |
|
9683 * @ngdoc function |
|
9684 * @name angular.isUndefined |
|
9685 * @module ng |
|
9686 * @kind function |
|
9687 * |
|
9688 * @description |
|
9689 * Determines if a reference is undefined. |
|
9690 * |
|
9691 * @param {*} value Reference to check. |
|
9692 * @returns {boolean} True if `value` is undefined. |
|
9693 */ |
|
9694 function isUndefined(value){return typeof value === 'undefined';} |
|
9695 |
|
9696 |
|
9697 /** |
|
9698 * @ngdoc function |
|
9699 * @name angular.isDefined |
|
9700 * @module ng |
|
9701 * @kind function |
|
9702 * |
|
9703 * @description |
|
9704 * Determines if a reference is defined. |
|
9705 * |
|
9706 * @param {*} value Reference to check. |
|
9707 * @returns {boolean} True if `value` is defined. |
|
9708 */ |
|
9709 function isDefined(value){return typeof value !== 'undefined';} |
|
9710 |
|
9711 |
|
9712 /** |
|
9713 * @ngdoc function |
|
9714 * @name angular.isObject |
|
9715 * @module ng |
|
9716 * @kind function |
|
9717 * |
|
9718 * @description |
|
9719 * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not |
|
9720 * considered to be objects. Note that JavaScript arrays are objects. |
|
9721 * |
|
9722 * @param {*} value Reference to check. |
|
9723 * @returns {boolean} True if `value` is an `Object` but not `null`. |
|
9724 */ |
|
9725 function isObject(value){ |
|
9726 // http://jsperf.com/isobject4 |
|
9727 return value !== null && typeof value === 'object'; |
|
9728 } |
|
9729 |
|
9730 |
|
9731 /** |
|
9732 * @ngdoc function |
|
9733 * @name angular.isString |
|
9734 * @module ng |
|
9735 * @kind function |
|
9736 * |
|
9737 * @description |
|
9738 * Determines if a reference is a `String`. |
|
9739 * |
|
9740 * @param {*} value Reference to check. |
|
9741 * @returns {boolean} True if `value` is a `String`. |
|
9742 */ |
|
9743 function isString(value){return typeof value === 'string';} |
|
9744 |
|
9745 |
|
9746 /** |
|
9747 * @ngdoc function |
|
9748 * @name angular.isNumber |
|
9749 * @module ng |
|
9750 * @kind function |
|
9751 * |
|
9752 * @description |
|
9753 * Determines if a reference is a `Number`. |
|
9754 * |
|
9755 * @param {*} value Reference to check. |
|
9756 * @returns {boolean} True if `value` is a `Number`. |
|
9757 */ |
|
9758 function isNumber(value){return typeof value === 'number';} |
|
9759 |
|
9760 |
|
9761 /** |
|
9762 * @ngdoc function |
|
9763 * @name angular.isDate |
|
9764 * @module ng |
|
9765 * @kind function |
|
9766 * |
|
9767 * @description |
|
9768 * Determines if a value is a date. |
|
9769 * |
|
9770 * @param {*} value Reference to check. |
|
9771 * @returns {boolean} True if `value` is a `Date`. |
|
9772 */ |
|
9773 function isDate(value) { |
|
9774 return toString.call(value) === '[object Date]'; |
|
9775 } |
|
9776 |
|
9777 |
|
9778 /** |
|
9779 * @ngdoc function |
|
9780 * @name angular.isArray |
|
9781 * @module ng |
|
9782 * @kind function |
|
9783 * |
|
9784 * @description |
|
9785 * Determines if a reference is an `Array`. |
|
9786 * |
|
9787 * @param {*} value Reference to check. |
|
9788 * @returns {boolean} True if `value` is an `Array`. |
|
9789 */ |
|
9790 var isArray = Array.isArray; |
|
9791 |
|
9792 /** |
|
9793 * @ngdoc function |
|
9794 * @name angular.isFunction |
|
9795 * @module ng |
|
9796 * @kind function |
|
9797 * |
|
9798 * @description |
|
9799 * Determines if a reference is a `Function`. |
|
9800 * |
|
9801 * @param {*} value Reference to check. |
|
9802 * @returns {boolean} True if `value` is a `Function`. |
|
9803 */ |
|
9804 function isFunction(value){return typeof value === 'function';} |
|
9805 |
|
9806 |
|
9807 /** |
|
9808 * Determines if a value is a regular expression object. |
|
9809 * |
|
9810 * @private |
|
9811 * @param {*} value Reference to check. |
|
9812 * @returns {boolean} True if `value` is a `RegExp`. |
|
9813 */ |
|
9814 function isRegExp(value) { |
|
9815 return toString.call(value) === '[object RegExp]'; |
|
9816 } |
|
9817 |
|
9818 |
|
9819 /** |
|
9820 * Checks if `obj` is a window object. |
|
9821 * |
|
9822 * @private |
|
9823 * @param {*} obj Object to check |
|
9824 * @returns {boolean} True if `obj` is a window obj. |
|
9825 */ |
|
9826 function isWindow(obj) { |
|
9827 return obj && obj.window === obj; |
|
9828 } |
|
9829 |
|
9830 |
|
9831 function isScope(obj) { |
|
9832 return obj && obj.$evalAsync && obj.$watch; |
|
9833 } |
|
9834 |
|
9835 |
|
9836 function isFile(obj) { |
|
9837 return toString.call(obj) === '[object File]'; |
|
9838 } |
|
9839 |
|
9840 |
|
9841 function isBlob(obj) { |
|
9842 return toString.call(obj) === '[object Blob]'; |
|
9843 } |
|
9844 |
|
9845 |
|
9846 function isBoolean(value) { |
|
9847 return typeof value === 'boolean'; |
|
9848 } |
|
9849 |
|
9850 |
|
9851 function isPromiseLike(obj) { |
|
9852 return obj && isFunction(obj.then); |
|
9853 } |
|
9854 |
|
9855 |
|
9856 var trim = function(value) { |
|
9857 return isString(value) ? value.trim() : value; |
|
9858 }; |
|
9859 |
|
9860 |
|
9861 /** |
|
9862 * @ngdoc function |
|
9863 * @name angular.isElement |
|
9864 * @module ng |
|
9865 * @kind function |
|
9866 * |
|
9867 * @description |
|
9868 * Determines if a reference is a DOM element (or wrapped jQuery element). |
|
9869 * |
|
9870 * @param {*} value Reference to check. |
|
9871 * @returns {boolean} True if `value` is a DOM element (or wrapped jQuery element). |
|
9872 */ |
|
9873 function isElement(node) { |
|
9874 return !!(node && |
|
9875 (node.nodeName // we are a direct element |
|
9876 || (node.prop && node.attr && node.find))); // we have an on and find method part of jQuery API |
|
9877 } |
|
9878 |
|
9879 /** |
|
9880 * @param str 'key1,key2,...' |
|
9881 * @returns {object} in the form of {key1:true, key2:true, ...} |
|
9882 */ |
|
9883 function makeMap(str) { |
|
9884 var obj = {}, items = str.split(","), i; |
|
9885 for ( i = 0; i < items.length; i++ ) |
|
9886 obj[ items[i] ] = true; |
|
9887 return obj; |
|
9888 } |
|
9889 |
|
9890 |
|
9891 function nodeName_(element) { |
|
9892 return lowercase(element.nodeName || element[0].nodeName); |
|
9893 } |
|
9894 |
|
9895 |
|
9896 /** |
|
9897 * @description |
|
9898 * Determines the number of elements in an array, the number of properties an object has, or |
|
9899 * the length of a string. |
|
9900 * |
|
9901 * Note: This function is used to augment the Object type in Angular expressions. See |
|
9902 * {@link angular.Object} for more information about Angular arrays. |
|
9903 * |
|
9904 * @param {Object|Array|string} obj Object, array, or string to inspect. |
|
9905 * @param {boolean} [ownPropsOnly=false] Count only "own" properties in an object |
|
9906 * @returns {number} The size of `obj` or `0` if `obj` is neither an object nor an array. |
|
9907 */ |
|
9908 function size(obj, ownPropsOnly) { |
|
9909 var count = 0, key; |
|
9910 |
|
9911 if (isArray(obj) || isString(obj)) { |
|
9912 return obj.length; |
|
9913 } else if (isObject(obj)) { |
|
9914 for (key in obj) |
|
9915 if (!ownPropsOnly || obj.hasOwnProperty(key)) |
|
9916 count++; |
|
9917 } |
|
9918 |
|
9919 return count; |
|
9920 } |
|
9921 |
|
9922 |
|
9923 function includes(array, obj) { |
|
9924 return Array.prototype.indexOf.call(array, obj) != -1; |
|
9925 } |
|
9926 |
|
9927 function arrayRemove(array, value) { |
|
9928 var index = array.indexOf(value); |
|
9929 if (index >=0) |
|
9930 array.splice(index, 1); |
|
9931 return value; |
|
9932 } |
|
9933 |
|
9934 function isLeafNode (node) { |
|
9935 if (node) { |
|
9936 switch (nodeName_(node)) { |
|
9937 case "option": |
|
9938 case "pre": |
|
9939 case "title": |
|
9940 return true; |
|
9941 } |
|
9942 } |
|
9943 return false; |
|
9944 } |
|
9945 |
|
9946 /** |
|
9947 * @ngdoc function |
|
9948 * @name angular.copy |
|
9949 * @module ng |
|
9950 * @kind function |
|
9951 * |
|
9952 * @description |
|
9953 * Creates a deep copy of `source`, which should be an object or an array. |
|
9954 * |
|
9955 * * If no destination is supplied, a copy of the object or array is created. |
|
9956 * * If a destination is provided, all of its elements (for array) or properties (for objects) |
|
9957 * are deleted and then all elements/properties from the source are copied to it. |
|
9958 * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned. |
|
9959 * * If `source` is identical to 'destination' an exception will be thrown. |
|
9960 * |
|
9961 * @param {*} source The source that will be used to make a copy. |
|
9962 * Can be any type, including primitives, `null`, and `undefined`. |
|
9963 * @param {(Object|Array)=} destination Destination into which the source is copied. If |
|
9964 * provided, must be of the same type as `source`. |
|
9965 * @returns {*} The copy or updated `destination`, if `destination` was specified. |
|
9966 * |
|
9967 * @example |
|
9968 <example module="copyExample"> |
|
9969 <file name="index.html"> |
|
9970 <div ng-controller="ExampleController"> |
|
9971 <form novalidate class="simple-form"> |
|
9972 Name: <input type="text" ng-model="user.name" /><br /> |
|
9973 E-mail: <input type="email" ng-model="user.email" /><br /> |
|
9974 Gender: <input type="radio" ng-model="user.gender" value="male" />male |
|
9975 <input type="radio" ng-model="user.gender" value="female" />female<br /> |
|
9976 <button ng-click="reset()">RESET</button> |
|
9977 <button ng-click="update(user)">SAVE</button> |
|
9978 </form> |
|
9979 <pre>form = {{user | json}}</pre> |
|
9980 <pre>master = {{master | json}}</pre> |
|
9981 </div> |
|
9982 |
|
9983 <script> |
|
9984 angular.module('copyExample', []) |
|
9985 .controller('ExampleController', ['$scope', function($scope) { |
|
9986 $scope.master= {}; |
|
9987 |
|
9988 $scope.update = function(user) { |
|
9989 // Example with 1 argument |
|
9990 $scope.master= angular.copy(user); |
|
9991 }; |
|
9992 |
|
9993 $scope.reset = function() { |
|
9994 // Example with 2 arguments |
|
9995 angular.copy($scope.master, $scope.user); |
|
9996 }; |
|
9997 |
|
9998 $scope.reset(); |
|
9999 }]); |
|
10000 </script> |
|
10001 </file> |
|
10002 </example> |
|
10003 */ |
|
10004 function copy(source, destination, stackSource, stackDest) { |
|
10005 if (isWindow(source) || isScope(source)) { |
|
10006 throw ngMinErr('cpws', |
|
10007 "Can't copy! Making copies of Window or Scope instances is not supported."); |
|
10008 } |
|
10009 |
|
10010 if (!destination) { |
|
10011 destination = source; |
|
10012 if (source) { |
|
10013 if (isArray(source)) { |
|
10014 destination = copy(source, [], stackSource, stackDest); |
|
10015 } else if (isDate(source)) { |
|
10016 destination = new Date(source.getTime()); |
|
10017 } else if (isRegExp(source)) { |
|
10018 destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); |
|
10019 destination.lastIndex = source.lastIndex; |
|
10020 } else if (isObject(source)) { |
|
10021 var emptyObject = Object.create(Object.getPrototypeOf(source)); |
|
10022 destination = copy(source, emptyObject, stackSource, stackDest); |
|
10023 } |
|
10024 } |
|
10025 } else { |
|
10026 if (source === destination) throw ngMinErr('cpi', |
|
10027 "Can't copy! Source and destination are identical."); |
|
10028 |
|
10029 stackSource = stackSource || []; |
|
10030 stackDest = stackDest || []; |
|
10031 |
|
10032 if (isObject(source)) { |
|
10033 var index = stackSource.indexOf(source); |
|
10034 if (index !== -1) return stackDest[index]; |
|
10035 |
|
10036 stackSource.push(source); |
|
10037 stackDest.push(destination); |
|
10038 } |
|
10039 |
|
10040 var result; |
|
10041 if (isArray(source)) { |
|
10042 destination.length = 0; |
|
10043 for ( var i = 0; i < source.length; i++) { |
|
10044 result = copy(source[i], null, stackSource, stackDest); |
|
10045 if (isObject(source[i])) { |
|
10046 stackSource.push(source[i]); |
|
10047 stackDest.push(result); |
|
10048 } |
|
10049 destination.push(result); |
|
10050 } |
|
10051 } else { |
|
10052 var h = destination.$$hashKey; |
|
10053 if (isArray(destination)) { |
|
10054 destination.length = 0; |
|
10055 } else { |
|
10056 forEach(destination, function(value, key) { |
|
10057 delete destination[key]; |
|
10058 }); |
|
10059 } |
|
10060 for ( var key in source) { |
|
10061 if(source.hasOwnProperty(key)) { |
|
10062 result = copy(source[key], null, stackSource, stackDest); |
|
10063 if (isObject(source[key])) { |
|
10064 stackSource.push(source[key]); |
|
10065 stackDest.push(result); |
|
10066 } |
|
10067 destination[key] = result; |
|
10068 } |
|
10069 } |
|
10070 setHashKey(destination,h); |
|
10071 } |
|
10072 |
|
10073 } |
|
10074 return destination; |
|
10075 } |
|
10076 |
|
10077 /** |
|
10078 * Creates a shallow copy of an object, an array or a primitive. |
|
10079 * |
|
10080 * Assumes that there are no proto properties for objects. |
|
10081 */ |
|
10082 function shallowCopy(src, dst) { |
|
10083 if (isArray(src)) { |
|
10084 dst = dst || []; |
|
10085 |
|
10086 for (var i = 0, ii = src.length; i < ii; i++) { |
|
10087 dst[i] = src[i]; |
|
10088 } |
|
10089 } else if (isObject(src)) { |
|
10090 dst = dst || {}; |
|
10091 |
|
10092 for (var key in src) { |
|
10093 if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { |
|
10094 dst[key] = src[key]; |
|
10095 } |
|
10096 } |
|
10097 } |
|
10098 |
|
10099 return dst || src; |
|
10100 } |
|
10101 |
|
10102 |
|
10103 /** |
|
10104 * @ngdoc function |
|
10105 * @name angular.equals |
|
10106 * @module ng |
|
10107 * @kind function |
|
10108 * |
|
10109 * @description |
|
10110 * Determines if two objects or two values are equivalent. Supports value types, regular |
|
10111 * expressions, arrays and objects. |
|
10112 * |
|
10113 * Two objects or values are considered equivalent if at least one of the following is true: |
|
10114 * |
|
10115 * * Both objects or values pass `===` comparison. |
|
10116 * * Both objects or values are of the same type and all of their properties are equal by |
|
10117 * comparing them with `angular.equals`. |
|
10118 * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) |
|
10119 * * Both values represent the same regular expression (In JavaScript, |
|
10120 * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual |
|
10121 * representation matches). |
|
10122 * |
|
10123 * During a property comparison, properties of `function` type and properties with names |
|
10124 * that begin with `$` are ignored. |
|
10125 * |
|
10126 * Scope and DOMWindow objects are being compared only by identify (`===`). |
|
10127 * |
|
10128 * @param {*} o1 Object or value to compare. |
|
10129 * @param {*} o2 Object or value to compare. |
|
10130 * @returns {boolean} True if arguments are equal. |
|
10131 */ |
|
10132 function equals(o1, o2) { |
|
10133 if (o1 === o2) return true; |
|
10134 if (o1 === null || o2 === null) return false; |
|
10135 if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN |
|
10136 var t1 = typeof o1, t2 = typeof o2, length, key, keySet; |
|
10137 if (t1 == t2) { |
|
10138 if (t1 == 'object') { |
|
10139 if (isArray(o1)) { |
|
10140 if (!isArray(o2)) return false; |
|
10141 if ((length = o1.length) == o2.length) { |
|
10142 for(key=0; key<length; key++) { |
|
10143 if (!equals(o1[key], o2[key])) return false; |
|
10144 } |
|
10145 return true; |
|
10146 } |
|
10147 } else if (isDate(o1)) { |
|
10148 if (!isDate(o2)) return false; |
|
10149 return equals(o1.getTime(), o2.getTime()); |
|
10150 } else if (isRegExp(o1) && isRegExp(o2)) { |
|
10151 return o1.toString() == o2.toString(); |
|
10152 } else { |
|
10153 if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || isArray(o2)) return false; |
|
10154 keySet = {}; |
|
10155 for(key in o1) { |
|
10156 if (key.charAt(0) === '$' || isFunction(o1[key])) continue; |
|
10157 if (!equals(o1[key], o2[key])) return false; |
|
10158 keySet[key] = true; |
|
10159 } |
|
10160 for(key in o2) { |
|
10161 if (!keySet.hasOwnProperty(key) && |
|
10162 key.charAt(0) !== '$' && |
|
10163 o2[key] !== undefined && |
|
10164 !isFunction(o2[key])) return false; |
|
10165 } |
|
10166 return true; |
|
10167 } |
|
10168 } |
|
10169 } |
|
10170 return false; |
|
10171 } |
|
10172 |
|
10173 var csp = function() { |
|
10174 if (isDefined(csp.isActive_)) return csp.isActive_; |
|
10175 |
|
10176 var active = !!(document.querySelector('[ng-csp]') || |
|
10177 document.querySelector('[data-ng-csp]')); |
|
10178 |
|
10179 if (!active) { |
|
10180 try { |
|
10181 /* jshint -W031, -W054 */ |
|
10182 new Function(''); |
|
10183 /* jshint +W031, +W054 */ |
|
10184 } catch (e) { |
|
10185 active = true; |
|
10186 } |
|
10187 } |
|
10188 |
|
10189 return (csp.isActive_ = active); |
|
10190 }; |
|
10191 |
|
10192 |
|
10193 |
|
10194 function concat(array1, array2, index) { |
|
10195 return array1.concat(slice.call(array2, index)); |
|
10196 } |
|
10197 |
|
10198 function sliceArgs(args, startIndex) { |
|
10199 return slice.call(args, startIndex || 0); |
|
10200 } |
|
10201 |
|
10202 |
|
10203 /* jshint -W101 */ |
|
10204 /** |
|
10205 * @ngdoc function |
|
10206 * @name angular.bind |
|
10207 * @module ng |
|
10208 * @kind function |
|
10209 * |
|
10210 * @description |
|
10211 * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for |
|
10212 * `fn`). You can supply optional `args` that are prebound to the function. This feature is also |
|
10213 * known as [partial application](http://en.wikipedia.org/wiki/Partial_application), as |
|
10214 * distinguished from [function currying](http://en.wikipedia.org/wiki/Currying#Contrast_with_partial_function_application). |
|
10215 * |
|
10216 * @param {Object} self Context which `fn` should be evaluated in. |
|
10217 * @param {function()} fn Function to be bound. |
|
10218 * @param {...*} args Optional arguments to be prebound to the `fn` function call. |
|
10219 * @returns {function()} Function that wraps the `fn` with all the specified bindings. |
|
10220 */ |
|
10221 /* jshint +W101 */ |
|
10222 function bind(self, fn) { |
|
10223 var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; |
|
10224 if (isFunction(fn) && !(fn instanceof RegExp)) { |
|
10225 return curryArgs.length |
|
10226 ? function() { |
|
10227 return arguments.length |
|
10228 ? fn.apply(self, curryArgs.concat(slice.call(arguments, 0))) |
|
10229 : fn.apply(self, curryArgs); |
|
10230 } |
|
10231 : function() { |
|
10232 return arguments.length |
|
10233 ? fn.apply(self, arguments) |
|
10234 : fn.call(self); |
|
10235 }; |
|
10236 } else { |
|
10237 // in IE, native methods are not functions so they cannot be bound (note: they don't need to be) |
|
10238 return fn; |
|
10239 } |
|
10240 } |
|
10241 |
|
10242 |
|
10243 function toJsonReplacer(key, value) { |
|
10244 var val = value; |
|
10245 |
|
10246 if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { |
|
10247 val = undefined; |
|
10248 } else if (isWindow(value)) { |
|
10249 val = '$WINDOW'; |
|
10250 } else if (value && document === value) { |
|
10251 val = '$DOCUMENT'; |
|
10252 } else if (isScope(value)) { |
|
10253 val = '$SCOPE'; |
|
10254 } |
|
10255 |
|
10256 return val; |
|
10257 } |
|
10258 |
|
10259 |
|
10260 /** |
|
10261 * @ngdoc function |
|
10262 * @name angular.toJson |
|
10263 * @module ng |
|
10264 * @kind function |
|
10265 * |
|
10266 * @description |
|
10267 * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be |
|
10268 * stripped since angular uses this notation internally. |
|
10269 * |
|
10270 * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. |
|
10271 * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. |
|
10272 * @returns {string|undefined} JSON-ified string representing `obj`. |
|
10273 */ |
|
10274 function toJson(obj, pretty) { |
|
10275 if (typeof obj === 'undefined') return undefined; |
|
10276 return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); |
|
10277 } |
|
10278 |
|
10279 |
|
10280 /** |
|
10281 * @ngdoc function |
|
10282 * @name angular.fromJson |
|
10283 * @module ng |
|
10284 * @kind function |
|
10285 * |
|
10286 * @description |
|
10287 * Deserializes a JSON string. |
|
10288 * |
|
10289 * @param {string} json JSON string to deserialize. |
|
10290 * @returns {Object|Array|string|number} Deserialized thingy. |
|
10291 */ |
|
10292 function fromJson(json) { |
|
10293 return isString(json) |
|
10294 ? JSON.parse(json) |
|
10295 : json; |
|
10296 } |
|
10297 |
|
10298 |
|
10299 /** |
|
10300 * @returns {string} Returns the string representation of the element. |
|
10301 */ |
|
10302 function startingTag(element) { |
|
10303 element = jqLite(element).clone(); |
|
10304 try { |
|
10305 // turns out IE does not let you set .html() on elements which |
|
10306 // are not allowed to have children. So we just ignore it. |
|
10307 element.empty(); |
|
10308 } catch(e) {} |
|
10309 var elemHtml = jqLite('<div>').append(element).html(); |
|
10310 try { |
|
10311 return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : |
|
10312 elemHtml. |
|
10313 match(/^(<[^>]+>)/)[1]. |
|
10314 replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); |
|
10315 } catch(e) { |
|
10316 return lowercase(elemHtml); |
|
10317 } |
|
10318 |
|
10319 } |
|
10320 |
|
10321 |
|
10322 ///////////////////////////////////////////////// |
|
10323 |
|
10324 /** |
|
10325 * Tries to decode the URI component without throwing an exception. |
|
10326 * |
|
10327 * @private |
|
10328 * @param str value potential URI component to check. |
|
10329 * @returns {boolean} True if `value` can be decoded |
|
10330 * with the decodeURIComponent function. |
|
10331 */ |
|
10332 function tryDecodeURIComponent(value) { |
|
10333 try { |
|
10334 return decodeURIComponent(value); |
|
10335 } catch(e) { |
|
10336 // Ignore any invalid uri component |
|
10337 } |
|
10338 } |
|
10339 |
|
10340 |
|
10341 /** |
|
10342 * Parses an escaped url query string into key-value pairs. |
|
10343 * @returns {Object.<string,boolean|Array>} |
|
10344 */ |
|
10345 function parseKeyValue(/**string*/keyValue) { |
|
10346 var obj = {}, key_value, key; |
|
10347 forEach((keyValue || "").split('&'), function(keyValue) { |
|
10348 if ( keyValue ) { |
|
10349 key_value = keyValue.replace(/\+/g,'%20').split('='); |
|
10350 key = tryDecodeURIComponent(key_value[0]); |
|
10351 if ( isDefined(key) ) { |
|
10352 var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; |
|
10353 if (!hasOwnProperty.call(obj, key)) { |
|
10354 obj[key] = val; |
|
10355 } else if(isArray(obj[key])) { |
|
10356 obj[key].push(val); |
|
10357 } else { |
|
10358 obj[key] = [obj[key],val]; |
|
10359 } |
|
10360 } |
|
10361 } |
|
10362 }); |
|
10363 return obj; |
|
10364 } |
|
10365 |
|
10366 function toKeyValue(obj) { |
|
10367 var parts = []; |
|
10368 forEach(obj, function(value, key) { |
|
10369 if (isArray(value)) { |
|
10370 forEach(value, function(arrayValue) { |
|
10371 parts.push(encodeUriQuery(key, true) + |
|
10372 (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); |
|
10373 }); |
|
10374 } else { |
|
10375 parts.push(encodeUriQuery(key, true) + |
|
10376 (value === true ? '' : '=' + encodeUriQuery(value, true))); |
|
10377 } |
|
10378 }); |
|
10379 return parts.length ? parts.join('&') : ''; |
|
10380 } |
|
10381 |
|
10382 |
|
10383 /** |
|
10384 * We need our custom method because encodeURIComponent is too aggressive and doesn't follow |
|
10385 * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path |
|
10386 * segments: |
|
10387 * segment = *pchar |
|
10388 * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
|
10389 * pct-encoded = "%" HEXDIG HEXDIG |
|
10390 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" |
|
10391 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" |
|
10392 * / "*" / "+" / "," / ";" / "=" |
|
10393 */ |
|
10394 function encodeUriSegment(val) { |
|
10395 return encodeUriQuery(val, true). |
|
10396 replace(/%26/gi, '&'). |
|
10397 replace(/%3D/gi, '='). |
|
10398 replace(/%2B/gi, '+'); |
|
10399 } |
|
10400 |
|
10401 |
|
10402 /** |
|
10403 * This method is intended for encoding *key* or *value* parts of query component. We need a custom |
|
10404 * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be |
|
10405 * encoded per http://tools.ietf.org/html/rfc3986: |
|
10406 * query = *( pchar / "/" / "?" ) |
|
10407 * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
|
10408 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" |
|
10409 * pct-encoded = "%" HEXDIG HEXDIG |
|
10410 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" |
|
10411 * / "*" / "+" / "," / ";" / "=" |
|
10412 */ |
|
10413 function encodeUriQuery(val, pctEncodeSpaces) { |
|
10414 return encodeURIComponent(val). |
|
10415 replace(/%40/gi, '@'). |
|
10416 replace(/%3A/gi, ':'). |
|
10417 replace(/%24/g, '$'). |
|
10418 replace(/%2C/gi, ','). |
|
10419 replace(/%3B/gi, ';'). |
|
10420 replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); |
|
10421 } |
|
10422 |
|
10423 var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-']; |
|
10424 |
|
10425 function getNgAttribute(element, ngAttr) { |
|
10426 var attr, i, ii = ngAttrPrefixes.length; |
|
10427 element = jqLite(element); |
|
10428 for (i=0; i<ii; ++i) { |
|
10429 attr = ngAttrPrefixes[i] + ngAttr; |
|
10430 if (isString(attr = element.attr(attr))) { |
|
10431 return attr; |
|
10432 } |
|
10433 } |
|
10434 return null; |
|
10435 } |
|
10436 |
|
10437 /** |
|
10438 * @ngdoc directive |
|
10439 * @name ngApp |
|
10440 * @module ng |
|
10441 * |
|
10442 * @element ANY |
|
10443 * @param {angular.Module} ngApp an optional application |
|
10444 * {@link angular.module module} name to load. |
|
10445 * @param {boolean=} ngStrictDi if this attribute is present on the app element, the injector will be |
|
10446 * created in "strict-di" mode. This means that the application will fail to invoke functions which |
|
10447 * do not use explicit function annotation (and are thus unsuitable for minification), as described |
|
10448 * in {@link guide/di the Dependency Injection guide}, and useful debugging info will assist in |
|
10449 * tracking down the root of these bugs. |
|
10450 * |
|
10451 * @description |
|
10452 * |
|
10453 * Use this directive to **auto-bootstrap** an AngularJS application. The `ngApp` directive |
|
10454 * designates the **root element** of the application and is typically placed near the root element |
|
10455 * of the page - e.g. on the `<body>` or `<html>` tags. |
|
10456 * |
|
10457 * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` |
|
10458 * found in the document will be used to define the root element to auto-bootstrap as an |
|
10459 * application. To run multiple applications in an HTML document you must manually bootstrap them using |
|
10460 * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. |
|
10461 * |
|
10462 * You can specify an **AngularJS module** to be used as the root module for the application. This |
|
10463 * module will be loaded into the {@link auto.$injector} when the application is bootstrapped and |
|
10464 * should contain the application code needed or have dependencies on other modules that will |
|
10465 * contain the code. See {@link angular.module} for more information. |
|
10466 * |
|
10467 * In the example below if the `ngApp` directive were not placed on the `html` element then the |
|
10468 * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}` |
|
10469 * would not be resolved to `3`. |
|
10470 * |
|
10471 * `ngApp` is the easiest, and most common, way to bootstrap an application. |
|
10472 * |
|
10473 <example module="ngAppDemo"> |
|
10474 <file name="index.html"> |
|
10475 <div ng-controller="ngAppDemoController"> |
|
10476 I can add: {{a}} + {{b}} = {{ a+b }} |
|
10477 </div> |
|
10478 </file> |
|
10479 <file name="script.js"> |
|
10480 angular.module('ngAppDemo', []).controller('ngAppDemoController', function($scope) { |
|
10481 $scope.a = 1; |
|
10482 $scope.b = 2; |
|
10483 }); |
|
10484 </file> |
|
10485 </example> |
|
10486 * |
|
10487 * Using `ngStrictDi`, you would see something like this: |
|
10488 * |
|
10489 <example ng-app-included="true"> |
|
10490 <file name="index.html"> |
|
10491 <div ng-app="ngAppStrictDemo" ng-strict-di> |
|
10492 <div ng-controller="GoodController1"> |
|
10493 I can add: {{a}} + {{b}} = {{ a+b }} |
|
10494 |
|
10495 <p>This renders because the controller does not fail to |
|
10496 instantiate, by using explicit annotation style (see |
|
10497 script.js for details) |
|
10498 </p> |
|
10499 </div> |
|
10500 |
|
10501 <div ng-controller="GoodController2"> |
|
10502 Name: <input ng-model="name"><br /> |
|
10503 Hello, {{name}}! |
|
10504 |
|
10505 <p>This renders because the controller does not fail to |
|
10506 instantiate, by using explicit annotation style |
|
10507 (see script.js for details) |
|
10508 </p> |
|
10509 </div> |
|
10510 |
|
10511 <div ng-controller="BadController"> |
|
10512 I can add: {{a}} + {{b}} = {{ a+b }} |
|
10513 |
|
10514 <p>The controller could not be instantiated, due to relying |
|
10515 on automatic function annotations (which are disabled in |
|
10516 strict mode). As such, the content of this section is not |
|
10517 interpolated, and there should be an error in your web console. |
|
10518 </p> |
|
10519 </div> |
|
10520 </div> |
|
10521 </file> |
|
10522 <file name="script.js"> |
|
10523 angular.module('ngAppStrictDemo', []) |
|
10524 // BadController will fail to instantiate, due to relying on automatic function annotation, |
|
10525 // rather than an explicit annotation |
|
10526 .controller('BadController', function($scope) { |
|
10527 $scope.a = 1; |
|
10528 $scope.b = 2; |
|
10529 }) |
|
10530 // Unlike BadController, GoodController1 and GoodController2 will not fail to be instantiated, |
|
10531 // due to using explicit annotations using the array style and $inject property, respectively. |
|
10532 .controller('GoodController1', ['$scope', function($scope) { |
|
10533 $scope.a = 1; |
|
10534 $scope.b = 2; |
|
10535 }]) |
|
10536 .controller('GoodController2', GoodController2); |
|
10537 function GoodController2($scope) { |
|
10538 $scope.name = "World"; |
|
10539 } |
|
10540 GoodController2.$inject = ['$scope']; |
|
10541 </file> |
|
10542 <file name="style.css"> |
|
10543 div[ng-controller] { |
|
10544 margin-bottom: 1em; |
|
10545 -webkit-border-radius: 4px; |
|
10546 border-radius: 4px; |
|
10547 border: 1px solid; |
|
10548 padding: .5em; |
|
10549 } |
|
10550 div[ng-controller^=Good] { |
|
10551 border-color: #d6e9c6; |
|
10552 background-color: #dff0d8; |
|
10553 color: #3c763d; |
|
10554 } |
|
10555 div[ng-controller^=Bad] { |
|
10556 border-color: #ebccd1; |
|
10557 background-color: #f2dede; |
|
10558 color: #a94442; |
|
10559 margin-bottom: 0; |
|
10560 } |
|
10561 </file> |
|
10562 </example> |
|
10563 */ |
|
10564 function angularInit(element, bootstrap) { |
|
10565 var appElement, |
|
10566 module, |
|
10567 config = {}; |
|
10568 |
|
10569 // The element `element` has priority over any other element |
|
10570 forEach(ngAttrPrefixes, function(prefix) { |
|
10571 var name = prefix + 'app'; |
|
10572 |
|
10573 if (!appElement && element.hasAttribute && element.hasAttribute(name)) { |
|
10574 appElement = element; |
|
10575 module = element.getAttribute(name); |
|
10576 } |
|
10577 }); |
|
10578 forEach(ngAttrPrefixes, function(prefix) { |
|
10579 var name = prefix + 'app'; |
|
10580 var candidate; |
|
10581 |
|
10582 if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) { |
|
10583 appElement = candidate; |
|
10584 module = candidate.getAttribute(name); |
|
10585 } |
|
10586 }); |
|
10587 if (appElement) { |
|
10588 config.strictDi = getNgAttribute(appElement, "strict-di") !== null; |
|
10589 bootstrap(appElement, module ? [module] : [], config); |
|
10590 } |
|
10591 } |
|
10592 |
|
10593 /** |
|
10594 * @ngdoc function |
|
10595 * @name angular.bootstrap |
|
10596 * @module ng |
|
10597 * @description |
|
10598 * Use this function to manually start up angular application. |
|
10599 * |
|
10600 * See: {@link guide/bootstrap Bootstrap} |
|
10601 * |
|
10602 * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually. |
|
10603 * They must use {@link ng.directive:ngApp ngApp}. |
|
10604 * |
|
10605 * Angular will detect if it has been loaded into the browser more than once and only allow the |
|
10606 * first loaded script to be bootstrapped and will report a warning to the browser console for |
|
10607 * each of the subsequent scripts. This prevents strange results in applications, where otherwise |
|
10608 * multiple instances of Angular try to work on the DOM. |
|
10609 * |
|
10610 * ```html |
|
10611 * <!doctype html> |
|
10612 * <html> |
|
10613 * <body> |
|
10614 * <div ng-controller="WelcomeController"> |
|
10615 * {{greeting}} |
|
10616 * </div> |
|
10617 * |
|
10618 * <script src="angular.js"></script> |
|
10619 * <script> |
|
10620 * var app = angular.module('demo', []) |
|
10621 * .controller('WelcomeController', function($scope) { |
|
10622 * $scope.greeting = 'Welcome!'; |
|
10623 * }); |
|
10624 * angular.bootstrap(document, ['demo']); |
|
10625 * </script> |
|
10626 * </body> |
|
10627 * </html> |
|
10628 * ``` |
|
10629 * |
|
10630 * @param {DOMElement} element DOM element which is the root of angular application. |
|
10631 * @param {Array<String|Function|Array>=} modules an array of modules to load into the application. |
|
10632 * Each item in the array should be the name of a predefined module or a (DI annotated) |
|
10633 * function that will be invoked by the injector as a run block. |
|
10634 * See: {@link angular.module modules} |
|
10635 * @param {Object=} config an object for defining configuration options for the application. The |
|
10636 * following keys are supported: |
|
10637 * |
|
10638 * - `strictDi`: disable automatic function annotation for the application. This is meant to |
|
10639 * assist in finding bugs which break minified code. |
|
10640 * |
|
10641 * @returns {auto.$injector} Returns the newly created injector for this app. |
|
10642 */ |
|
10643 function bootstrap(element, modules, config) { |
|
10644 if (!isObject(config)) config = {}; |
|
10645 var defaultConfig = { |
|
10646 strictDi: false |
|
10647 }; |
|
10648 config = extend(defaultConfig, config); |
|
10649 var doBootstrap = function() { |
|
10650 element = jqLite(element); |
|
10651 |
|
10652 if (element.injector()) { |
|
10653 var tag = (element[0] === document) ? 'document' : startingTag(element); |
|
10654 //Encode angle brackets to prevent input from being sanitized to empty string #8683 |
|
10655 throw ngMinErr( |
|
10656 'btstrpd', |
|
10657 "App Already Bootstrapped with this Element '{0}'", |
|
10658 tag.replace(/</,'<').replace(/>/,'>')); |
|
10659 } |
|
10660 |
|
10661 modules = modules || []; |
|
10662 modules.unshift(['$provide', function($provide) { |
|
10663 $provide.value('$rootElement', element); |
|
10664 }]); |
|
10665 |
|
10666 if (config.debugInfoEnabled) { |
|
10667 // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`. |
|
10668 modules.push(['$compileProvider', function($compileProvider) { |
|
10669 $compileProvider.debugInfoEnabled(true); |
|
10670 }]); |
|
10671 } |
|
10672 |
|
10673 modules.unshift('ng'); |
|
10674 var injector = createInjector(modules, config.strictDi); |
|
10675 injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', |
|
10676 function bootstrapApply(scope, element, compile, injector) { |
|
10677 scope.$apply(function() { |
|
10678 element.data('$injector', injector); |
|
10679 compile(element)(scope); |
|
10680 }); |
|
10681 }] |
|
10682 ); |
|
10683 return injector; |
|
10684 }; |
|
10685 |
|
10686 var NG_ENABLE_DEBUG_INFO = /^NG_ENABLE_DEBUG_INFO!/; |
|
10687 var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; |
|
10688 |
|
10689 if (window && NG_ENABLE_DEBUG_INFO.test(window.name)) { |
|
10690 config.debugInfoEnabled = true; |
|
10691 window.name = window.name.replace(NG_ENABLE_DEBUG_INFO, ''); |
|
10692 } |
|
10693 |
|
10694 if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { |
|
10695 return doBootstrap(); |
|
10696 } |
|
10697 |
|
10698 window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); |
|
10699 angular.resumeBootstrap = function(extraModules) { |
|
10700 forEach(extraModules, function(module) { |
|
10701 modules.push(module); |
|
10702 }); |
|
10703 doBootstrap(); |
|
10704 }; |
|
10705 } |
|
10706 |
|
10707 /** |
|
10708 * @ngdoc function |
|
10709 * @name angular.reloadWithDebugInfo |
|
10710 * @module ng |
|
10711 * @description |
|
10712 * Use this function to reload the current application with debug information turned on. |
|
10713 * This takes precedence over a call to `$compileProvider.debugInfoEnabled(false)`. |
|
10714 * |
|
10715 * See {@link ng.$compileProvider#debugInfoEnabled} for more. |
|
10716 */ |
|
10717 function reloadWithDebugInfo() { |
|
10718 window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; |
|
10719 window.location.reload(); |
|
10720 } |
|
10721 |
|
10722 /** |
|
10723 * @name angular.getTestability |
|
10724 * @module ng |
|
10725 * @description |
|
10726 * Get the testability service for the instance of Angular on the given |
|
10727 * element. |
|
10728 * @param {DOMElement} element DOM element which is the root of angular application. |
|
10729 */ |
|
10730 function getTestability(rootElement) { |
|
10731 return angular.element(rootElement).injector().get('$$testability'); |
|
10732 } |
|
10733 |
|
10734 var SNAKE_CASE_REGEXP = /[A-Z]/g; |
|
10735 function snake_case(name, separator) { |
|
10736 separator = separator || '_'; |
|
10737 return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { |
|
10738 return (pos ? separator : '') + letter.toLowerCase(); |
|
10739 }); |
|
10740 } |
|
10741 |
|
10742 var bindJQueryFired = false; |
|
10743 var skipDestroyOnNextJQueryCleanData; |
|
10744 function bindJQuery() { |
|
10745 var originalCleanData; |
|
10746 |
|
10747 if (bindJQueryFired) { |
|
10748 return; |
|
10749 } |
|
10750 |
|
10751 // bind to jQuery if present; |
|
10752 jQuery = window.jQuery; |
|
10753 // Use jQuery if it exists with proper functionality, otherwise default to us. |
|
10754 // Angular 1.2+ requires jQuery 1.7+ for on()/off() support. |
|
10755 // Angular 1.3+ technically requires at least jQuery 2.1+ but it may work with older |
|
10756 // versions. It will not work for sure with jQuery <1.7, though. |
|
10757 if (jQuery && jQuery.fn.on) { |
|
10758 jqLite = jQuery; |
|
10759 extend(jQuery.fn, { |
|
10760 scope: JQLitePrototype.scope, |
|
10761 isolateScope: JQLitePrototype.isolateScope, |
|
10762 controller: JQLitePrototype.controller, |
|
10763 injector: JQLitePrototype.injector, |
|
10764 inheritedData: JQLitePrototype.inheritedData |
|
10765 }); |
|
10766 |
|
10767 // All nodes removed from the DOM via various jQuery APIs like .remove() |
|
10768 // are passed through jQuery.cleanData. Monkey-patch this method to fire |
|
10769 // the $destroy event on all removed nodes. |
|
10770 originalCleanData = jQuery.cleanData; |
|
10771 jQuery.cleanData = function(elems) { |
|
10772 var events; |
|
10773 if (!skipDestroyOnNextJQueryCleanData) { |
|
10774 for (var i = 0, elem; (elem = elems[i]) != null; i++) { |
|
10775 events = jQuery._data(elem, "events"); |
|
10776 if (events && events.$destroy) { |
|
10777 jQuery(elem).triggerHandler('$destroy'); |
|
10778 } |
|
10779 } |
|
10780 } else { |
|
10781 skipDestroyOnNextJQueryCleanData = false; |
|
10782 } |
|
10783 originalCleanData(elems); |
|
10784 }; |
|
10785 } else { |
|
10786 jqLite = JQLite; |
|
10787 } |
|
10788 |
|
10789 angular.element = jqLite; |
|
10790 |
|
10791 // Prevent double-proxying. |
|
10792 bindJQueryFired = true; |
|
10793 } |
|
10794 |
|
10795 /** |
|
10796 * throw error if the argument is falsy. |
|
10797 */ |
|
10798 function assertArg(arg, name, reason) { |
|
10799 if (!arg) { |
|
10800 throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); |
|
10801 } |
|
10802 return arg; |
|
10803 } |
|
10804 |
|
10805 function assertArgFn(arg, name, acceptArrayAnnotation) { |
|
10806 if (acceptArrayAnnotation && isArray(arg)) { |
|
10807 arg = arg[arg.length - 1]; |
|
10808 } |
|
10809 |
|
10810 assertArg(isFunction(arg), name, 'not a function, got ' + |
|
10811 (arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg)); |
|
10812 return arg; |
|
10813 } |
|
10814 |
|
10815 /** |
|
10816 * throw error if the name given is hasOwnProperty |
|
10817 * @param {String} name the name to test |
|
10818 * @param {String} context the context in which the name is used, such as module or directive |
|
10819 */ |
|
10820 function assertNotHasOwnProperty(name, context) { |
|
10821 if (name === 'hasOwnProperty') { |
|
10822 throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); |
|
10823 } |
|
10824 } |
|
10825 |
|
10826 /** |
|
10827 * Return the value accessible from the object by path. Any undefined traversals are ignored |
|
10828 * @param {Object} obj starting object |
|
10829 * @param {String} path path to traverse |
|
10830 * @param {boolean} [bindFnToScope=true] |
|
10831 * @returns {Object} value as accessible by path |
|
10832 */ |
|
10833 //TODO(misko): this function needs to be removed |
|
10834 function getter(obj, path, bindFnToScope) { |
|
10835 if (!path) return obj; |
|
10836 var keys = path.split('.'); |
|
10837 var key; |
|
10838 var lastInstance = obj; |
|
10839 var len = keys.length; |
|
10840 |
|
10841 for (var i = 0; i < len; i++) { |
|
10842 key = keys[i]; |
|
10843 if (obj) { |
|
10844 obj = (lastInstance = obj)[key]; |
|
10845 } |
|
10846 } |
|
10847 if (!bindFnToScope && isFunction(obj)) { |
|
10848 return bind(lastInstance, obj); |
|
10849 } |
|
10850 return obj; |
|
10851 } |
|
10852 |
|
10853 /** |
|
10854 * Return the DOM siblings between the first and last node in the given array. |
|
10855 * @param {Array} array like object |
|
10856 * @returns {jqLite} jqLite collection containing the nodes |
|
10857 */ |
|
10858 function getBlockNodes(nodes) { |
|
10859 // TODO(perf): just check if all items in `nodes` are siblings and if they are return the original |
|
10860 // collection, otherwise update the original collection. |
|
10861 var node = nodes[0]; |
|
10862 var endNode = nodes[nodes.length - 1]; |
|
10863 var blockNodes = [node]; |
|
10864 |
|
10865 do { |
|
10866 node = node.nextSibling; |
|
10867 if (!node) break; |
|
10868 blockNodes.push(node); |
|
10869 } while (node !== endNode); |
|
10870 |
|
10871 return jqLite(blockNodes); |
|
10872 } |
|
10873 |
|
10874 |
|
10875 /** |
|
10876 * Creates a new object without a prototype. This object is useful for lookup without having to |
|
10877 * guard against prototypically inherited properties via hasOwnProperty. |
|
10878 * |
|
10879 * Related micro-benchmarks: |
|
10880 * - http://jsperf.com/object-create2 |
|
10881 * - http://jsperf.com/proto-map-lookup/2 |
|
10882 * - http://jsperf.com/for-in-vs-object-keys2 |
|
10883 * |
|
10884 * @returns {Object} |
|
10885 */ |
|
10886 function createMap() { |
|
10887 return Object.create(null); |
|
10888 } |
|
10889 |
|
10890 var NODE_TYPE_ELEMENT = 1; |
|
10891 var NODE_TYPE_TEXT = 3; |
|
10892 var NODE_TYPE_COMMENT = 8; |
|
10893 var NODE_TYPE_DOCUMENT = 9; |
|
10894 var NODE_TYPE_DOCUMENT_FRAGMENT = 11; |
|
10895 |
|
10896 /** |
|
10897 * @ngdoc type |
|
10898 * @name angular.Module |
|
10899 * @module ng |
|
10900 * @description |
|
10901 * |
|
10902 * Interface for configuring angular {@link angular.module modules}. |
|
10903 */ |
|
10904 |
|
10905 function setupModuleLoader(window) { |
|
10906 |
|
10907 var $injectorMinErr = minErr('$injector'); |
|
10908 var ngMinErr = minErr('ng'); |
|
10909 |
|
10910 function ensure(obj, name, factory) { |
|
10911 return obj[name] || (obj[name] = factory()); |
|
10912 } |
|
10913 |
|
10914 var angular = ensure(window, 'angular', Object); |
|
10915 |
|
10916 // We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap |
|
10917 angular.$$minErr = angular.$$minErr || minErr; |
|
10918 |
|
10919 return ensure(angular, 'module', function() { |
|
10920 /** @type {Object.<string, angular.Module>} */ |
|
10921 var modules = {}; |
|
10922 |
|
10923 /** |
|
10924 * @ngdoc function |
|
10925 * @name angular.module |
|
10926 * @module ng |
|
10927 * @description |
|
10928 * |
|
10929 * The `angular.module` is a global place for creating, registering and retrieving Angular |
|
10930 * modules. |
|
10931 * All modules (angular core or 3rd party) that should be available to an application must be |
|
10932 * registered using this mechanism. |
|
10933 * |
|
10934 * When passed two or more arguments, a new module is created. If passed only one argument, an |
|
10935 * existing module (the name passed as the first argument to `module`) is retrieved. |
|
10936 * |
|
10937 * |
|
10938 * # Module |
|
10939 * |
|
10940 * A module is a collection of services, directives, controllers, filters, and configuration information. |
|
10941 * `angular.module` is used to configure the {@link auto.$injector $injector}. |
|
10942 * |
|
10943 * ```js |
|
10944 * // Create a new module |
|
10945 * var myModule = angular.module('myModule', []); |
|
10946 * |
|
10947 * // register a new service |
|
10948 * myModule.value('appName', 'MyCoolApp'); |
|
10949 * |
|
10950 * // configure existing services inside initialization blocks. |
|
10951 * myModule.config(['$locationProvider', function($locationProvider) { |
|
10952 * // Configure existing providers |
|
10953 * $locationProvider.hashPrefix('!'); |
|
10954 * }]); |
|
10955 * ``` |
|
10956 * |
|
10957 * Then you can create an injector and load your modules like this: |
|
10958 * |
|
10959 * ```js |
|
10960 * var injector = angular.injector(['ng', 'myModule']) |
|
10961 * ``` |
|
10962 * |
|
10963 * However it's more likely that you'll just use |
|
10964 * {@link ng.directive:ngApp ngApp} or |
|
10965 * {@link angular.bootstrap} to simplify this process for you. |
|
10966 * |
|
10967 * @param {!string} name The name of the module to create or retrieve. |
|
10968 * @param {!Array.<string>=} requires If specified then new module is being created. If |
|
10969 * unspecified then the module is being retrieved for further configuration. |
|
10970 * @param {Function=} configFn Optional configuration function for the module. Same as |
|
10971 * {@link angular.Module#config Module#config()}. |
|
10972 * @returns {module} new module with the {@link angular.Module} api. |
|
10973 */ |
|
10974 return function module(name, requires, configFn) { |
|
10975 var assertNotHasOwnProperty = function(name, context) { |
|
10976 if (name === 'hasOwnProperty') { |
|
10977 throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); |
|
10978 } |
|
10979 }; |
|
10980 |
|
10981 assertNotHasOwnProperty(name, 'module'); |
|
10982 if (requires && modules.hasOwnProperty(name)) { |
|
10983 modules[name] = null; |
|
10984 } |
|
10985 return ensure(modules, name, function() { |
|
10986 if (!requires) { |
|
10987 throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + |
|
10988 "the module name or forgot to load it. If registering a module ensure that you " + |
|
10989 "specify the dependencies as the second argument.", name); |
|
10990 } |
|
10991 |
|
10992 /** @type {!Array.<Array.<*>>} */ |
|
10993 var invokeQueue = []; |
|
10994 |
|
10995 /** @type {!Array.<Function>} */ |
|
10996 var configBlocks = []; |
|
10997 |
|
10998 /** @type {!Array.<Function>} */ |
|
10999 var runBlocks = []; |
|
11000 |
|
11001 var config = invokeLater('$injector', 'invoke', 'push', configBlocks); |
|
11002 |
|
11003 /** @type {angular.Module} */ |
|
11004 var moduleInstance = { |
|
11005 // Private state |
|
11006 _invokeQueue: invokeQueue, |
|
11007 _configBlocks: configBlocks, |
|
11008 _runBlocks: runBlocks, |
|
11009 |
|
11010 /** |
|
11011 * @ngdoc property |
|
11012 * @name angular.Module#requires |
|
11013 * @module ng |
|
11014 * |
|
11015 * @description |
|
11016 * Holds the list of modules which the injector will load before the current module is |
|
11017 * loaded. |
|
11018 */ |
|
11019 requires: requires, |
|
11020 |
|
11021 /** |
|
11022 * @ngdoc property |
|
11023 * @name angular.Module#name |
|
11024 * @module ng |
|
11025 * |
|
11026 * @description |
|
11027 * Name of the module. |
|
11028 */ |
|
11029 name: name, |
|
11030 |
|
11031 |
|
11032 /** |
|
11033 * @ngdoc method |
|
11034 * @name angular.Module#provider |
|
11035 * @module ng |
|
11036 * @param {string} name service name |
|
11037 * @param {Function} providerType Construction function for creating new instance of the |
|
11038 * service. |
|
11039 * @description |
|
11040 * See {@link auto.$provide#provider $provide.provider()}. |
|
11041 */ |
|
11042 provider: invokeLater('$provide', 'provider'), |
|
11043 |
|
11044 /** |
|
11045 * @ngdoc method |
|
11046 * @name angular.Module#factory |
|
11047 * @module ng |
|
11048 * @param {string} name service name |
|
11049 * @param {Function} providerFunction Function for creating new instance of the service. |
|
11050 * @description |
|
11051 * See {@link auto.$provide#factory $provide.factory()}. |
|
11052 */ |
|
11053 factory: invokeLater('$provide', 'factory'), |
|
11054 |
|
11055 /** |
|
11056 * @ngdoc method |
|
11057 * @name angular.Module#service |
|
11058 * @module ng |
|
11059 * @param {string} name service name |
|
11060 * @param {Function} constructor A constructor function that will be instantiated. |
|
11061 * @description |
|
11062 * See {@link auto.$provide#service $provide.service()}. |
|
11063 */ |
|
11064 service: invokeLater('$provide', 'service'), |
|
11065 |
|
11066 /** |
|
11067 * @ngdoc method |
|
11068 * @name angular.Module#value |
|
11069 * @module ng |
|
11070 * @param {string} name service name |
|
11071 * @param {*} object Service instance object. |
|
11072 * @description |
|
11073 * See {@link auto.$provide#value $provide.value()}. |
|
11074 */ |
|
11075 value: invokeLater('$provide', 'value'), |
|
11076 |
|
11077 /** |
|
11078 * @ngdoc method |
|
11079 * @name angular.Module#constant |
|
11080 * @module ng |
|
11081 * @param {string} name constant name |
|
11082 * @param {*} object Constant value. |
|
11083 * @description |
|
11084 * Because the constant are fixed, they get applied before other provide methods. |
|
11085 * See {@link auto.$provide#constant $provide.constant()}. |
|
11086 */ |
|
11087 constant: invokeLater('$provide', 'constant', 'unshift'), |
|
11088 |
|
11089 /** |
|
11090 * @ngdoc method |
|
11091 * @name angular.Module#animation |
|
11092 * @module ng |
|
11093 * @param {string} name animation name |
|
11094 * @param {Function} animationFactory Factory function for creating new instance of an |
|
11095 * animation. |
|
11096 * @description |
|
11097 * |
|
11098 * **NOTE**: animations take effect only if the **ngAnimate** module is loaded. |
|
11099 * |
|
11100 * |
|
11101 * Defines an animation hook that can be later used with |
|
11102 * {@link ngAnimate.$animate $animate} service and directives that use this service. |
|
11103 * |
|
11104 * ```js |
|
11105 * module.animation('.animation-name', function($inject1, $inject2) { |
|
11106 * return { |
|
11107 * eventName : function(element, done) { |
|
11108 * //code to run the animation |
|
11109 * //once complete, then run done() |
|
11110 * return function cancellationFunction(element) { |
|
11111 * //code to cancel the animation |
|
11112 * } |
|
11113 * } |
|
11114 * } |
|
11115 * }) |
|
11116 * ``` |
|
11117 * |
|
11118 * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and |
|
11119 * {@link ngAnimate ngAnimate module} for more information. |
|
11120 */ |
|
11121 animation: invokeLater('$animateProvider', 'register'), |
|
11122 |
|
11123 /** |
|
11124 * @ngdoc method |
|
11125 * @name angular.Module#filter |
|
11126 * @module ng |
|
11127 * @param {string} name Filter name. |
|
11128 * @param {Function} filterFactory Factory function for creating new instance of filter. |
|
11129 * @description |
|
11130 * See {@link ng.$filterProvider#register $filterProvider.register()}. |
|
11131 */ |
|
11132 filter: invokeLater('$filterProvider', 'register'), |
|
11133 |
|
11134 /** |
|
11135 * @ngdoc method |
|
11136 * @name angular.Module#controller |
|
11137 * @module ng |
|
11138 * @param {string|Object} name Controller name, or an object map of controllers where the |
|
11139 * keys are the names and the values are the constructors. |
|
11140 * @param {Function} constructor Controller constructor function. |
|
11141 * @description |
|
11142 * See {@link ng.$controllerProvider#register $controllerProvider.register()}. |
|
11143 */ |
|
11144 controller: invokeLater('$controllerProvider', 'register'), |
|
11145 |
|
11146 /** |
|
11147 * @ngdoc method |
|
11148 * @name angular.Module#directive |
|
11149 * @module ng |
|
11150 * @param {string|Object} name Directive name, or an object map of directives where the |
|
11151 * keys are the names and the values are the factories. |
|
11152 * @param {Function} directiveFactory Factory function for creating new instance of |
|
11153 * directives. |
|
11154 * @description |
|
11155 * See {@link ng.$compileProvider#directive $compileProvider.directive()}. |
|
11156 */ |
|
11157 directive: invokeLater('$compileProvider', 'directive'), |
|
11158 |
|
11159 /** |
|
11160 * @ngdoc method |
|
11161 * @name angular.Module#config |
|
11162 * @module ng |
|
11163 * @param {Function} configFn Execute this function on module load. Useful for service |
|
11164 * configuration. |
|
11165 * @description |
|
11166 * Use this method to register work which needs to be performed on module loading. |
|
11167 * For more about how to configure services, see |
|
11168 * {@link providers#providers_provider-recipe Provider Recipe}. |
|
11169 */ |
|
11170 config: config, |
|
11171 |
|
11172 /** |
|
11173 * @ngdoc method |
|
11174 * @name angular.Module#run |
|
11175 * @module ng |
|
11176 * @param {Function} initializationFn Execute this function after injector creation. |
|
11177 * Useful for application initialization. |
|
11178 * @description |
|
11179 * Use this method to register work which should be performed when the injector is done |
|
11180 * loading all modules. |
|
11181 */ |
|
11182 run: function(block) { |
|
11183 runBlocks.push(block); |
|
11184 return this; |
|
11185 } |
|
11186 }; |
|
11187 |
|
11188 if (configFn) { |
|
11189 config(configFn); |
|
11190 } |
|
11191 |
|
11192 return moduleInstance; |
|
11193 |
|
11194 /** |
|
11195 * @param {string} provider |
|
11196 * @param {string} method |
|
11197 * @param {String=} insertMethod |
|
11198 * @returns {angular.Module} |
|
11199 */ |
|
11200 function invokeLater(provider, method, insertMethod, queue) { |
|
11201 if (!queue) queue = invokeQueue; |
|
11202 return function() { |
|
11203 queue[insertMethod || 'push']([provider, method, arguments]); |
|
11204 return moduleInstance; |
|
11205 }; |
|
11206 } |
|
11207 }); |
|
11208 }; |
|
11209 }); |
|
11210 |
|
11211 } |
|
11212 |
|
11213 /* global angularModule: true, |
|
11214 version: true, |
|
11215 |
|
11216 $LocaleProvider, |
|
11217 $CompileProvider, |
|
11218 |
|
11219 htmlAnchorDirective, |
|
11220 inputDirective, |
|
11221 inputDirective, |
|
11222 formDirective, |
|
11223 scriptDirective, |
|
11224 selectDirective, |
|
11225 styleDirective, |
|
11226 optionDirective, |
|
11227 ngBindDirective, |
|
11228 ngBindHtmlDirective, |
|
11229 ngBindTemplateDirective, |
|
11230 ngClassDirective, |
|
11231 ngClassEvenDirective, |
|
11232 ngClassOddDirective, |
|
11233 ngCspDirective, |
|
11234 ngCloakDirective, |
|
11235 ngControllerDirective, |
|
11236 ngFormDirective, |
|
11237 ngHideDirective, |
|
11238 ngIfDirective, |
|
11239 ngIncludeDirective, |
|
11240 ngIncludeFillContentDirective, |
|
11241 ngInitDirective, |
|
11242 ngNonBindableDirective, |
|
11243 ngPluralizeDirective, |
|
11244 ngRepeatDirective, |
|
11245 ngShowDirective, |
|
11246 ngStyleDirective, |
|
11247 ngSwitchDirective, |
|
11248 ngSwitchWhenDirective, |
|
11249 ngSwitchDefaultDirective, |
|
11250 ngOptionsDirective, |
|
11251 ngTranscludeDirective, |
|
11252 ngModelDirective, |
|
11253 ngListDirective, |
|
11254 ngChangeDirective, |
|
11255 patternDirective, |
|
11256 patternDirective, |
|
11257 requiredDirective, |
|
11258 requiredDirective, |
|
11259 minlengthDirective, |
|
11260 minlengthDirective, |
|
11261 maxlengthDirective, |
|
11262 maxlengthDirective, |
|
11263 ngValueDirective, |
|
11264 ngModelOptionsDirective, |
|
11265 ngAttributeAliasDirectives, |
|
11266 ngEventDirectives, |
|
11267 |
|
11268 $AnchorScrollProvider, |
|
11269 $AnimateProvider, |
|
11270 $BrowserProvider, |
|
11271 $CacheFactoryProvider, |
|
11272 $ControllerProvider, |
|
11273 $DocumentProvider, |
|
11274 $ExceptionHandlerProvider, |
|
11275 $FilterProvider, |
|
11276 $InterpolateProvider, |
|
11277 $IntervalProvider, |
|
11278 $HttpProvider, |
|
11279 $HttpBackendProvider, |
|
11280 $LocationProvider, |
|
11281 $LogProvider, |
|
11282 $ParseProvider, |
|
11283 $RootScopeProvider, |
|
11284 $QProvider, |
|
11285 $$QProvider, |
|
11286 $$SanitizeUriProvider, |
|
11287 $SceProvider, |
|
11288 $SceDelegateProvider, |
|
11289 $SnifferProvider, |
|
11290 $TemplateCacheProvider, |
|
11291 $TemplateRequestProvider, |
|
11292 $$TestabilityProvider, |
|
11293 $TimeoutProvider, |
|
11294 $$RAFProvider, |
|
11295 $$AsyncCallbackProvider, |
|
11296 $WindowProvider |
|
11297 */ |
|
11298 |
|
11299 |
|
11300 /** |
|
11301 * @ngdoc object |
|
11302 * @name angular.version |
|
11303 * @module ng |
|
11304 * @description |
|
11305 * An object that contains information about the current AngularJS version. This object has the |
|
11306 * following properties: |
|
11307 * |
|
11308 * - `full` – `{string}` – Full version string, such as "0.9.18". |
|
11309 * - `major` – `{number}` – Major version number, such as "0". |
|
11310 * - `minor` – `{number}` – Minor version number, such as "9". |
|
11311 * - `dot` – `{number}` – Dot version number, such as "18". |
|
11312 * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". |
|
11313 */ |
|
11314 var version = { |
|
11315 full: '1.3.0-rc.5', // all of these placeholder strings will be replaced by grunt's |
|
11316 major: 1, // package task |
|
11317 minor: 3, |
|
11318 dot: 0, |
|
11319 codeName: 'impossible-choreography' |
|
11320 }; |
|
11321 |
|
11322 |
|
11323 function publishExternalAPI(angular){ |
|
11324 extend(angular, { |
|
11325 'bootstrap': bootstrap, |
|
11326 'copy': copy, |
|
11327 'extend': extend, |
|
11328 'equals': equals, |
|
11329 'element': jqLite, |
|
11330 'forEach': forEach, |
|
11331 'injector': createInjector, |
|
11332 'noop': noop, |
|
11333 'bind': bind, |
|
11334 'toJson': toJson, |
|
11335 'fromJson': fromJson, |
|
11336 'identity': identity, |
|
11337 'isUndefined': isUndefined, |
|
11338 'isDefined': isDefined, |
|
11339 'isString': isString, |
|
11340 'isFunction': isFunction, |
|
11341 'isObject': isObject, |
|
11342 'isNumber': isNumber, |
|
11343 'isElement': isElement, |
|
11344 'isArray': isArray, |
|
11345 'version': version, |
|
11346 'isDate': isDate, |
|
11347 'lowercase': lowercase, |
|
11348 'uppercase': uppercase, |
|
11349 'callbacks': {counter: 0}, |
|
11350 'getTestability': getTestability, |
|
11351 '$$minErr': minErr, |
|
11352 '$$csp': csp, |
|
11353 'reloadWithDebugInfo': reloadWithDebugInfo |
|
11354 }); |
|
11355 |
|
11356 angularModule = setupModuleLoader(window); |
|
11357 try { |
|
11358 angularModule('ngLocale'); |
|
11359 } catch (e) { |
|
11360 angularModule('ngLocale', []).provider('$locale', $LocaleProvider); |
|
11361 } |
|
11362 |
|
11363 angularModule('ng', ['ngLocale'], ['$provide', |
|
11364 function ngModule($provide) { |
|
11365 // $$sanitizeUriProvider needs to be before $compileProvider as it is used by it. |
|
11366 $provide.provider({ |
|
11367 $$sanitizeUri: $$SanitizeUriProvider |
|
11368 }); |
|
11369 $provide.provider('$compile', $CompileProvider). |
|
11370 directive({ |
|
11371 a: htmlAnchorDirective, |
|
11372 input: inputDirective, |
|
11373 textarea: inputDirective, |
|
11374 form: formDirective, |
|
11375 script: scriptDirective, |
|
11376 select: selectDirective, |
|
11377 style: styleDirective, |
|
11378 option: optionDirective, |
|
11379 ngBind: ngBindDirective, |
|
11380 ngBindHtml: ngBindHtmlDirective, |
|
11381 ngBindTemplate: ngBindTemplateDirective, |
|
11382 ngClass: ngClassDirective, |
|
11383 ngClassEven: ngClassEvenDirective, |
|
11384 ngClassOdd: ngClassOddDirective, |
|
11385 ngCloak: ngCloakDirective, |
|
11386 ngController: ngControllerDirective, |
|
11387 ngForm: ngFormDirective, |
|
11388 ngHide: ngHideDirective, |
|
11389 ngIf: ngIfDirective, |
|
11390 ngInclude: ngIncludeDirective, |
|
11391 ngInit: ngInitDirective, |
|
11392 ngNonBindable: ngNonBindableDirective, |
|
11393 ngPluralize: ngPluralizeDirective, |
|
11394 ngRepeat: ngRepeatDirective, |
|
11395 ngShow: ngShowDirective, |
|
11396 ngStyle: ngStyleDirective, |
|
11397 ngSwitch: ngSwitchDirective, |
|
11398 ngSwitchWhen: ngSwitchWhenDirective, |
|
11399 ngSwitchDefault: ngSwitchDefaultDirective, |
|
11400 ngOptions: ngOptionsDirective, |
|
11401 ngTransclude: ngTranscludeDirective, |
|
11402 ngModel: ngModelDirective, |
|
11403 ngList: ngListDirective, |
|
11404 ngChange: ngChangeDirective, |
|
11405 pattern: patternDirective, |
|
11406 ngPattern: patternDirective, |
|
11407 required: requiredDirective, |
|
11408 ngRequired: requiredDirective, |
|
11409 minlength: minlengthDirective, |
|
11410 ngMinlength: minlengthDirective, |
|
11411 maxlength: maxlengthDirective, |
|
11412 ngMaxlength: maxlengthDirective, |
|
11413 ngValue: ngValueDirective, |
|
11414 ngModelOptions: ngModelOptionsDirective |
|
11415 }). |
|
11416 directive({ |
|
11417 ngInclude: ngIncludeFillContentDirective |
|
11418 }). |
|
11419 directive(ngAttributeAliasDirectives). |
|
11420 directive(ngEventDirectives); |
|
11421 $provide.provider({ |
|
11422 $anchorScroll: $AnchorScrollProvider, |
|
11423 $animate: $AnimateProvider, |
|
11424 $browser: $BrowserProvider, |
|
11425 $cacheFactory: $CacheFactoryProvider, |
|
11426 $controller: $ControllerProvider, |
|
11427 $document: $DocumentProvider, |
|
11428 $exceptionHandler: $ExceptionHandlerProvider, |
|
11429 $filter: $FilterProvider, |
|
11430 $interpolate: $InterpolateProvider, |
|
11431 $interval: $IntervalProvider, |
|
11432 $http: $HttpProvider, |
|
11433 $httpBackend: $HttpBackendProvider, |
|
11434 $location: $LocationProvider, |
|
11435 $log: $LogProvider, |
|
11436 $parse: $ParseProvider, |
|
11437 $rootScope: $RootScopeProvider, |
|
11438 $q: $QProvider, |
|
11439 $$q: $$QProvider, |
|
11440 $sce: $SceProvider, |
|
11441 $sceDelegate: $SceDelegateProvider, |
|
11442 $sniffer: $SnifferProvider, |
|
11443 $templateCache: $TemplateCacheProvider, |
|
11444 $templateRequest: $TemplateRequestProvider, |
|
11445 $$testability: $$TestabilityProvider, |
|
11446 $timeout: $TimeoutProvider, |
|
11447 $window: $WindowProvider, |
|
11448 $$rAF: $$RAFProvider, |
|
11449 $$asyncCallback : $$AsyncCallbackProvider |
|
11450 }); |
|
11451 } |
|
11452 ]); |
|
11453 } |
|
11454 |
|
11455 /* global JQLitePrototype: true, |
|
11456 addEventListenerFn: true, |
|
11457 removeEventListenerFn: true, |
|
11458 BOOLEAN_ATTR: true, |
|
11459 ALIASED_ATTR: true, |
|
11460 */ |
|
11461 |
|
11462 ////////////////////////////////// |
|
11463 //JQLite |
|
11464 ////////////////////////////////// |
|
11465 |
|
11466 /** |
|
11467 * @ngdoc function |
|
11468 * @name angular.element |
|
11469 * @module ng |
|
11470 * @kind function |
|
11471 * |
|
11472 * @description |
|
11473 * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. |
|
11474 * |
|
11475 * If jQuery is available, `angular.element` is an alias for the |
|
11476 * [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element` |
|
11477 * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." |
|
11478 * |
|
11479 * <div class="alert alert-success">jqLite is a tiny, API-compatible subset of jQuery that allows |
|
11480 * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most |
|
11481 * commonly needed functionality with the goal of having a very small footprint.</div> |
|
11482 * |
|
11483 * To use jQuery, simply load it before `DOMContentLoaded` event fired. |
|
11484 * |
|
11485 * <div class="alert">**Note:** all element references in Angular are always wrapped with jQuery or |
|
11486 * jqLite; they are never raw DOM references.</div> |
|
11487 * |
|
11488 * ## Angular's jqLite |
|
11489 * jqLite provides only the following jQuery methods: |
|
11490 * |
|
11491 * - [`addClass()`](http://api.jquery.com/addClass/) |
|
11492 * - [`after()`](http://api.jquery.com/after/) |
|
11493 * - [`append()`](http://api.jquery.com/append/) |
|
11494 * - [`attr()`](http://api.jquery.com/attr/) |
|
11495 * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData |
|
11496 * - [`children()`](http://api.jquery.com/children/) - Does not support selectors |
|
11497 * - [`clone()`](http://api.jquery.com/clone/) |
|
11498 * - [`contents()`](http://api.jquery.com/contents/) |
|
11499 * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyles()` |
|
11500 * - [`data()`](http://api.jquery.com/data/) |
|
11501 * - [`detach()`](http://api.jquery.com/detach/) |
|
11502 * - [`empty()`](http://api.jquery.com/empty/) |
|
11503 * - [`eq()`](http://api.jquery.com/eq/) |
|
11504 * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name |
|
11505 * - [`hasClass()`](http://api.jquery.com/hasClass/) |
|
11506 * - [`html()`](http://api.jquery.com/html/) |
|
11507 * - [`next()`](http://api.jquery.com/next/) - Does not support selectors |
|
11508 * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData |
|
11509 * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors |
|
11510 * - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors |
|
11511 * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors |
|
11512 * - [`prepend()`](http://api.jquery.com/prepend/) |
|
11513 * - [`prop()`](http://api.jquery.com/prop/) |
|
11514 * - [`ready()`](http://api.jquery.com/ready/) |
|
11515 * - [`remove()`](http://api.jquery.com/remove/) |
|
11516 * - [`removeAttr()`](http://api.jquery.com/removeAttr/) |
|
11517 * - [`removeClass()`](http://api.jquery.com/removeClass/) |
|
11518 * - [`removeData()`](http://api.jquery.com/removeData/) |
|
11519 * - [`replaceWith()`](http://api.jquery.com/replaceWith/) |
|
11520 * - [`text()`](http://api.jquery.com/text/) |
|
11521 * - [`toggleClass()`](http://api.jquery.com/toggleClass/) |
|
11522 * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. |
|
11523 * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces |
|
11524 * - [`val()`](http://api.jquery.com/val/) |
|
11525 * - [`wrap()`](http://api.jquery.com/wrap/) |
|
11526 * |
|
11527 * ## jQuery/jqLite Extras |
|
11528 * Angular also provides the following additional methods and events to both jQuery and jqLite: |
|
11529 * |
|
11530 * ### Events |
|
11531 * - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event |
|
11532 * on all DOM nodes being removed. This can be used to clean up any 3rd party bindings to the DOM |
|
11533 * element before it is removed. |
|
11534 * |
|
11535 * ### Methods |
|
11536 * - `controller(name)` - retrieves the controller of the current element or its parent. By default |
|
11537 * retrieves controller associated with the `ngController` directive. If `name` is provided as |
|
11538 * camelCase directive name, then the controller for this directive will be retrieved (e.g. |
|
11539 * `'ngModel'`). |
|
11540 * - `injector()` - retrieves the injector of the current element or its parent. |
|
11541 * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current |
|
11542 * element or its parent. |
|
11543 * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the |
|
11544 * current element. This getter should be used only on elements that contain a directive which starts a new isolate |
|
11545 * scope. Calling `scope()` on this element always returns the original non-isolate scope. |
|
11546 * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top |
|
11547 * parent element is reached. |
|
11548 * |
|
11549 * @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. |
|
11550 * @returns {Object} jQuery object. |
|
11551 */ |
|
11552 |
|
11553 JQLite.expando = 'ng339'; |
|
11554 |
|
11555 var jqCache = JQLite.cache = {}, |
|
11556 jqId = 1, |
|
11557 addEventListenerFn = function(element, type, fn) { |
|
11558 element.addEventListener(type, fn, false); |
|
11559 }, |
|
11560 removeEventListenerFn = function(element, type, fn) { |
|
11561 element.removeEventListener(type, fn, false); |
|
11562 }; |
|
11563 |
|
11564 /* |
|
11565 * !!! This is an undocumented "private" function !!! |
|
11566 */ |
|
11567 JQLite._data = function(node) { |
|
11568 //jQuery always returns an object on cache miss |
|
11569 return this.cache[node[this.expando]] || {}; |
|
11570 }; |
|
11571 |
|
11572 function jqNextId() { return ++jqId; } |
|
11573 |
|
11574 |
|
11575 var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; |
|
11576 var MOZ_HACK_REGEXP = /^moz([A-Z])/; |
|
11577 var MOUSE_EVENT_MAP= { mouseleave : "mouseout", mouseenter : "mouseover"}; |
|
11578 var jqLiteMinErr = minErr('jqLite'); |
|
11579 |
|
11580 /** |
|
11581 * Converts snake_case to camelCase. |
|
11582 * Also there is special case for Moz prefix starting with upper case letter. |
|
11583 * @param name Name to normalize |
|
11584 */ |
|
11585 function camelCase(name) { |
|
11586 return name. |
|
11587 replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { |
|
11588 return offset ? letter.toUpperCase() : letter; |
|
11589 }). |
|
11590 replace(MOZ_HACK_REGEXP, 'Moz$1'); |
|
11591 } |
|
11592 |
|
11593 var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; |
|
11594 var HTML_REGEXP = /<|&#?\w+;/; |
|
11595 var TAG_NAME_REGEXP = /<([\w:]+)/; |
|
11596 var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi; |
|
11597 |
|
11598 var wrapMap = { |
|
11599 'option': [1, '<select multiple="multiple">', '</select>'], |
|
11600 |
|
11601 'thead': [1, '<table>', '</table>'], |
|
11602 'col': [2, '<table><colgroup>', '</colgroup></table>'], |
|
11603 'tr': [2, '<table><tbody>', '</tbody></table>'], |
|
11604 'td': [3, '<table><tbody><tr>', '</tr></tbody></table>'], |
|
11605 '_default': [0, "", ""] |
|
11606 }; |
|
11607 |
|
11608 wrapMap.optgroup = wrapMap.option; |
|
11609 wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; |
|
11610 wrapMap.th = wrapMap.td; |
|
11611 |
|
11612 |
|
11613 function jqLiteIsTextNode(html) { |
|
11614 return !HTML_REGEXP.test(html); |
|
11615 } |
|
11616 |
|
11617 function jqLiteAcceptsData(node) { |
|
11618 // The window object can accept data but has no nodeType |
|
11619 // Otherwise we are only interested in elements (1) and documents (9) |
|
11620 var nodeType = node.nodeType; |
|
11621 return nodeType === NODE_TYPE_ELEMENT || !nodeType || nodeType === NODE_TYPE_DOCUMENT; |
|
11622 } |
|
11623 |
|
11624 function jqLiteBuildFragment(html, context) { |
|
11625 var tmp, tag, wrap, |
|
11626 fragment = context.createDocumentFragment(), |
|
11627 nodes = [], i; |
|
11628 |
|
11629 if (jqLiteIsTextNode(html)) { |
|
11630 // Convert non-html into a text node |
|
11631 nodes.push(context.createTextNode(html)); |
|
11632 } else { |
|
11633 // Convert html into DOM nodes |
|
11634 tmp = tmp || fragment.appendChild(context.createElement("div")); |
|
11635 tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); |
|
11636 wrap = wrapMap[tag] || wrapMap._default; |
|
11637 tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2]; |
|
11638 |
|
11639 // Descend through wrappers to the right content |
|
11640 i = wrap[0]; |
|
11641 while (i--) { |
|
11642 tmp = tmp.lastChild; |
|
11643 } |
|
11644 |
|
11645 nodes = concat(nodes, tmp.childNodes); |
|
11646 |
|
11647 tmp = fragment.firstChild; |
|
11648 tmp.textContent = ""; |
|
11649 } |
|
11650 |
|
11651 // Remove wrapper from fragment |
|
11652 fragment.textContent = ""; |
|
11653 fragment.innerHTML = ""; // Clear inner HTML |
|
11654 forEach(nodes, function(node) { |
|
11655 fragment.appendChild(node); |
|
11656 }); |
|
11657 |
|
11658 return fragment; |
|
11659 } |
|
11660 |
|
11661 function jqLiteParseHTML(html, context) { |
|
11662 context = context || document; |
|
11663 var parsed; |
|
11664 |
|
11665 if ((parsed = SINGLE_TAG_REGEXP.exec(html))) { |
|
11666 return [context.createElement(parsed[1])]; |
|
11667 } |
|
11668 |
|
11669 if ((parsed = jqLiteBuildFragment(html, context))) { |
|
11670 return parsed.childNodes; |
|
11671 } |
|
11672 |
|
11673 return []; |
|
11674 } |
|
11675 |
|
11676 ///////////////////////////////////////////// |
|
11677 function JQLite(element) { |
|
11678 if (element instanceof JQLite) { |
|
11679 return element; |
|
11680 } |
|
11681 |
|
11682 var argIsString; |
|
11683 |
|
11684 if (isString(element)) { |
|
11685 element = trim(element); |
|
11686 argIsString = true; |
|
11687 } |
|
11688 if (!(this instanceof JQLite)) { |
|
11689 if (argIsString && element.charAt(0) != '<') { |
|
11690 throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); |
|
11691 } |
|
11692 return new JQLite(element); |
|
11693 } |
|
11694 |
|
11695 if (argIsString) { |
|
11696 jqLiteAddNodes(this, jqLiteParseHTML(element)); |
|
11697 } else { |
|
11698 jqLiteAddNodes(this, element); |
|
11699 } |
|
11700 } |
|
11701 |
|
11702 function jqLiteClone(element) { |
|
11703 return element.cloneNode(true); |
|
11704 } |
|
11705 |
|
11706 function jqLiteDealoc(element, onlyDescendants){ |
|
11707 if (!onlyDescendants) jqLiteRemoveData(element); |
|
11708 |
|
11709 if (element.querySelectorAll) { |
|
11710 var descendants = element.querySelectorAll('*'); |
|
11711 for (var i = 0, l = descendants.length; i < l; i++) { |
|
11712 jqLiteRemoveData(descendants[i]); |
|
11713 } |
|
11714 } |
|
11715 } |
|
11716 |
|
11717 function jqLiteOff(element, type, fn, unsupported) { |
|
11718 if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); |
|
11719 |
|
11720 var expandoStore = jqLiteExpandoStore(element); |
|
11721 var events = expandoStore && expandoStore.events; |
|
11722 var handle = expandoStore && expandoStore.handle; |
|
11723 |
|
11724 if (!handle) return; //no listeners registered |
|
11725 |
|
11726 if (!type) { |
|
11727 for (type in events) { |
|
11728 if (type !== '$destroy') { |
|
11729 removeEventListenerFn(element, type, events[type]); |
|
11730 } |
|
11731 delete events[type]; |
|
11732 } |
|
11733 } else { |
|
11734 forEach(type.split(' '), function(type) { |
|
11735 if (isUndefined(fn)) { |
|
11736 removeEventListenerFn(element, type, events[type]); |
|
11737 delete events[type]; |
|
11738 } else { |
|
11739 arrayRemove(events[type] || [], fn); |
|
11740 } |
|
11741 }); |
|
11742 } |
|
11743 } |
|
11744 |
|
11745 function jqLiteRemoveData(element, name) { |
|
11746 var expandoId = element.ng339; |
|
11747 var expandoStore = expandoId && jqCache[expandoId]; |
|
11748 |
|
11749 if (expandoStore) { |
|
11750 if (name) { |
|
11751 delete expandoStore.data[name]; |
|
11752 return; |
|
11753 } |
|
11754 |
|
11755 if (expandoStore.handle) { |
|
11756 if (expandoStore.events.$destroy) { |
|
11757 expandoStore.handle({}, '$destroy'); |
|
11758 } |
|
11759 jqLiteOff(element); |
|
11760 } |
|
11761 delete jqCache[expandoId]; |
|
11762 element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it |
|
11763 } |
|
11764 } |
|
11765 |
|
11766 |
|
11767 function jqLiteExpandoStore(element, createIfNecessary) { |
|
11768 var expandoId = element.ng339, |
|
11769 expandoStore = expandoId && jqCache[expandoId]; |
|
11770 |
|
11771 if (createIfNecessary && !expandoStore) { |
|
11772 element.ng339 = expandoId = jqNextId(); |
|
11773 expandoStore = jqCache[expandoId] = {events: {}, data: {}, handle: undefined}; |
|
11774 } |
|
11775 |
|
11776 return expandoStore; |
|
11777 } |
|
11778 |
|
11779 |
|
11780 function jqLiteData(element, key, value) { |
|
11781 if (jqLiteAcceptsData(element)) { |
|
11782 |
|
11783 var isSimpleSetter = isDefined(value); |
|
11784 var isSimpleGetter = !isSimpleSetter && key && !isObject(key); |
|
11785 var massGetter = !key; |
|
11786 var expandoStore = jqLiteExpandoStore(element, !isSimpleGetter); |
|
11787 var data = expandoStore && expandoStore.data; |
|
11788 |
|
11789 if (isSimpleSetter) { // data('key', value) |
|
11790 data[key] = value; |
|
11791 } else { |
|
11792 if (massGetter) { // data() |
|
11793 return data; |
|
11794 } else { |
|
11795 if (isSimpleGetter) { // data('key') |
|
11796 // don't force creation of expandoStore if it doesn't exist yet |
|
11797 return data && data[key]; |
|
11798 } else { // mass-setter: data({key1: val1, key2: val2}) |
|
11799 extend(data, key); |
|
11800 } |
|
11801 } |
|
11802 } |
|
11803 } |
|
11804 } |
|
11805 |
|
11806 function jqLiteHasClass(element, selector) { |
|
11807 if (!element.getAttribute) return false; |
|
11808 return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " "). |
|
11809 indexOf( " " + selector + " " ) > -1); |
|
11810 } |
|
11811 |
|
11812 function jqLiteRemoveClass(element, cssClasses) { |
|
11813 if (cssClasses && element.setAttribute) { |
|
11814 forEach(cssClasses.split(' '), function(cssClass) { |
|
11815 element.setAttribute('class', trim( |
|
11816 (" " + (element.getAttribute('class') || '') + " ") |
|
11817 .replace(/[\n\t]/g, " ") |
|
11818 .replace(" " + trim(cssClass) + " ", " ")) |
|
11819 ); |
|
11820 }); |
|
11821 } |
|
11822 } |
|
11823 |
|
11824 function jqLiteAddClass(element, cssClasses) { |
|
11825 if (cssClasses && element.setAttribute) { |
|
11826 var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') |
|
11827 .replace(/[\n\t]/g, " "); |
|
11828 |
|
11829 forEach(cssClasses.split(' '), function(cssClass) { |
|
11830 cssClass = trim(cssClass); |
|
11831 if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { |
|
11832 existingClasses += cssClass + ' '; |
|
11833 } |
|
11834 }); |
|
11835 |
|
11836 element.setAttribute('class', trim(existingClasses)); |
|
11837 } |
|
11838 } |
|
11839 |
|
11840 |
|
11841 function jqLiteAddNodes(root, elements) { |
|
11842 // THIS CODE IS VERY HOT. Don't make changes without benchmarking. |
|
11843 |
|
11844 if (elements) { |
|
11845 |
|
11846 // if a Node (the most common case) |
|
11847 if (elements.nodeType) { |
|
11848 root[root.length++] = elements; |
|
11849 } else { |
|
11850 var length = elements.length; |
|
11851 |
|
11852 // if an Array or NodeList and not a Window |
|
11853 if (typeof length === 'number' && elements.window !== elements) { |
|
11854 if (length) { |
|
11855 for (var i = 0; i < length; i++) { |
|
11856 root[root.length++] = elements[i]; |
|
11857 } |
|
11858 } |
|
11859 } else { |
|
11860 root[root.length++] = elements; |
|
11861 } |
|
11862 } |
|
11863 } |
|
11864 } |
|
11865 |
|
11866 |
|
11867 function jqLiteController(element, name) { |
|
11868 return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); |
|
11869 } |
|
11870 |
|
11871 function jqLiteInheritedData(element, name, value) { |
|
11872 // if element is the document object work with the html element instead |
|
11873 // this makes $(document).scope() possible |
|
11874 if(element.nodeType == NODE_TYPE_DOCUMENT) { |
|
11875 element = element.documentElement; |
|
11876 } |
|
11877 var names = isArray(name) ? name : [name]; |
|
11878 |
|
11879 while (element) { |
|
11880 for (var i = 0, ii = names.length; i < ii; i++) { |
|
11881 if ((value = jqLite.data(element, names[i])) !== undefined) return value; |
|
11882 } |
|
11883 |
|
11884 // If dealing with a document fragment node with a host element, and no parent, use the host |
|
11885 // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM |
|
11886 // to lookup parent controllers. |
|
11887 element = element.parentNode || (element.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT && element.host); |
|
11888 } |
|
11889 } |
|
11890 |
|
11891 function jqLiteEmpty(element) { |
|
11892 jqLiteDealoc(element, true); |
|
11893 while (element.firstChild) { |
|
11894 element.removeChild(element.firstChild); |
|
11895 } |
|
11896 } |
|
11897 |
|
11898 function jqLiteRemove(element, keepData) { |
|
11899 if (!keepData) jqLiteDealoc(element); |
|
11900 var parent = element.parentNode; |
|
11901 if (parent) parent.removeChild(element); |
|
11902 } |
|
11903 |
|
11904 ////////////////////////////////////////// |
|
11905 // Functions which are declared directly. |
|
11906 ////////////////////////////////////////// |
|
11907 var JQLitePrototype = JQLite.prototype = { |
|
11908 ready: function(fn) { |
|
11909 var fired = false; |
|
11910 |
|
11911 function trigger() { |
|
11912 if (fired) return; |
|
11913 fired = true; |
|
11914 fn(); |
|
11915 } |
|
11916 |
|
11917 // check if document is already loaded |
|
11918 if (document.readyState === 'complete'){ |
|
11919 setTimeout(trigger); |
|
11920 } else { |
|
11921 this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 |
|
11922 // we can not use jqLite since we are not done loading and jQuery could be loaded later. |
|
11923 // jshint -W064 |
|
11924 JQLite(window).on('load', trigger); // fallback to window.onload for others |
|
11925 // jshint +W064 |
|
11926 this.on('DOMContentLoaded', trigger); |
|
11927 } |
|
11928 }, |
|
11929 toString: function() { |
|
11930 var value = []; |
|
11931 forEach(this, function(e){ value.push('' + e);}); |
|
11932 return '[' + value.join(', ') + ']'; |
|
11933 }, |
|
11934 |
|
11935 eq: function(index) { |
|
11936 return (index >= 0) ? jqLite(this[index]) : jqLite(this[this.length + index]); |
|
11937 }, |
|
11938 |
|
11939 length: 0, |
|
11940 push: push, |
|
11941 sort: [].sort, |
|
11942 splice: [].splice |
|
11943 }; |
|
11944 |
|
11945 ////////////////////////////////////////// |
|
11946 // Functions iterating getter/setters. |
|
11947 // these functions return self on setter and |
|
11948 // value on get. |
|
11949 ////////////////////////////////////////// |
|
11950 var BOOLEAN_ATTR = {}; |
|
11951 forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) { |
|
11952 BOOLEAN_ATTR[lowercase(value)] = value; |
|
11953 }); |
|
11954 var BOOLEAN_ELEMENTS = {}; |
|
11955 forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { |
|
11956 BOOLEAN_ELEMENTS[value] = true; |
|
11957 }); |
|
11958 var ALIASED_ATTR = { |
|
11959 'ngMinlength' : 'minlength', |
|
11960 'ngMaxlength' : 'maxlength', |
|
11961 'ngMin' : 'min', |
|
11962 'ngMax' : 'max', |
|
11963 'ngPattern' : 'pattern' |
|
11964 }; |
|
11965 |
|
11966 function getBooleanAttrName(element, name) { |
|
11967 // check dom last since we will most likely fail on name |
|
11968 var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; |
|
11969 |
|
11970 // booleanAttr is here twice to minimize DOM access |
|
11971 return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; |
|
11972 } |
|
11973 |
|
11974 function getAliasedAttrName(element, name) { |
|
11975 var nodeName = element.nodeName; |
|
11976 return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name]; |
|
11977 } |
|
11978 |
|
11979 forEach({ |
|
11980 data: jqLiteData, |
|
11981 removeData: jqLiteRemoveData |
|
11982 }, function(fn, name) { |
|
11983 JQLite[name] = fn; |
|
11984 }); |
|
11985 |
|
11986 forEach({ |
|
11987 data: jqLiteData, |
|
11988 inheritedData: jqLiteInheritedData, |
|
11989 |
|
11990 scope: function(element) { |
|
11991 // Can't use jqLiteData here directly so we stay compatible with jQuery! |
|
11992 return jqLite.data(element, '$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); |
|
11993 }, |
|
11994 |
|
11995 isolateScope: function(element) { |
|
11996 // Can't use jqLiteData here directly so we stay compatible with jQuery! |
|
11997 return jqLite.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate'); |
|
11998 }, |
|
11999 |
|
12000 controller: jqLiteController, |
|
12001 |
|
12002 injector: function(element) { |
|
12003 return jqLiteInheritedData(element, '$injector'); |
|
12004 }, |
|
12005 |
|
12006 removeAttr: function(element, name) { |
|
12007 element.removeAttribute(name); |
|
12008 }, |
|
12009 |
|
12010 hasClass: jqLiteHasClass, |
|
12011 |
|
12012 css: function(element, name, value) { |
|
12013 name = camelCase(name); |
|
12014 |
|
12015 if (isDefined(value)) { |
|
12016 element.style[name] = value; |
|
12017 } else { |
|
12018 return element.style[name]; |
|
12019 } |
|
12020 }, |
|
12021 |
|
12022 attr: function(element, name, value){ |
|
12023 var lowercasedName = lowercase(name); |
|
12024 if (BOOLEAN_ATTR[lowercasedName]) { |
|
12025 if (isDefined(value)) { |
|
12026 if (!!value) { |
|
12027 element[name] = true; |
|
12028 element.setAttribute(name, lowercasedName); |
|
12029 } else { |
|
12030 element[name] = false; |
|
12031 element.removeAttribute(lowercasedName); |
|
12032 } |
|
12033 } else { |
|
12034 return (element[name] || |
|
12035 (element.attributes.getNamedItem(name)|| noop).specified) |
|
12036 ? lowercasedName |
|
12037 : undefined; |
|
12038 } |
|
12039 } else if (isDefined(value)) { |
|
12040 element.setAttribute(name, value); |
|
12041 } else if (element.getAttribute) { |
|
12042 // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code |
|
12043 // some elements (e.g. Document) don't have get attribute, so return undefined |
|
12044 var ret = element.getAttribute(name, 2); |
|
12045 // normalize non-existing attributes to undefined (as jQuery) |
|
12046 return ret === null ? undefined : ret; |
|
12047 } |
|
12048 }, |
|
12049 |
|
12050 prop: function(element, name, value) { |
|
12051 if (isDefined(value)) { |
|
12052 element[name] = value; |
|
12053 } else { |
|
12054 return element[name]; |
|
12055 } |
|
12056 }, |
|
12057 |
|
12058 text: (function() { |
|
12059 getText.$dv = ''; |
|
12060 return getText; |
|
12061 |
|
12062 function getText(element, value) { |
|
12063 if (isUndefined(value)) { |
|
12064 var nodeType = element.nodeType; |
|
12065 return (nodeType === NODE_TYPE_ELEMENT || nodeType === NODE_TYPE_TEXT) ? element.textContent : ''; |
|
12066 } |
|
12067 element.textContent = value; |
|
12068 } |
|
12069 })(), |
|
12070 |
|
12071 val: function(element, value) { |
|
12072 if (isUndefined(value)) { |
|
12073 if (element.multiple && nodeName_(element) === 'select') { |
|
12074 var result = []; |
|
12075 forEach(element.options, function (option) { |
|
12076 if (option.selected) { |
|
12077 result.push(option.value || option.text); |
|
12078 } |
|
12079 }); |
|
12080 return result.length === 0 ? null : result; |
|
12081 } |
|
12082 return element.value; |
|
12083 } |
|
12084 element.value = value; |
|
12085 }, |
|
12086 |
|
12087 html: function(element, value) { |
|
12088 if (isUndefined(value)) { |
|
12089 return element.innerHTML; |
|
12090 } |
|
12091 jqLiteDealoc(element, true); |
|
12092 element.innerHTML = value; |
|
12093 }, |
|
12094 |
|
12095 empty: jqLiteEmpty |
|
12096 }, function(fn, name){ |
|
12097 /** |
|
12098 * Properties: writes return selection, reads return first value |
|
12099 */ |
|
12100 JQLite.prototype[name] = function(arg1, arg2) { |
|
12101 var i, key; |
|
12102 var nodeCount = this.length; |
|
12103 |
|
12104 // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it |
|
12105 // in a way that survives minification. |
|
12106 // jqLiteEmpty takes no arguments but is a setter. |
|
12107 if (fn !== jqLiteEmpty && |
|
12108 (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) { |
|
12109 if (isObject(arg1)) { |
|
12110 |
|
12111 // we are a write, but the object properties are the key/values |
|
12112 for (i = 0; i < nodeCount; i++) { |
|
12113 if (fn === jqLiteData) { |
|
12114 // data() takes the whole object in jQuery |
|
12115 fn(this[i], arg1); |
|
12116 } else { |
|
12117 for (key in arg1) { |
|
12118 fn(this[i], key, arg1[key]); |
|
12119 } |
|
12120 } |
|
12121 } |
|
12122 // return self for chaining |
|
12123 return this; |
|
12124 } else { |
|
12125 // we are a read, so read the first child. |
|
12126 // TODO: do we still need this? |
|
12127 var value = fn.$dv; |
|
12128 // Only if we have $dv do we iterate over all, otherwise it is just the first element. |
|
12129 var jj = (value === undefined) ? Math.min(nodeCount, 1) : nodeCount; |
|
12130 for (var j = 0; j < jj; j++) { |
|
12131 var nodeValue = fn(this[j], arg1, arg2); |
|
12132 value = value ? value + nodeValue : nodeValue; |
|
12133 } |
|
12134 return value; |
|
12135 } |
|
12136 } else { |
|
12137 // we are a write, so apply to all children |
|
12138 for (i = 0; i < nodeCount; i++) { |
|
12139 fn(this[i], arg1, arg2); |
|
12140 } |
|
12141 // return self for chaining |
|
12142 return this; |
|
12143 } |
|
12144 }; |
|
12145 }); |
|
12146 |
|
12147 function createEventHandler(element, events) { |
|
12148 var eventHandler = function (event, type) { |
|
12149 // jQuery specific api |
|
12150 event.isDefaultPrevented = function() { |
|
12151 return event.defaultPrevented; |
|
12152 }; |
|
12153 |
|
12154 var eventFns = events[type || event.type]; |
|
12155 var eventFnsLength = eventFns ? eventFns.length : 0; |
|
12156 |
|
12157 if (!eventFnsLength) return; |
|
12158 |
|
12159 if (isUndefined(event.immediatePropagationStopped)) { |
|
12160 var originalStopImmediatePropagation = event.stopImmediatePropagation; |
|
12161 event.stopImmediatePropagation = function() { |
|
12162 event.immediatePropagationStopped = true; |
|
12163 |
|
12164 if (event.stopPropagation) { |
|
12165 event.stopPropagation(); |
|
12166 } |
|
12167 |
|
12168 if (originalStopImmediatePropagation) { |
|
12169 originalStopImmediatePropagation.call(event); |
|
12170 } |
|
12171 }; |
|
12172 } |
|
12173 |
|
12174 event.isImmediatePropagationStopped = function() { |
|
12175 return event.immediatePropagationStopped === true; |
|
12176 }; |
|
12177 |
|
12178 // Copy event handlers in case event handlers array is modified during execution. |
|
12179 if ((eventFnsLength > 1)) { |
|
12180 eventFns = shallowCopy(eventFns); |
|
12181 } |
|
12182 |
|
12183 for (var i = 0; i < eventFnsLength; i++) { |
|
12184 if (!event.isImmediatePropagationStopped()) { |
|
12185 eventFns[i].call(element, event); |
|
12186 } |
|
12187 } |
|
12188 }; |
|
12189 |
|
12190 // TODO: this is a hack for angularMocks/clearDataCache that makes it possible to deregister all |
|
12191 // events on `element` |
|
12192 eventHandler.elem = element; |
|
12193 return eventHandler; |
|
12194 } |
|
12195 |
|
12196 ////////////////////////////////////////// |
|
12197 // Functions iterating traversal. |
|
12198 // These functions chain results into a single |
|
12199 // selector. |
|
12200 ////////////////////////////////////////// |
|
12201 forEach({ |
|
12202 removeData: jqLiteRemoveData, |
|
12203 |
|
12204 on: function jqLiteOn(element, type, fn, unsupported){ |
|
12205 if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); |
|
12206 |
|
12207 // Do not add event handlers to non-elements because they will not be cleaned up. |
|
12208 if (!jqLiteAcceptsData(element)) { |
|
12209 return; |
|
12210 } |
|
12211 |
|
12212 var expandoStore = jqLiteExpandoStore(element, true); |
|
12213 var events = expandoStore.events; |
|
12214 var handle = expandoStore.handle; |
|
12215 |
|
12216 if (!handle) { |
|
12217 handle = expandoStore.handle = createEventHandler(element, events); |
|
12218 } |
|
12219 |
|
12220 // http://jsperf.com/string-indexof-vs-split |
|
12221 var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type]; |
|
12222 var i = types.length; |
|
12223 |
|
12224 while (i--) { |
|
12225 type = types[i]; |
|
12226 var eventFns = events[type]; |
|
12227 |
|
12228 if (!eventFns) { |
|
12229 events[type] = []; |
|
12230 |
|
12231 if (type === 'mouseenter' || type === 'mouseleave') { |
|
12232 // Refer to jQuery's implementation of mouseenter & mouseleave |
|
12233 // Read about mouseenter and mouseleave: |
|
12234 // http://www.quirksmode.org/js/events_mouse.html#link8 |
|
12235 |
|
12236 jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) { |
|
12237 var target = this, related = event.relatedTarget; |
|
12238 // For mousenter/leave call the handler if related is outside the target. |
|
12239 // NB: No relatedTarget if the mouse left/entered the browser window |
|
12240 if ( !related || (related !== target && !target.contains(related)) ){ |
|
12241 handle(event, type); |
|
12242 } |
|
12243 }); |
|
12244 |
|
12245 } else { |
|
12246 if (type !== '$destroy') { |
|
12247 addEventListenerFn(element, type, handle); |
|
12248 } |
|
12249 } |
|
12250 eventFns = events[type]; |
|
12251 } |
|
12252 eventFns.push(fn); |
|
12253 } |
|
12254 }, |
|
12255 |
|
12256 off: jqLiteOff, |
|
12257 |
|
12258 one: function(element, type, fn) { |
|
12259 element = jqLite(element); |
|
12260 |
|
12261 //add the listener twice so that when it is called |
|
12262 //you can remove the original function and still be |
|
12263 //able to call element.off(ev, fn) normally |
|
12264 element.on(type, function onFn() { |
|
12265 element.off(type, fn); |
|
12266 element.off(type, onFn); |
|
12267 }); |
|
12268 element.on(type, fn); |
|
12269 }, |
|
12270 |
|
12271 replaceWith: function(element, replaceNode) { |
|
12272 var index, parent = element.parentNode; |
|
12273 jqLiteDealoc(element); |
|
12274 forEach(new JQLite(replaceNode), function(node){ |
|
12275 if (index) { |
|
12276 parent.insertBefore(node, index.nextSibling); |
|
12277 } else { |
|
12278 parent.replaceChild(node, element); |
|
12279 } |
|
12280 index = node; |
|
12281 }); |
|
12282 }, |
|
12283 |
|
12284 children: function(element) { |
|
12285 var children = []; |
|
12286 forEach(element.childNodes, function(element){ |
|
12287 if (element.nodeType === NODE_TYPE_ELEMENT) |
|
12288 children.push(element); |
|
12289 }); |
|
12290 return children; |
|
12291 }, |
|
12292 |
|
12293 contents: function(element) { |
|
12294 return element.contentDocument || element.childNodes || []; |
|
12295 }, |
|
12296 |
|
12297 append: function(element, node) { |
|
12298 var nodeType = element.nodeType; |
|
12299 if (nodeType !== NODE_TYPE_ELEMENT && nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT) return; |
|
12300 |
|
12301 node = new JQLite(node); |
|
12302 |
|
12303 for (var i = 0, ii = node.length; i < ii; i++) { |
|
12304 var child = node[i]; |
|
12305 element.appendChild(child); |
|
12306 } |
|
12307 }, |
|
12308 |
|
12309 prepend: function(element, node) { |
|
12310 if (element.nodeType === NODE_TYPE_ELEMENT) { |
|
12311 var index = element.firstChild; |
|
12312 forEach(new JQLite(node), function(child){ |
|
12313 element.insertBefore(child, index); |
|
12314 }); |
|
12315 } |
|
12316 }, |
|
12317 |
|
12318 wrap: function(element, wrapNode) { |
|
12319 wrapNode = jqLite(wrapNode).eq(0).clone()[0]; |
|
12320 var parent = element.parentNode; |
|
12321 if (parent) { |
|
12322 parent.replaceChild(wrapNode, element); |
|
12323 } |
|
12324 wrapNode.appendChild(element); |
|
12325 }, |
|
12326 |
|
12327 remove: jqLiteRemove, |
|
12328 |
|
12329 detach: function(element) { |
|
12330 jqLiteRemove(element, true); |
|
12331 }, |
|
12332 |
|
12333 after: function(element, newElement) { |
|
12334 var index = element, parent = element.parentNode; |
|
12335 newElement = new JQLite(newElement); |
|
12336 |
|
12337 for (var i = 0, ii = newElement.length; i < ii; i++) { |
|
12338 var node = newElement[i]; |
|
12339 parent.insertBefore(node, index.nextSibling); |
|
12340 index = node; |
|
12341 } |
|
12342 }, |
|
12343 |
|
12344 addClass: jqLiteAddClass, |
|
12345 removeClass: jqLiteRemoveClass, |
|
12346 |
|
12347 toggleClass: function(element, selector, condition) { |
|
12348 if (selector) { |
|
12349 forEach(selector.split(' '), function(className){ |
|
12350 var classCondition = condition; |
|
12351 if (isUndefined(classCondition)) { |
|
12352 classCondition = !jqLiteHasClass(element, className); |
|
12353 } |
|
12354 (classCondition ? jqLiteAddClass : jqLiteRemoveClass)(element, className); |
|
12355 }); |
|
12356 } |
|
12357 }, |
|
12358 |
|
12359 parent: function(element) { |
|
12360 var parent = element.parentNode; |
|
12361 return parent && parent.nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT ? parent : null; |
|
12362 }, |
|
12363 |
|
12364 next: function(element) { |
|
12365 return element.nextElementSibling; |
|
12366 }, |
|
12367 |
|
12368 find: function(element, selector) { |
|
12369 if (element.getElementsByTagName) { |
|
12370 return element.getElementsByTagName(selector); |
|
12371 } else { |
|
12372 return []; |
|
12373 } |
|
12374 }, |
|
12375 |
|
12376 clone: jqLiteClone, |
|
12377 |
|
12378 triggerHandler: function(element, event, extraParameters) { |
|
12379 |
|
12380 var dummyEvent, eventFnsCopy, handlerArgs; |
|
12381 var eventName = event.type || event; |
|
12382 var expandoStore = jqLiteExpandoStore(element); |
|
12383 var events = expandoStore && expandoStore.events; |
|
12384 var eventFns = events && events[eventName]; |
|
12385 |
|
12386 if (eventFns) { |
|
12387 // Create a dummy event to pass to the handlers |
|
12388 dummyEvent = { |
|
12389 preventDefault: function() { this.defaultPrevented = true; }, |
|
12390 isDefaultPrevented: function() { return this.defaultPrevented === true; }, |
|
12391 stopImmediatePropagation: function() { this.immediatePropagationStopped = true; }, |
|
12392 isImmediatePropagationStopped: function() { return this.immediatePropagationStopped === true; }, |
|
12393 stopPropagation: noop, |
|
12394 type: eventName, |
|
12395 target: element |
|
12396 }; |
|
12397 |
|
12398 // If a custom event was provided then extend our dummy event with it |
|
12399 if (event.type) { |
|
12400 dummyEvent = extend(dummyEvent, event); |
|
12401 } |
|
12402 |
|
12403 // Copy event handlers in case event handlers array is modified during execution. |
|
12404 eventFnsCopy = shallowCopy(eventFns); |
|
12405 handlerArgs = extraParameters ? [dummyEvent].concat(extraParameters) : [dummyEvent]; |
|
12406 |
|
12407 forEach(eventFnsCopy, function(fn) { |
|
12408 if (!dummyEvent.isImmediatePropagationStopped()) { |
|
12409 fn.apply(element, handlerArgs); |
|
12410 } |
|
12411 }); |
|
12412 } |
|
12413 } |
|
12414 }, function(fn, name){ |
|
12415 /** |
|
12416 * chaining functions |
|
12417 */ |
|
12418 JQLite.prototype[name] = function(arg1, arg2, arg3) { |
|
12419 var value; |
|
12420 |
|
12421 for(var i = 0, ii = this.length; i < ii; i++) { |
|
12422 if (isUndefined(value)) { |
|
12423 value = fn(this[i], arg1, arg2, arg3); |
|
12424 if (isDefined(value)) { |
|
12425 // any function which returns a value needs to be wrapped |
|
12426 value = jqLite(value); |
|
12427 } |
|
12428 } else { |
|
12429 jqLiteAddNodes(value, fn(this[i], arg1, arg2, arg3)); |
|
12430 } |
|
12431 } |
|
12432 return isDefined(value) ? value : this; |
|
12433 }; |
|
12434 |
|
12435 // bind legacy bind/unbind to on/off |
|
12436 JQLite.prototype.bind = JQLite.prototype.on; |
|
12437 JQLite.prototype.unbind = JQLite.prototype.off; |
|
12438 }); |
|
12439 |
|
12440 /** |
|
12441 * Computes a hash of an 'obj'. |
|
12442 * Hash of a: |
|
12443 * string is string |
|
12444 * number is number as string |
|
12445 * object is either result of calling $$hashKey function on the object or uniquely generated id, |
|
12446 * that is also assigned to the $$hashKey property of the object. |
|
12447 * |
|
12448 * @param obj |
|
12449 * @returns {string} hash string such that the same input will have the same hash string. |
|
12450 * The resulting string key is in 'type:hashKey' format. |
|
12451 */ |
|
12452 function hashKey(obj, nextUidFn) { |
|
12453 var key = obj && obj.$$hashKey; |
|
12454 |
|
12455 if (key) { |
|
12456 if (typeof key === 'function') { |
|
12457 key = obj.$$hashKey(); |
|
12458 } |
|
12459 return key; |
|
12460 } |
|
12461 |
|
12462 var objType = typeof obj; |
|
12463 if (objType == 'function' || (objType == 'object' && obj !== null)) { |
|
12464 key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)(); |
|
12465 } else { |
|
12466 key = objType + ':' + obj; |
|
12467 } |
|
12468 |
|
12469 return key; |
|
12470 } |
|
12471 |
|
12472 /** |
|
12473 * HashMap which can use objects as keys |
|
12474 */ |
|
12475 function HashMap(array, isolatedUid) { |
|
12476 if (isolatedUid) { |
|
12477 var uid = 0; |
|
12478 this.nextUid = function() { |
|
12479 return ++uid; |
|
12480 }; |
|
12481 } |
|
12482 forEach(array, this.put, this); |
|
12483 } |
|
12484 HashMap.prototype = { |
|
12485 /** |
|
12486 * Store key value pair |
|
12487 * @param key key to store can be any type |
|
12488 * @param value value to store can be any type |
|
12489 */ |
|
12490 put: function(key, value) { |
|
12491 this[hashKey(key, this.nextUid)] = value; |
|
12492 }, |
|
12493 |
|
12494 /** |
|
12495 * @param key |
|
12496 * @returns {Object} the value for the key |
|
12497 */ |
|
12498 get: function(key) { |
|
12499 return this[hashKey(key, this.nextUid)]; |
|
12500 }, |
|
12501 |
|
12502 /** |
|
12503 * Remove the key/value pair |
|
12504 * @param key |
|
12505 */ |
|
12506 remove: function(key) { |
|
12507 var value = this[key = hashKey(key, this.nextUid)]; |
|
12508 delete this[key]; |
|
12509 return value; |
|
12510 } |
|
12511 }; |
|
12512 |
|
12513 /** |
|
12514 * @ngdoc function |
|
12515 * @module ng |
|
12516 * @name angular.injector |
|
12517 * @kind function |
|
12518 * |
|
12519 * @description |
|
12520 * Creates an injector object that can be used for retrieving services as well as for |
|
12521 * dependency injection (see {@link guide/di dependency injection}). |
|
12522 * |
|
12523 |
|
12524 * @param {Array.<string|Function>} modules A list of module functions or their aliases. See |
|
12525 * {@link angular.module}. The `ng` module must be explicitly added. |
|
12526 * @returns {function()} Injector object. See {@link auto.$injector $injector}. |
|
12527 * |
|
12528 * @example |
|
12529 * Typical usage |
|
12530 * ```js |
|
12531 * // create an injector |
|
12532 * var $injector = angular.injector(['ng']); |
|
12533 * |
|
12534 * // use the injector to kick off your application |
|
12535 * // use the type inference to auto inject arguments, or use implicit injection |
|
12536 * $injector.invoke(function($rootScope, $compile, $document) { |
|
12537 * $compile($document)($rootScope); |
|
12538 * $rootScope.$digest(); |
|
12539 * }); |
|
12540 * ``` |
|
12541 * |
|
12542 * Sometimes you want to get access to the injector of a currently running Angular app |
|
12543 * from outside Angular. Perhaps, you want to inject and compile some markup after the |
|
12544 * application has been bootstrapped. You can do this using the extra `injector()` added |
|
12545 * to JQuery/jqLite elements. See {@link angular.element}. |
|
12546 * |
|
12547 * *This is fairly rare but could be the case if a third party library is injecting the |
|
12548 * markup.* |
|
12549 * |
|
12550 * In the following example a new block of HTML containing a `ng-controller` |
|
12551 * directive is added to the end of the document body by JQuery. We then compile and link |
|
12552 * it into the current AngularJS scope. |
|
12553 * |
|
12554 * ```js |
|
12555 * var $div = $('<div ng-controller="MyCtrl">{{content.label}}</div>'); |
|
12556 * $(document.body).append($div); |
|
12557 * |
|
12558 * angular.element(document).injector().invoke(function($compile) { |
|
12559 * var scope = angular.element($div).scope(); |
|
12560 * $compile($div)(scope); |
|
12561 * }); |
|
12562 * ``` |
|
12563 */ |
|
12564 |
|
12565 |
|
12566 /** |
|
12567 * @ngdoc module |
|
12568 * @name auto |
|
12569 * @description |
|
12570 * |
|
12571 * Implicit module which gets automatically added to each {@link auto.$injector $injector}. |
|
12572 */ |
|
12573 |
|
12574 var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; |
|
12575 var FN_ARG_SPLIT = /,/; |
|
12576 var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; |
|
12577 var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; |
|
12578 var $injectorMinErr = minErr('$injector'); |
|
12579 |
|
12580 function anonFn(fn) { |
|
12581 // For anonymous functions, showing at the very least the function signature can help in |
|
12582 // debugging. |
|
12583 var fnText = fn.toString().replace(STRIP_COMMENTS, ''), |
|
12584 args = fnText.match(FN_ARGS); |
|
12585 if (args) { |
|
12586 return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')'; |
|
12587 } |
|
12588 return 'fn'; |
|
12589 } |
|
12590 |
|
12591 function annotate(fn, strictDi, name) { |
|
12592 var $inject, |
|
12593 fnText, |
|
12594 argDecl, |
|
12595 last; |
|
12596 |
|
12597 if (typeof fn === 'function') { |
|
12598 if (!($inject = fn.$inject)) { |
|
12599 $inject = []; |
|
12600 if (fn.length) { |
|
12601 if (strictDi) { |
|
12602 if (!isString(name) || !name) { |
|
12603 name = fn.name || anonFn(fn); |
|
12604 } |
|
12605 throw $injectorMinErr('strictdi', |
|
12606 '{0} is not using explicit annotation and cannot be invoked in strict mode', name); |
|
12607 } |
|
12608 fnText = fn.toString().replace(STRIP_COMMENTS, ''); |
|
12609 argDecl = fnText.match(FN_ARGS); |
|
12610 forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { |
|
12611 arg.replace(FN_ARG, function(all, underscore, name) { |
|
12612 $inject.push(name); |
|
12613 }); |
|
12614 }); |
|
12615 } |
|
12616 fn.$inject = $inject; |
|
12617 } |
|
12618 } else if (isArray(fn)) { |
|
12619 last = fn.length - 1; |
|
12620 assertArgFn(fn[last], 'fn'); |
|
12621 $inject = fn.slice(0, last); |
|
12622 } else { |
|
12623 assertArgFn(fn, 'fn', true); |
|
12624 } |
|
12625 return $inject; |
|
12626 } |
|
12627 |
|
12628 /////////////////////////////////////// |
|
12629 |
|
12630 /** |
|
12631 * @ngdoc service |
|
12632 * @name $injector |
|
12633 * |
|
12634 * @description |
|
12635 * |
|
12636 * `$injector` is used to retrieve object instances as defined by |
|
12637 * {@link auto.$provide provider}, instantiate types, invoke methods, |
|
12638 * and load modules. |
|
12639 * |
|
12640 * The following always holds true: |
|
12641 * |
|
12642 * ```js |
|
12643 * var $injector = angular.injector(); |
|
12644 * expect($injector.get('$injector')).toBe($injector); |
|
12645 * expect($injector.invoke(function($injector) { |
|
12646 * return $injector; |
|
12647 * })).toBe($injector); |
|
12648 * ``` |
|
12649 * |
|
12650 * # Injection Function Annotation |
|
12651 * |
|
12652 * JavaScript does not have annotations, and annotations are needed for dependency injection. The |
|
12653 * following are all valid ways of annotating function with injection arguments and are equivalent. |
|
12654 * |
|
12655 * ```js |
|
12656 * // inferred (only works if code not minified/obfuscated) |
|
12657 * $injector.invoke(function(serviceA){}); |
|
12658 * |
|
12659 * // annotated |
|
12660 * function explicit(serviceA) {}; |
|
12661 * explicit.$inject = ['serviceA']; |
|
12662 * $injector.invoke(explicit); |
|
12663 * |
|
12664 * // inline |
|
12665 * $injector.invoke(['serviceA', function(serviceA){}]); |
|
12666 * ``` |
|
12667 * |
|
12668 * ## Inference |
|
12669 * |
|
12670 * In JavaScript calling `toString()` on a function returns the function definition. The definition |
|
12671 * can then be parsed and the function arguments can be extracted. *NOTE:* This does not work with |
|
12672 * minification, and obfuscation tools since these tools change the argument names. |
|
12673 * |
|
12674 * ## `$inject` Annotation |
|
12675 * By adding an `$inject` property onto a function the injection parameters can be specified. |
|
12676 * |
|
12677 * ## Inline |
|
12678 * As an array of injection names, where the last item in the array is the function to call. |
|
12679 */ |
|
12680 |
|
12681 /** |
|
12682 * @ngdoc method |
|
12683 * @name $injector#get |
|
12684 * |
|
12685 * @description |
|
12686 * Return an instance of the service. |
|
12687 * |
|
12688 * @param {string} name The name of the instance to retrieve. |
|
12689 * @return {*} The instance. |
|
12690 */ |
|
12691 |
|
12692 /** |
|
12693 * @ngdoc method |
|
12694 * @name $injector#invoke |
|
12695 * |
|
12696 * @description |
|
12697 * Invoke the method and supply the method arguments from the `$injector`. |
|
12698 * |
|
12699 * @param {!Function} fn The function to invoke. Function parameters are injected according to the |
|
12700 * {@link guide/di $inject Annotation} rules. |
|
12701 * @param {Object=} self The `this` for the invoked method. |
|
12702 * @param {Object=} locals Optional object. If preset then any argument names are read from this |
|
12703 * object first, before the `$injector` is consulted. |
|
12704 * @returns {*} the value returned by the invoked `fn` function. |
|
12705 */ |
|
12706 |
|
12707 /** |
|
12708 * @ngdoc method |
|
12709 * @name $injector#has |
|
12710 * |
|
12711 * @description |
|
12712 * Allows the user to query if the particular service exists. |
|
12713 * |
|
12714 * @param {string} name Name of the service to query. |
|
12715 * @returns {boolean} `true` if injector has given service. |
|
12716 */ |
|
12717 |
|
12718 /** |
|
12719 * @ngdoc method |
|
12720 * @name $injector#instantiate |
|
12721 * @description |
|
12722 * Create a new instance of JS type. The method takes a constructor function, invokes the new |
|
12723 * operator, and supplies all of the arguments to the constructor function as specified by the |
|
12724 * constructor annotation. |
|
12725 * |
|
12726 * @param {Function} Type Annotated constructor function. |
|
12727 * @param {Object=} locals Optional object. If preset then any argument names are read from this |
|
12728 * object first, before the `$injector` is consulted. |
|
12729 * @returns {Object} new instance of `Type`. |
|
12730 */ |
|
12731 |
|
12732 /** |
|
12733 * @ngdoc method |
|
12734 * @name $injector#annotate |
|
12735 * |
|
12736 * @description |
|
12737 * Returns an array of service names which the function is requesting for injection. This API is |
|
12738 * used by the injector to determine which services need to be injected into the function when the |
|
12739 * function is invoked. There are three ways in which the function can be annotated with the needed |
|
12740 * dependencies. |
|
12741 * |
|
12742 * # Argument names |
|
12743 * |
|
12744 * The simplest form is to extract the dependencies from the arguments of the function. This is done |
|
12745 * by converting the function into a string using `toString()` method and extracting the argument |
|
12746 * names. |
|
12747 * ```js |
|
12748 * // Given |
|
12749 * function MyController($scope, $route) { |
|
12750 * // ... |
|
12751 * } |
|
12752 * |
|
12753 * // Then |
|
12754 * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); |
|
12755 * ``` |
|
12756 * |
|
12757 * This method does not work with code minification / obfuscation. For this reason the following |
|
12758 * annotation strategies are supported. |
|
12759 * |
|
12760 * # The `$inject` property |
|
12761 * |
|
12762 * If a function has an `$inject` property and its value is an array of strings, then the strings |
|
12763 * represent names of services to be injected into the function. |
|
12764 * ```js |
|
12765 * // Given |
|
12766 * var MyController = function(obfuscatedScope, obfuscatedRoute) { |
|
12767 * // ... |
|
12768 * } |
|
12769 * // Define function dependencies |
|
12770 * MyController['$inject'] = ['$scope', '$route']; |
|
12771 * |
|
12772 * // Then |
|
12773 * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); |
|
12774 * ``` |
|
12775 * |
|
12776 * # The array notation |
|
12777 * |
|
12778 * It is often desirable to inline Injected functions and that's when setting the `$inject` property |
|
12779 * is very inconvenient. In these situations using the array notation to specify the dependencies in |
|
12780 * a way that survives minification is a better choice: |
|
12781 * |
|
12782 * ```js |
|
12783 * // We wish to write this (not minification / obfuscation safe) |
|
12784 * injector.invoke(function($compile, $rootScope) { |
|
12785 * // ... |
|
12786 * }); |
|
12787 * |
|
12788 * // We are forced to write break inlining |
|
12789 * var tmpFn = function(obfuscatedCompile, obfuscatedRootScope) { |
|
12790 * // ... |
|
12791 * }; |
|
12792 * tmpFn.$inject = ['$compile', '$rootScope']; |
|
12793 * injector.invoke(tmpFn); |
|
12794 * |
|
12795 * // To better support inline function the inline annotation is supported |
|
12796 * injector.invoke(['$compile', '$rootScope', function(obfCompile, obfRootScope) { |
|
12797 * // ... |
|
12798 * }]); |
|
12799 * |
|
12800 * // Therefore |
|
12801 * expect(injector.annotate( |
|
12802 * ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}]) |
|
12803 * ).toEqual(['$compile', '$rootScope']); |
|
12804 * ``` |
|
12805 * |
|
12806 * @param {Function|Array.<string|Function>} fn Function for which dependent service names need to |
|
12807 * be retrieved as described above. |
|
12808 * |
|
12809 * @returns {Array.<string>} The names of the services which the function requires. |
|
12810 */ |
|
12811 |
|
12812 |
|
12813 |
|
12814 |
|
12815 /** |
|
12816 * @ngdoc service |
|
12817 * @name $provide |
|
12818 * |
|
12819 * @description |
|
12820 * |
|
12821 * The {@link auto.$provide $provide} service has a number of methods for registering components |
|
12822 * with the {@link auto.$injector $injector}. Many of these functions are also exposed on |
|
12823 * {@link angular.Module}. |
|
12824 * |
|
12825 * An Angular **service** is a singleton object created by a **service factory**. These **service |
|
12826 * factories** are functions which, in turn, are created by a **service provider**. |
|
12827 * The **service providers** are constructor functions. When instantiated they must contain a |
|
12828 * property called `$get`, which holds the **service factory** function. |
|
12829 * |
|
12830 * When you request a service, the {@link auto.$injector $injector} is responsible for finding the |
|
12831 * correct **service provider**, instantiating it and then calling its `$get` **service factory** |
|
12832 * function to get the instance of the **service**. |
|
12833 * |
|
12834 * Often services have no configuration options and there is no need to add methods to the service |
|
12835 * provider. The provider will be no more than a constructor function with a `$get` property. For |
|
12836 * these cases the {@link auto.$provide $provide} service has additional helper methods to register |
|
12837 * services without specifying a provider. |
|
12838 * |
|
12839 * * {@link auto.$provide#provider provider(provider)} - registers a **service provider** with the |
|
12840 * {@link auto.$injector $injector} |
|
12841 * * {@link auto.$provide#constant constant(obj)} - registers a value/object that can be accessed by |
|
12842 * providers and services. |
|
12843 * * {@link auto.$provide#value value(obj)} - registers a value/object that can only be accessed by |
|
12844 * services, not providers. |
|
12845 * * {@link auto.$provide#factory factory(fn)} - registers a service **factory function**, `fn`, |
|
12846 * that will be wrapped in a **service provider** object, whose `$get` property will contain the |
|
12847 * given factory function. |
|
12848 * * {@link auto.$provide#service service(class)} - registers a **constructor function**, `class` |
|
12849 * that will be wrapped in a **service provider** object, whose `$get` property will instantiate |
|
12850 * a new object using the given constructor function. |
|
12851 * |
|
12852 * See the individual methods for more information and examples. |
|
12853 */ |
|
12854 |
|
12855 /** |
|
12856 * @ngdoc method |
|
12857 * @name $provide#provider |
|
12858 * @description |
|
12859 * |
|
12860 * Register a **provider function** with the {@link auto.$injector $injector}. Provider functions |
|
12861 * are constructor functions, whose instances are responsible for "providing" a factory for a |
|
12862 * service. |
|
12863 * |
|
12864 * Service provider names start with the name of the service they provide followed by `Provider`. |
|
12865 * For example, the {@link ng.$log $log} service has a provider called |
|
12866 * {@link ng.$logProvider $logProvider}. |
|
12867 * |
|
12868 * Service provider objects can have additional methods which allow configuration of the provider |
|
12869 * and its service. Importantly, you can configure what kind of service is created by the `$get` |
|
12870 * method, or how that service will act. For example, the {@link ng.$logProvider $logProvider} has a |
|
12871 * method {@link ng.$logProvider#debugEnabled debugEnabled} |
|
12872 * which lets you specify whether the {@link ng.$log $log} service will log debug messages to the |
|
12873 * console or not. |
|
12874 * |
|
12875 * @param {string} name The name of the instance. NOTE: the provider will be available under `name + |
|
12876 'Provider'` key. |
|
12877 * @param {(Object|function())} provider If the provider is: |
|
12878 * |
|
12879 * - `Object`: then it should have a `$get` method. The `$get` method will be invoked using |
|
12880 * {@link auto.$injector#invoke $injector.invoke()} when an instance needs to be created. |
|
12881 * - `Constructor`: a new instance of the provider will be created using |
|
12882 * {@link auto.$injector#instantiate $injector.instantiate()}, then treated as `object`. |
|
12883 * |
|
12884 * @returns {Object} registered provider instance |
|
12885 |
|
12886 * @example |
|
12887 * |
|
12888 * The following example shows how to create a simple event tracking service and register it using |
|
12889 * {@link auto.$provide#provider $provide.provider()}. |
|
12890 * |
|
12891 * ```js |
|
12892 * // Define the eventTracker provider |
|
12893 * function EventTrackerProvider() { |
|
12894 * var trackingUrl = '/track'; |
|
12895 * |
|
12896 * // A provider method for configuring where the tracked events should been saved |
|
12897 * this.setTrackingUrl = function(url) { |
|
12898 * trackingUrl = url; |
|
12899 * }; |
|
12900 * |
|
12901 * // The service factory function |
|
12902 * this.$get = ['$http', function($http) { |
|
12903 * var trackedEvents = {}; |
|
12904 * return { |
|
12905 * // Call this to track an event |
|
12906 * event: function(event) { |
|
12907 * var count = trackedEvents[event] || 0; |
|
12908 * count += 1; |
|
12909 * trackedEvents[event] = count; |
|
12910 * return count; |
|
12911 * }, |
|
12912 * // Call this to save the tracked events to the trackingUrl |
|
12913 * save: function() { |
|
12914 * $http.post(trackingUrl, trackedEvents); |
|
12915 * } |
|
12916 * }; |
|
12917 * }]; |
|
12918 * } |
|
12919 * |
|
12920 * describe('eventTracker', function() { |
|
12921 * var postSpy; |
|
12922 * |
|
12923 * beforeEach(module(function($provide) { |
|
12924 * // Register the eventTracker provider |
|
12925 * $provide.provider('eventTracker', EventTrackerProvider); |
|
12926 * })); |
|
12927 * |
|
12928 * beforeEach(module(function(eventTrackerProvider) { |
|
12929 * // Configure eventTracker provider |
|
12930 * eventTrackerProvider.setTrackingUrl('/custom-track'); |
|
12931 * })); |
|
12932 * |
|
12933 * it('tracks events', inject(function(eventTracker) { |
|
12934 * expect(eventTracker.event('login')).toEqual(1); |
|
12935 * expect(eventTracker.event('login')).toEqual(2); |
|
12936 * })); |
|
12937 * |
|
12938 * it('saves to the tracking url', inject(function(eventTracker, $http) { |
|
12939 * postSpy = spyOn($http, 'post'); |
|
12940 * eventTracker.event('login'); |
|
12941 * eventTracker.save(); |
|
12942 * expect(postSpy).toHaveBeenCalled(); |
|
12943 * expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track'); |
|
12944 * expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track'); |
|
12945 * expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 }); |
|
12946 * })); |
|
12947 * }); |
|
12948 * ``` |
|
12949 */ |
|
12950 |
|
12951 /** |
|
12952 * @ngdoc method |
|
12953 * @name $provide#factory |
|
12954 * @description |
|
12955 * |
|
12956 * Register a **service factory**, which will be called to return the service instance. |
|
12957 * This is short for registering a service where its provider consists of only a `$get` property, |
|
12958 * which is the given service factory function. |
|
12959 * You should use {@link auto.$provide#factory $provide.factory(getFn)} if you do not need to |
|
12960 * configure your service in a provider. |
|
12961 * |
|
12962 * @param {string} name The name of the instance. |
|
12963 * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand |
|
12964 * for `$provide.provider(name, {$get: $getFn})`. |
|
12965 * @returns {Object} registered provider instance |
|
12966 * |
|
12967 * @example |
|
12968 * Here is an example of registering a service |
|
12969 * ```js |
|
12970 * $provide.factory('ping', ['$http', function($http) { |
|
12971 * return function ping() { |
|
12972 * return $http.send('/ping'); |
|
12973 * }; |
|
12974 * }]); |
|
12975 * ``` |
|
12976 * You would then inject and use this service like this: |
|
12977 * ```js |
|
12978 * someModule.controller('Ctrl', ['ping', function(ping) { |
|
12979 * ping(); |
|
12980 * }]); |
|
12981 * ``` |
|
12982 */ |
|
12983 |
|
12984 |
|
12985 /** |
|
12986 * @ngdoc method |
|
12987 * @name $provide#service |
|
12988 * @description |
|
12989 * |
|
12990 * Register a **service constructor**, which will be invoked with `new` to create the service |
|
12991 * instance. |
|
12992 * This is short for registering a service where its provider's `$get` property is the service |
|
12993 * constructor function that will be used to instantiate the service instance. |
|
12994 * |
|
12995 * You should use {@link auto.$provide#service $provide.service(class)} if you define your service |
|
12996 * as a type/class. |
|
12997 * |
|
12998 * @param {string} name The name of the instance. |
|
12999 * @param {Function} constructor A class (constructor function) that will be instantiated. |
|
13000 * @returns {Object} registered provider instance |
|
13001 * |
|
13002 * @example |
|
13003 * Here is an example of registering a service using |
|
13004 * {@link auto.$provide#service $provide.service(class)}. |
|
13005 * ```js |
|
13006 * var Ping = function($http) { |
|
13007 * this.$http = $http; |
|
13008 * }; |
|
13009 * |
|
13010 * Ping.$inject = ['$http']; |
|
13011 * |
|
13012 * Ping.prototype.send = function() { |
|
13013 * return this.$http.get('/ping'); |
|
13014 * }; |
|
13015 * $provide.service('ping', Ping); |
|
13016 * ``` |
|
13017 * You would then inject and use this service like this: |
|
13018 * ```js |
|
13019 * someModule.controller('Ctrl', ['ping', function(ping) { |
|
13020 * ping.send(); |
|
13021 * }]); |
|
13022 * ``` |
|
13023 */ |
|
13024 |
|
13025 |
|
13026 /** |
|
13027 * @ngdoc method |
|
13028 * @name $provide#value |
|
13029 * @description |
|
13030 * |
|
13031 * Register a **value service** with the {@link auto.$injector $injector}, such as a string, a |
|
13032 * number, an array, an object or a function. This is short for registering a service where its |
|
13033 * provider's `$get` property is a factory function that takes no arguments and returns the **value |
|
13034 * service**. |
|
13035 * |
|
13036 * Value services are similar to constant services, except that they cannot be injected into a |
|
13037 * module configuration function (see {@link angular.Module#config}) but they can be overridden by |
|
13038 * an Angular |
|
13039 * {@link auto.$provide#decorator decorator}. |
|
13040 * |
|
13041 * @param {string} name The name of the instance. |
|
13042 * @param {*} value The value. |
|
13043 * @returns {Object} registered provider instance |
|
13044 * |
|
13045 * @example |
|
13046 * Here are some examples of creating value services. |
|
13047 * ```js |
|
13048 * $provide.value('ADMIN_USER', 'admin'); |
|
13049 * |
|
13050 * $provide.value('RoleLookup', { admin: 0, writer: 1, reader: 2 }); |
|
13051 * |
|
13052 * $provide.value('halfOf', function(value) { |
|
13053 * return value / 2; |
|
13054 * }); |
|
13055 * ``` |
|
13056 */ |
|
13057 |
|
13058 |
|
13059 /** |
|
13060 * @ngdoc method |
|
13061 * @name $provide#constant |
|
13062 * @description |
|
13063 * |
|
13064 * Register a **constant service**, such as a string, a number, an array, an object or a function, |
|
13065 * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it can be |
|
13066 * injected into a module configuration function (see {@link angular.Module#config}) and it cannot |
|
13067 * be overridden by an Angular {@link auto.$provide#decorator decorator}. |
|
13068 * |
|
13069 * @param {string} name The name of the constant. |
|
13070 * @param {*} value The constant value. |
|
13071 * @returns {Object} registered instance |
|
13072 * |
|
13073 * @example |
|
13074 * Here a some examples of creating constants: |
|
13075 * ```js |
|
13076 * $provide.constant('SHARD_HEIGHT', 306); |
|
13077 * |
|
13078 * $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']); |
|
13079 * |
|
13080 * $provide.constant('double', function(value) { |
|
13081 * return value * 2; |
|
13082 * }); |
|
13083 * ``` |
|
13084 */ |
|
13085 |
|
13086 |
|
13087 /** |
|
13088 * @ngdoc method |
|
13089 * @name $provide#decorator |
|
13090 * @description |
|
13091 * |
|
13092 * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator |
|
13093 * intercepts the creation of a service, allowing it to override or modify the behaviour of the |
|
13094 * service. The object returned by the decorator may be the original service, or a new service |
|
13095 * object which replaces or wraps and delegates to the original service. |
|
13096 * |
|
13097 * @param {string} name The name of the service to decorate. |
|
13098 * @param {function()} decorator This function will be invoked when the service needs to be |
|
13099 * instantiated and should return the decorated service instance. The function is called using |
|
13100 * the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable. |
|
13101 * Local injection arguments: |
|
13102 * |
|
13103 * * `$delegate` - The original service instance, which can be monkey patched, configured, |
|
13104 * decorated or delegated to. |
|
13105 * |
|
13106 * @example |
|
13107 * Here we decorate the {@link ng.$log $log} service to convert warnings to errors by intercepting |
|
13108 * calls to {@link ng.$log#error $log.warn()}. |
|
13109 * ```js |
|
13110 * $provide.decorator('$log', ['$delegate', function($delegate) { |
|
13111 * $delegate.warn = $delegate.error; |
|
13112 * return $delegate; |
|
13113 * }]); |
|
13114 * ``` |
|
13115 */ |
|
13116 |
|
13117 |
|
13118 function createInjector(modulesToLoad, strictDi) { |
|
13119 strictDi = (strictDi === true); |
|
13120 var INSTANTIATING = {}, |
|
13121 providerSuffix = 'Provider', |
|
13122 path = [], |
|
13123 loadedModules = new HashMap([], true), |
|
13124 providerCache = { |
|
13125 $provide: { |
|
13126 provider: supportObject(provider), |
|
13127 factory: supportObject(factory), |
|
13128 service: supportObject(service), |
|
13129 value: supportObject(value), |
|
13130 constant: supportObject(constant), |
|
13131 decorator: decorator |
|
13132 } |
|
13133 }, |
|
13134 providerInjector = (providerCache.$injector = |
|
13135 createInternalInjector(providerCache, function() { |
|
13136 throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); |
|
13137 })), |
|
13138 instanceCache = {}, |
|
13139 instanceInjector = (instanceCache.$injector = |
|
13140 createInternalInjector(instanceCache, function(servicename) { |
|
13141 var provider = providerInjector.get(servicename + providerSuffix); |
|
13142 return instanceInjector.invoke(provider.$get, provider, undefined, servicename); |
|
13143 })); |
|
13144 |
|
13145 |
|
13146 forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); |
|
13147 |
|
13148 return instanceInjector; |
|
13149 |
|
13150 //////////////////////////////////// |
|
13151 // $provider |
|
13152 //////////////////////////////////// |
|
13153 |
|
13154 function supportObject(delegate) { |
|
13155 return function(key, value) { |
|
13156 if (isObject(key)) { |
|
13157 forEach(key, reverseParams(delegate)); |
|
13158 } else { |
|
13159 return delegate(key, value); |
|
13160 } |
|
13161 }; |
|
13162 } |
|
13163 |
|
13164 function provider(name, provider_) { |
|
13165 assertNotHasOwnProperty(name, 'service'); |
|
13166 if (isFunction(provider_) || isArray(provider_)) { |
|
13167 provider_ = providerInjector.instantiate(provider_); |
|
13168 } |
|
13169 if (!provider_.$get) { |
|
13170 throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); |
|
13171 } |
|
13172 return providerCache[name + providerSuffix] = provider_; |
|
13173 } |
|
13174 |
|
13175 function enforceReturnValue(name, factory) { |
|
13176 return function enforcedReturnValue() { |
|
13177 var result = instanceInjector.invoke(factory); |
|
13178 if (isUndefined(result)) { |
|
13179 throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name); |
|
13180 } |
|
13181 return result; |
|
13182 }; |
|
13183 } |
|
13184 |
|
13185 function factory(name, factoryFn, enforce) { |
|
13186 return provider(name, { |
|
13187 $get: enforce !== false ? enforceReturnValue(name, factoryFn) : factoryFn |
|
13188 }); |
|
13189 } |
|
13190 |
|
13191 function service(name, constructor) { |
|
13192 return factory(name, ['$injector', function($injector) { |
|
13193 return $injector.instantiate(constructor); |
|
13194 }]); |
|
13195 } |
|
13196 |
|
13197 function value(name, val) { return factory(name, valueFn(val), false); } |
|
13198 |
|
13199 function constant(name, value) { |
|
13200 assertNotHasOwnProperty(name, 'constant'); |
|
13201 providerCache[name] = value; |
|
13202 instanceCache[name] = value; |
|
13203 } |
|
13204 |
|
13205 function decorator(serviceName, decorFn) { |
|
13206 var origProvider = providerInjector.get(serviceName + providerSuffix), |
|
13207 orig$get = origProvider.$get; |
|
13208 |
|
13209 origProvider.$get = function() { |
|
13210 var origInstance = instanceInjector.invoke(orig$get, origProvider); |
|
13211 return instanceInjector.invoke(decorFn, null, {$delegate: origInstance}); |
|
13212 }; |
|
13213 } |
|
13214 |
|
13215 //////////////////////////////////// |
|
13216 // Module Loading |
|
13217 //////////////////////////////////// |
|
13218 function loadModules(modulesToLoad){ |
|
13219 var runBlocks = [], moduleFn; |
|
13220 forEach(modulesToLoad, function(module) { |
|
13221 if (loadedModules.get(module)) return; |
|
13222 loadedModules.put(module, true); |
|
13223 |
|
13224 function runInvokeQueue(queue) { |
|
13225 var i, ii; |
|
13226 for(i = 0, ii = queue.length; i < ii; i++) { |
|
13227 var invokeArgs = queue[i], |
|
13228 provider = providerInjector.get(invokeArgs[0]); |
|
13229 |
|
13230 provider[invokeArgs[1]].apply(provider, invokeArgs[2]); |
|
13231 } |
|
13232 } |
|
13233 |
|
13234 try { |
|
13235 if (isString(module)) { |
|
13236 moduleFn = angularModule(module); |
|
13237 runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); |
|
13238 runInvokeQueue(moduleFn._invokeQueue); |
|
13239 runInvokeQueue(moduleFn._configBlocks); |
|
13240 } else if (isFunction(module)) { |
|
13241 runBlocks.push(providerInjector.invoke(module)); |
|
13242 } else if (isArray(module)) { |
|
13243 runBlocks.push(providerInjector.invoke(module)); |
|
13244 } else { |
|
13245 assertArgFn(module, 'module'); |
|
13246 } |
|
13247 } catch (e) { |
|
13248 if (isArray(module)) { |
|
13249 module = module[module.length - 1]; |
|
13250 } |
|
13251 if (e.message && e.stack && e.stack.indexOf(e.message) == -1) { |
|
13252 // Safari & FF's stack traces don't contain error.message content |
|
13253 // unlike those of Chrome and IE |
|
13254 // So if stack doesn't contain message, we create a new string that contains both. |
|
13255 // Since error.stack is read-only in Safari, I'm overriding e and not e.stack here. |
|
13256 /* jshint -W022 */ |
|
13257 e = e.message + '\n' + e.stack; |
|
13258 } |
|
13259 throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", |
|
13260 module, e.stack || e.message || e); |
|
13261 } |
|
13262 }); |
|
13263 return runBlocks; |
|
13264 } |
|
13265 |
|
13266 //////////////////////////////////// |
|
13267 // internal Injector |
|
13268 //////////////////////////////////// |
|
13269 |
|
13270 function createInternalInjector(cache, factory) { |
|
13271 |
|
13272 function getService(serviceName) { |
|
13273 if (cache.hasOwnProperty(serviceName)) { |
|
13274 if (cache[serviceName] === INSTANTIATING) { |
|
13275 throw $injectorMinErr('cdep', 'Circular dependency found: {0}', |
|
13276 serviceName + ' <- ' + path.join(' <- ')); |
|
13277 } |
|
13278 return cache[serviceName]; |
|
13279 } else { |
|
13280 try { |
|
13281 path.unshift(serviceName); |
|
13282 cache[serviceName] = INSTANTIATING; |
|
13283 return cache[serviceName] = factory(serviceName); |
|
13284 } catch (err) { |
|
13285 if (cache[serviceName] === INSTANTIATING) { |
|
13286 delete cache[serviceName]; |
|
13287 } |
|
13288 throw err; |
|
13289 } finally { |
|
13290 path.shift(); |
|
13291 } |
|
13292 } |
|
13293 } |
|
13294 |
|
13295 function invoke(fn, self, locals, serviceName) { |
|
13296 if (typeof locals === 'string') { |
|
13297 serviceName = locals; |
|
13298 locals = null; |
|
13299 } |
|
13300 |
|
13301 var args = [], |
|
13302 $inject = annotate(fn, strictDi, serviceName), |
|
13303 length, i, |
|
13304 key; |
|
13305 |
|
13306 for(i = 0, length = $inject.length; i < length; i++) { |
|
13307 key = $inject[i]; |
|
13308 if (typeof key !== 'string') { |
|
13309 throw $injectorMinErr('itkn', |
|
13310 'Incorrect injection token! Expected service name as string, got {0}', key); |
|
13311 } |
|
13312 args.push( |
|
13313 locals && locals.hasOwnProperty(key) |
|
13314 ? locals[key] |
|
13315 : getService(key) |
|
13316 ); |
|
13317 } |
|
13318 if (isArray(fn)) { |
|
13319 fn = fn[length]; |
|
13320 } |
|
13321 |
|
13322 // http://jsperf.com/angularjs-invoke-apply-vs-switch |
|
13323 // #5388 |
|
13324 return fn.apply(self, args); |
|
13325 } |
|
13326 |
|
13327 function instantiate(Type, locals, serviceName) { |
|
13328 var Constructor = function() {}, |
|
13329 instance, returnedValue; |
|
13330 |
|
13331 // Check if Type is annotated and use just the given function at n-1 as parameter |
|
13332 // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); |
|
13333 Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; |
|
13334 instance = new Constructor(); |
|
13335 returnedValue = invoke(Type, instance, locals, serviceName); |
|
13336 |
|
13337 return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; |
|
13338 } |
|
13339 |
|
13340 return { |
|
13341 invoke: invoke, |
|
13342 instantiate: instantiate, |
|
13343 get: getService, |
|
13344 annotate: annotate, |
|
13345 has: function(name) { |
|
13346 return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); |
|
13347 } |
|
13348 }; |
|
13349 } |
|
13350 } |
|
13351 |
|
13352 createInjector.$$annotate = annotate; |
|
13353 |
|
13354 /** |
|
13355 * @ngdoc service |
|
13356 * @name $anchorScroll |
|
13357 * @kind function |
|
13358 * @requires $window |
|
13359 * @requires $location |
|
13360 * @requires $rootScope |
|
13361 * |
|
13362 * @description |
|
13363 * When called, it checks current value of `$location.hash()` and scrolls to the related element, |
|
13364 * according to rules specified in |
|
13365 * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). |
|
13366 * |
|
13367 * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. |
|
13368 * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. |
|
13369 * |
|
13370 * @example |
|
13371 <example module="anchorScrollExample"> |
|
13372 <file name="index.html"> |
|
13373 <div id="scrollArea" ng-controller="ScrollController"> |
|
13374 <a ng-click="gotoBottom()">Go to bottom</a> |
|
13375 <a id="bottom"></a> You're at the bottom! |
|
13376 </div> |
|
13377 </file> |
|
13378 <file name="script.js"> |
|
13379 angular.module('anchorScrollExample', []) |
|
13380 .controller('ScrollController', ['$scope', '$location', '$anchorScroll', |
|
13381 function ($scope, $location, $anchorScroll) { |
|
13382 $scope.gotoBottom = function() { |
|
13383 // set the location.hash to the id of |
|
13384 // the element you wish to scroll to. |
|
13385 $location.hash('bottom'); |
|
13386 |
|
13387 // call $anchorScroll() |
|
13388 $anchorScroll(); |
|
13389 }; |
|
13390 }]); |
|
13391 </file> |
|
13392 <file name="style.css"> |
|
13393 #scrollArea { |
|
13394 height: 350px; |
|
13395 overflow: auto; |
|
13396 } |
|
13397 |
|
13398 #bottom { |
|
13399 display: block; |
|
13400 margin-top: 2000px; |
|
13401 } |
|
13402 </file> |
|
13403 </example> |
|
13404 */ |
|
13405 function $AnchorScrollProvider() { |
|
13406 |
|
13407 var autoScrollingEnabled = true; |
|
13408 |
|
13409 this.disableAutoScrolling = function() { |
|
13410 autoScrollingEnabled = false; |
|
13411 }; |
|
13412 |
|
13413 this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { |
|
13414 var document = $window.document; |
|
13415 |
|
13416 // helper function to get first anchor from a NodeList |
|
13417 // can't use filter.filter, as it accepts only instances of Array |
|
13418 // and IE can't convert NodeList to an array using [].slice |
|
13419 // TODO(vojta): use filter if we change it to accept lists as well |
|
13420 function getFirstAnchor(list) { |
|
13421 var result = null; |
|
13422 forEach(list, function(element) { |
|
13423 if (!result && nodeName_(element) === 'a') result = element; |
|
13424 }); |
|
13425 return result; |
|
13426 } |
|
13427 |
|
13428 function scroll() { |
|
13429 var hash = $location.hash(), elm; |
|
13430 |
|
13431 // empty hash, scroll to the top of the page |
|
13432 if (!hash) $window.scrollTo(0, 0); |
|
13433 |
|
13434 // element with given id |
|
13435 else if ((elm = document.getElementById(hash))) elm.scrollIntoView(); |
|
13436 |
|
13437 // first anchor with given name :-D |
|
13438 else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView(); |
|
13439 |
|
13440 // no element and hash == 'top', scroll to the top of the page |
|
13441 else if (hash === 'top') $window.scrollTo(0, 0); |
|
13442 } |
|
13443 |
|
13444 // does not scroll when user clicks on anchor link that is currently on |
|
13445 // (no url change, no $location.hash() change), browser native does scroll |
|
13446 if (autoScrollingEnabled) { |
|
13447 $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, |
|
13448 function autoScrollWatchAction(newVal, oldVal) { |
|
13449 // skip the initial scroll if $location.hash is empty |
|
13450 if (newVal === oldVal && newVal === '') return; |
|
13451 |
|
13452 $rootScope.$evalAsync(scroll); |
|
13453 }); |
|
13454 } |
|
13455 |
|
13456 return scroll; |
|
13457 }]; |
|
13458 } |
|
13459 |
|
13460 var $animateMinErr = minErr('$animate'); |
|
13461 |
|
13462 /** |
|
13463 * @ngdoc provider |
|
13464 * @name $animateProvider |
|
13465 * |
|
13466 * @description |
|
13467 * Default implementation of $animate that doesn't perform any animations, instead just |
|
13468 * synchronously performs DOM |
|
13469 * updates and calls done() callbacks. |
|
13470 * |
|
13471 * In order to enable animations the ngAnimate module has to be loaded. |
|
13472 * |
|
13473 * To see the functional implementation check out src/ngAnimate/animate.js |
|
13474 */ |
|
13475 var $AnimateProvider = ['$provide', function($provide) { |
|
13476 |
|
13477 |
|
13478 this.$$selectors = {}; |
|
13479 |
|
13480 |
|
13481 /** |
|
13482 * @ngdoc method |
|
13483 * @name $animateProvider#register |
|
13484 * |
|
13485 * @description |
|
13486 * Registers a new injectable animation factory function. The factory function produces the |
|
13487 * animation object which contains callback functions for each event that is expected to be |
|
13488 * animated. |
|
13489 * |
|
13490 * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` |
|
13491 * must be called once the element animation is complete. If a function is returned then the |
|
13492 * animation service will use this function to cancel the animation whenever a cancel event is |
|
13493 * triggered. |
|
13494 * |
|
13495 * |
|
13496 * ```js |
|
13497 * return { |
|
13498 * eventFn : function(element, done) { |
|
13499 * //code to run the animation |
|
13500 * //once complete, then run done() |
|
13501 * return function cancellationFunction() { |
|
13502 * //code to cancel the animation |
|
13503 * } |
|
13504 * } |
|
13505 * } |
|
13506 * ``` |
|
13507 * |
|
13508 * @param {string} name The name of the animation. |
|
13509 * @param {Function} factory The factory function that will be executed to return the animation |
|
13510 * object. |
|
13511 */ |
|
13512 this.register = function(name, factory) { |
|
13513 var key = name + '-animation'; |
|
13514 if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel', |
|
13515 "Expecting class selector starting with '.' got '{0}'.", name); |
|
13516 this.$$selectors[name.substr(1)] = key; |
|
13517 $provide.factory(key, factory); |
|
13518 }; |
|
13519 |
|
13520 /** |
|
13521 * @ngdoc method |
|
13522 * @name $animateProvider#classNameFilter |
|
13523 * |
|
13524 * @description |
|
13525 * Sets and/or returns the CSS class regular expression that is checked when performing |
|
13526 * an animation. Upon bootstrap the classNameFilter value is not set at all and will |
|
13527 * therefore enable $animate to attempt to perform an animation on any element. |
|
13528 * When setting the classNameFilter value, animations will only be performed on elements |
|
13529 * that successfully match the filter expression. This in turn can boost performance |
|
13530 * for low-powered devices as well as applications containing a lot of structural operations. |
|
13531 * @param {RegExp=} expression The className expression which will be checked against all animations |
|
13532 * @return {RegExp} The current CSS className expression value. If null then there is no expression value |
|
13533 */ |
|
13534 this.classNameFilter = function(expression) { |
|
13535 if(arguments.length === 1) { |
|
13536 this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; |
|
13537 } |
|
13538 return this.$$classNameFilter; |
|
13539 }; |
|
13540 |
|
13541 this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) { |
|
13542 |
|
13543 var currentDefer; |
|
13544 |
|
13545 function runAnimationPostDigest(fn) { |
|
13546 var cancelFn, defer = $$q.defer(); |
|
13547 defer.promise.$$cancelFn = function ngAnimateMaybeCancel() { |
|
13548 cancelFn && cancelFn(); |
|
13549 }; |
|
13550 |
|
13551 $rootScope.$$postDigest(function ngAnimatePostDigest() { |
|
13552 cancelFn = fn(function ngAnimateNotifyComplete() { |
|
13553 defer.resolve(); |
|
13554 }); |
|
13555 }); |
|
13556 |
|
13557 return defer.promise; |
|
13558 } |
|
13559 |
|
13560 function resolveElementClasses(element, cache) { |
|
13561 var toAdd = [], toRemove = []; |
|
13562 |
|
13563 var hasClasses = createMap(); |
|
13564 forEach((element.attr('class') || '').split(/\s+/), function(className) { |
|
13565 hasClasses[className] = true; |
|
13566 }); |
|
13567 |
|
13568 forEach(cache.classes, function(status, className) { |
|
13569 var hasClass = hasClasses[className]; |
|
13570 |
|
13571 // If the most recent class manipulation (via $animate) was to remove the class, and the |
|
13572 // element currently has the class, the class is scheduled for removal. Otherwise, if |
|
13573 // the most recent class manipulation (via $animate) was to add the class, and the |
|
13574 // element does not currently have the class, the class is scheduled to be added. |
|
13575 if (status === false && hasClass) { |
|
13576 toRemove.push(className); |
|
13577 } else if (status === true && !hasClass) { |
|
13578 toAdd.push(className); |
|
13579 } |
|
13580 }); |
|
13581 |
|
13582 return (toAdd.length + toRemove.length) > 0 && [toAdd.length && toAdd, toRemove.length && toRemove]; |
|
13583 } |
|
13584 |
|
13585 function cachedClassManipulation(cache, classes, op) { |
|
13586 for (var i=0, ii = classes.length; i < ii; ++i) { |
|
13587 var className = classes[i]; |
|
13588 cache[className] = op; |
|
13589 } |
|
13590 } |
|
13591 |
|
13592 function asyncPromise() { |
|
13593 // only serve one instance of a promise in order to save CPU cycles |
|
13594 if (!currentDefer) { |
|
13595 currentDefer = $$q.defer(); |
|
13596 $$asyncCallback(function() { |
|
13597 currentDefer.resolve(); |
|
13598 currentDefer = null; |
|
13599 }); |
|
13600 } |
|
13601 return currentDefer.promise; |
|
13602 } |
|
13603 |
|
13604 /** |
|
13605 * |
|
13606 * @ngdoc service |
|
13607 * @name $animate |
|
13608 * @description The $animate service provides rudimentary DOM manipulation functions to |
|
13609 * insert, remove and move elements within the DOM, as well as adding and removing classes. |
|
13610 * This service is the core service used by the ngAnimate $animator service which provides |
|
13611 * high-level animation hooks for CSS and JavaScript. |
|
13612 * |
|
13613 * $animate is available in the AngularJS core, however, the ngAnimate module must be included |
|
13614 * to enable full out animation support. Otherwise, $animate will only perform simple DOM |
|
13615 * manipulation operations. |
|
13616 * |
|
13617 * To learn more about enabling animation support, click here to visit the {@link ngAnimate |
|
13618 * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service |
|
13619 * page}. |
|
13620 */ |
|
13621 return { |
|
13622 |
|
13623 /** |
|
13624 * |
|
13625 * @ngdoc method |
|
13626 * @name $animate#enter |
|
13627 * @kind function |
|
13628 * @description Inserts the element into the DOM either after the `after` element or |
|
13629 * as the first child within the `parent` element. When the function is called a promise |
|
13630 * is returned that will be resolved at a later time. |
|
13631 * @param {DOMElement} element the element which will be inserted into the DOM |
|
13632 * @param {DOMElement} parent the parent element which will append the element as |
|
13633 * a child (if the after element is not present) |
|
13634 * @param {DOMElement} after the sibling element which will append the element |
|
13635 * after itself |
|
13636 * @return {Promise} the animation callback promise |
|
13637 */ |
|
13638 enter : function(element, parent, after) { |
|
13639 after ? after.after(element) |
|
13640 : parent.prepend(element); |
|
13641 return asyncPromise(); |
|
13642 }, |
|
13643 |
|
13644 /** |
|
13645 * |
|
13646 * @ngdoc method |
|
13647 * @name $animate#leave |
|
13648 * @kind function |
|
13649 * @description Removes the element from the DOM. When the function is called a promise |
|
13650 * is returned that will be resolved at a later time. |
|
13651 * @param {DOMElement} element the element which will be removed from the DOM |
|
13652 * @return {Promise} the animation callback promise |
|
13653 */ |
|
13654 leave : function(element) { |
|
13655 element.remove(); |
|
13656 return asyncPromise(); |
|
13657 }, |
|
13658 |
|
13659 /** |
|
13660 * |
|
13661 * @ngdoc method |
|
13662 * @name $animate#move |
|
13663 * @kind function |
|
13664 * @description Moves the position of the provided element within the DOM to be placed |
|
13665 * either after the `after` element or inside of the `parent` element. When the function |
|
13666 * is called a promise is returned that will be resolved at a later time. |
|
13667 * |
|
13668 * @param {DOMElement} element the element which will be moved around within the |
|
13669 * DOM |
|
13670 * @param {DOMElement} parent the parent element where the element will be |
|
13671 * inserted into (if the after element is not present) |
|
13672 * @param {DOMElement} after the sibling element where the element will be |
|
13673 * positioned next to |
|
13674 * @return {Promise} the animation callback promise |
|
13675 */ |
|
13676 move : function(element, parent, after) { |
|
13677 // Do not remove element before insert. Removing will cause data associated with the |
|
13678 // element to be dropped. Insert will implicitly do the remove. |
|
13679 return this.enter(element, parent, after); |
|
13680 }, |
|
13681 |
|
13682 /** |
|
13683 * |
|
13684 * @ngdoc method |
|
13685 * @name $animate#addClass |
|
13686 * @kind function |
|
13687 * @description Adds the provided className CSS class value to the provided element. |
|
13688 * When the function is called a promise is returned that will be resolved at a later time. |
|
13689 * @param {DOMElement} element the element which will have the className value |
|
13690 * added to it |
|
13691 * @param {string} className the CSS class which will be added to the element |
|
13692 * @return {Promise} the animation callback promise |
|
13693 */ |
|
13694 addClass : function(element, className) { |
|
13695 return this.setClass(element, className, []); |
|
13696 }, |
|
13697 |
|
13698 $$addClassImmediately : function addClassImmediately(element, className) { |
|
13699 element = jqLite(element); |
|
13700 className = !isString(className) |
|
13701 ? (isArray(className) ? className.join(' ') : '') |
|
13702 : className; |
|
13703 forEach(element, function (element) { |
|
13704 jqLiteAddClass(element, className); |
|
13705 }); |
|
13706 }, |
|
13707 |
|
13708 /** |
|
13709 * |
|
13710 * @ngdoc method |
|
13711 * @name $animate#removeClass |
|
13712 * @kind function |
|
13713 * @description Removes the provided className CSS class value from the provided element. |
|
13714 * When the function is called a promise is returned that will be resolved at a later time. |
|
13715 * @param {DOMElement} element the element which will have the className value |
|
13716 * removed from it |
|
13717 * @param {string} className the CSS class which will be removed from the element |
|
13718 * @return {Promise} the animation callback promise |
|
13719 */ |
|
13720 removeClass : function(element, className) { |
|
13721 return this.setClass(element, [], className); |
|
13722 }, |
|
13723 |
|
13724 $$removeClassImmediately : function removeClassImmediately(element, className) { |
|
13725 element = jqLite(element); |
|
13726 className = !isString(className) |
|
13727 ? (isArray(className) ? className.join(' ') : '') |
|
13728 : className; |
|
13729 forEach(element, function (element) { |
|
13730 jqLiteRemoveClass(element, className); |
|
13731 }); |
|
13732 return asyncPromise(); |
|
13733 }, |
|
13734 |
|
13735 /** |
|
13736 * |
|
13737 * @ngdoc method |
|
13738 * @name $animate#setClass |
|
13739 * @kind function |
|
13740 * @description Adds and/or removes the given CSS classes to and from the element. |
|
13741 * When the function is called a promise is returned that will be resolved at a later time. |
|
13742 * @param {DOMElement} element the element which will have its CSS classes changed |
|
13743 * removed from it |
|
13744 * @param {string} add the CSS classes which will be added to the element |
|
13745 * @param {string} remove the CSS class which will be removed from the element |
|
13746 * @return {Promise} the animation callback promise |
|
13747 */ |
|
13748 setClass : function(element, add, remove, runSynchronously) { |
|
13749 var self = this; |
|
13750 var STORAGE_KEY = '$$animateClasses'; |
|
13751 var createdCache = false; |
|
13752 element = jqLite(element); |
|
13753 |
|
13754 if (runSynchronously) { |
|
13755 // TODO(@caitp/@matsko): Remove undocumented `runSynchronously` parameter, and always |
|
13756 // perform DOM manipulation asynchronously or in postDigest. |
|
13757 self.$$addClassImmediately(element, add); |
|
13758 self.$$removeClassImmediately(element, remove); |
|
13759 return asyncPromise(); |
|
13760 } |
|
13761 |
|
13762 var cache = element.data(STORAGE_KEY); |
|
13763 if (!cache) { |
|
13764 cache = { |
|
13765 classes: {} |
|
13766 }; |
|
13767 createdCache = true; |
|
13768 } |
|
13769 |
|
13770 var classes = cache.classes; |
|
13771 |
|
13772 add = isArray(add) ? add : add.split(' '); |
|
13773 remove = isArray(remove) ? remove : remove.split(' '); |
|
13774 cachedClassManipulation(classes, add, true); |
|
13775 cachedClassManipulation(classes, remove, false); |
|
13776 |
|
13777 if (createdCache) { |
|
13778 cache.promise = runAnimationPostDigest(function(done) { |
|
13779 var cache = element.data(STORAGE_KEY); |
|
13780 element.removeData(STORAGE_KEY); |
|
13781 |
|
13782 var classes = cache && resolveElementClasses(element, cache); |
|
13783 |
|
13784 if (classes) { |
|
13785 if (classes[0]) self.$$addClassImmediately(element, classes[0]); |
|
13786 if (classes[1]) self.$$removeClassImmediately(element, classes[1]); |
|
13787 } |
|
13788 |
|
13789 done(); |
|
13790 }); |
|
13791 element.data(STORAGE_KEY, cache); |
|
13792 } |
|
13793 |
|
13794 return cache.promise; |
|
13795 }, |
|
13796 |
|
13797 enabled : noop, |
|
13798 cancel : noop |
|
13799 }; |
|
13800 }]; |
|
13801 }]; |
|
13802 |
|
13803 function $$AsyncCallbackProvider(){ |
|
13804 this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) { |
|
13805 return $$rAF.supported |
|
13806 ? function(fn) { return $$rAF(fn); } |
|
13807 : function(fn) { |
|
13808 return $timeout(fn, 0, false); |
|
13809 }; |
|
13810 }]; |
|
13811 } |
|
13812 |
|
13813 /* global stripHash: true */ |
|
13814 |
|
13815 /** |
|
13816 * ! This is a private undocumented service ! |
|
13817 * |
|
13818 * @name $browser |
|
13819 * @requires $log |
|
13820 * @description |
|
13821 * This object has two goals: |
|
13822 * |
|
13823 * - hide all the global state in the browser caused by the window object |
|
13824 * - abstract away all the browser specific features and inconsistencies |
|
13825 * |
|
13826 * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` |
|
13827 * service, which can be used for convenient testing of the application without the interaction with |
|
13828 * the real browser apis. |
|
13829 */ |
|
13830 /** |
|
13831 * @param {object} window The global window object. |
|
13832 * @param {object} document jQuery wrapped document. |
|
13833 * @param {function()} XHR XMLHttpRequest constructor. |
|
13834 * @param {object} $log console.log or an object with the same interface. |
|
13835 * @param {object} $sniffer $sniffer service |
|
13836 */ |
|
13837 function Browser(window, document, $log, $sniffer) { |
|
13838 var self = this, |
|
13839 rawDocument = document[0], |
|
13840 location = window.location, |
|
13841 history = window.history, |
|
13842 setTimeout = window.setTimeout, |
|
13843 clearTimeout = window.clearTimeout, |
|
13844 pendingDeferIds = {}; |
|
13845 |
|
13846 self.isMock = false; |
|
13847 |
|
13848 var outstandingRequestCount = 0; |
|
13849 var outstandingRequestCallbacks = []; |
|
13850 |
|
13851 // TODO(vojta): remove this temporary api |
|
13852 self.$$completeOutstandingRequest = completeOutstandingRequest; |
|
13853 self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; |
|
13854 |
|
13855 /** |
|
13856 * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` |
|
13857 * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. |
|
13858 */ |
|
13859 function completeOutstandingRequest(fn) { |
|
13860 try { |
|
13861 fn.apply(null, sliceArgs(arguments, 1)); |
|
13862 } finally { |
|
13863 outstandingRequestCount--; |
|
13864 if (outstandingRequestCount === 0) { |
|
13865 while(outstandingRequestCallbacks.length) { |
|
13866 try { |
|
13867 outstandingRequestCallbacks.pop()(); |
|
13868 } catch (e) { |
|
13869 $log.error(e); |
|
13870 } |
|
13871 } |
|
13872 } |
|
13873 } |
|
13874 } |
|
13875 |
|
13876 /** |
|
13877 * @private |
|
13878 * Note: this method is used only by scenario runner |
|
13879 * TODO(vojta): prefix this method with $$ ? |
|
13880 * @param {function()} callback Function that will be called when no outstanding request |
|
13881 */ |
|
13882 self.notifyWhenNoOutstandingRequests = function(callback) { |
|
13883 // force browser to execute all pollFns - this is needed so that cookies and other pollers fire |
|
13884 // at some deterministic time in respect to the test runner's actions. Leaving things up to the |
|
13885 // regular poller would result in flaky tests. |
|
13886 forEach(pollFns, function(pollFn){ pollFn(); }); |
|
13887 |
|
13888 if (outstandingRequestCount === 0) { |
|
13889 callback(); |
|
13890 } else { |
|
13891 outstandingRequestCallbacks.push(callback); |
|
13892 } |
|
13893 }; |
|
13894 |
|
13895 ////////////////////////////////////////////////////////////// |
|
13896 // Poll Watcher API |
|
13897 ////////////////////////////////////////////////////////////// |
|
13898 var pollFns = [], |
|
13899 pollTimeout; |
|
13900 |
|
13901 /** |
|
13902 * @name $browser#addPollFn |
|
13903 * |
|
13904 * @param {function()} fn Poll function to add |
|
13905 * |
|
13906 * @description |
|
13907 * Adds a function to the list of functions that poller periodically executes, |
|
13908 * and starts polling if not started yet. |
|
13909 * |
|
13910 * @returns {function()} the added function |
|
13911 */ |
|
13912 self.addPollFn = function(fn) { |
|
13913 if (isUndefined(pollTimeout)) startPoller(100, setTimeout); |
|
13914 pollFns.push(fn); |
|
13915 return fn; |
|
13916 }; |
|
13917 |
|
13918 /** |
|
13919 * @param {number} interval How often should browser call poll functions (ms) |
|
13920 * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. |
|
13921 * |
|
13922 * @description |
|
13923 * Configures the poller to run in the specified intervals, using the specified |
|
13924 * setTimeout fn and kicks it off. |
|
13925 */ |
|
13926 function startPoller(interval, setTimeout) { |
|
13927 (function check() { |
|
13928 forEach(pollFns, function(pollFn){ pollFn(); }); |
|
13929 pollTimeout = setTimeout(check, interval); |
|
13930 })(); |
|
13931 } |
|
13932 |
|
13933 ////////////////////////////////////////////////////////////// |
|
13934 // URL API |
|
13935 ////////////////////////////////////////////////////////////// |
|
13936 |
|
13937 var lastBrowserUrl = location.href, |
|
13938 lastHistoryState = history.state, |
|
13939 baseElement = document.find('base'), |
|
13940 reloadLocation = null; |
|
13941 |
|
13942 /** |
|
13943 * @name $browser#url |
|
13944 * |
|
13945 * @description |
|
13946 * GETTER: |
|
13947 * Without any argument, this method just returns current value of location.href. |
|
13948 * |
|
13949 * SETTER: |
|
13950 * With at least one argument, this method sets url to new value. |
|
13951 * If html5 history api supported, pushState/replaceState is used, otherwise |
|
13952 * location.href/location.replace is used. |
|
13953 * Returns its own instance to allow chaining |
|
13954 * |
|
13955 * NOTE: this api is intended for use only by the $location service. Please use the |
|
13956 * {@link ng.$location $location service} to change url. |
|
13957 * |
|
13958 * @param {string} url New url (when used as setter) |
|
13959 * @param {boolean=} replace Should new url replace current history record? |
|
13960 * @param {object=} state object to use with pushState/replaceState |
|
13961 */ |
|
13962 self.url = function(url, replace, state) { |
|
13963 // In modern browsers `history.state` is `null` by default; treating it separately |
|
13964 // from `undefined` would cause `$browser.url('/foo')` to change `history.state` |
|
13965 // to undefined via `pushState`. Instead, let's change `undefined` to `null` here. |
|
13966 if (isUndefined(state)) { |
|
13967 state = null; |
|
13968 } |
|
13969 |
|
13970 // Android Browser BFCache causes location, history reference to become stale. |
|
13971 if (location !== window.location) location = window.location; |
|
13972 if (history !== window.history) history = window.history; |
|
13973 |
|
13974 // setter |
|
13975 if (url) { |
|
13976 // Don't change anything if previous and current URLs and states match. This also prevents |
|
13977 // IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode. |
|
13978 // See https://github.com/angular/angular.js/commit/ffb2701 |
|
13979 if (lastBrowserUrl === url && (!$sniffer.history || history.state === state)) { |
|
13980 return; |
|
13981 } |
|
13982 var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url); |
|
13983 lastBrowserUrl = url; |
|
13984 // Don't use history API if only the hash changed |
|
13985 // due to a bug in IE10/IE11 which leads |
|
13986 // to not firing a `hashchange` nor `popstate` event |
|
13987 // in some cases (see #9143). |
|
13988 if ($sniffer.history && (!sameBase || history.state !== state)) { |
|
13989 history[replace ? 'replaceState' : 'pushState'](state, '', url); |
|
13990 lastHistoryState = history.state; |
|
13991 } else { |
|
13992 if (!sameBase) { |
|
13993 reloadLocation = url; |
|
13994 } |
|
13995 if (replace) { |
|
13996 location.replace(url); |
|
13997 } else { |
|
13998 location.href = url; |
|
13999 } |
|
14000 } |
|
14001 return self; |
|
14002 // getter |
|
14003 } else { |
|
14004 // - reloadLocation is needed as browsers don't allow to read out |
|
14005 // the new location.href if a reload happened. |
|
14006 // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 |
|
14007 return reloadLocation || location.href.replace(/%27/g,"'"); |
|
14008 } |
|
14009 }; |
|
14010 |
|
14011 /** |
|
14012 * @name $browser#state |
|
14013 * |
|
14014 * @description |
|
14015 * This method is a getter. |
|
14016 * |
|
14017 * Return history.state or null if history.state is undefined. |
|
14018 * |
|
14019 * @returns {object} state |
|
14020 */ |
|
14021 self.state = function() { |
|
14022 return isUndefined(history.state) ? null : history.state; |
|
14023 }; |
|
14024 |
|
14025 var urlChangeListeners = [], |
|
14026 urlChangeInit = false; |
|
14027 |
|
14028 function fireUrlChange() { |
|
14029 if (lastBrowserUrl === self.url() && lastHistoryState === history.state) { |
|
14030 return; |
|
14031 } |
|
14032 |
|
14033 lastBrowserUrl = self.url(); |
|
14034 forEach(urlChangeListeners, function(listener) { |
|
14035 listener(self.url(), history.state); |
|
14036 }); |
|
14037 } |
|
14038 |
|
14039 /** |
|
14040 * @name $browser#onUrlChange |
|
14041 * |
|
14042 * @description |
|
14043 * Register callback function that will be called, when url changes. |
|
14044 * |
|
14045 * It's only called when the url is changed from outside of angular: |
|
14046 * - user types different url into address bar |
|
14047 * - user clicks on history (forward/back) button |
|
14048 * - user clicks on a link |
|
14049 * |
|
14050 * It's not called when url is changed by $browser.url() method |
|
14051 * |
|
14052 * The listener gets called with new url as parameter. |
|
14053 * |
|
14054 * NOTE: this api is intended for use only by the $location service. Please use the |
|
14055 * {@link ng.$location $location service} to monitor url changes in angular apps. |
|
14056 * |
|
14057 * @param {function(string)} listener Listener function to be called when url changes. |
|
14058 * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. |
|
14059 */ |
|
14060 self.onUrlChange = function(callback) { |
|
14061 // TODO(vojta): refactor to use node's syntax for events |
|
14062 if (!urlChangeInit) { |
|
14063 // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) |
|
14064 // don't fire popstate when user change the address bar and don't fire hashchange when url |
|
14065 // changed by push/replaceState |
|
14066 |
|
14067 // html5 history api - popstate event |
|
14068 if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange); |
|
14069 // hashchange event |
|
14070 jqLite(window).on('hashchange', fireUrlChange); |
|
14071 |
|
14072 urlChangeInit = true; |
|
14073 } |
|
14074 |
|
14075 urlChangeListeners.push(callback); |
|
14076 return callback; |
|
14077 }; |
|
14078 |
|
14079 /** |
|
14080 * Checks whether the url has changed outside of Angular. |
|
14081 * Needs to be exported to be able to check for changes that have been done in sync, |
|
14082 * as hashchange/popstate events fire in async. |
|
14083 */ |
|
14084 self.$$checkUrlChange = fireUrlChange; |
|
14085 |
|
14086 ////////////////////////////////////////////////////////////// |
|
14087 // Misc API |
|
14088 ////////////////////////////////////////////////////////////// |
|
14089 |
|
14090 /** |
|
14091 * @name $browser#baseHref |
|
14092 * |
|
14093 * @description |
|
14094 * Returns current <base href> |
|
14095 * (always relative - without domain) |
|
14096 * |
|
14097 * @returns {string} The current base href |
|
14098 */ |
|
14099 self.baseHref = function() { |
|
14100 var href = baseElement.attr('href'); |
|
14101 return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; |
|
14102 }; |
|
14103 |
|
14104 ////////////////////////////////////////////////////////////// |
|
14105 // Cookies API |
|
14106 ////////////////////////////////////////////////////////////// |
|
14107 var lastCookies = {}; |
|
14108 var lastCookieString = ''; |
|
14109 var cookiePath = self.baseHref(); |
|
14110 |
|
14111 /** |
|
14112 * @name $browser#cookies |
|
14113 * |
|
14114 * @param {string=} name Cookie name |
|
14115 * @param {string=} value Cookie value |
|
14116 * |
|
14117 * @description |
|
14118 * The cookies method provides a 'private' low level access to browser cookies. |
|
14119 * It is not meant to be used directly, use the $cookie service instead. |
|
14120 * |
|
14121 * The return values vary depending on the arguments that the method was called with as follows: |
|
14122 * |
|
14123 * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify |
|
14124 * it |
|
14125 * - cookies(name, value) -> set name to value, if value is undefined delete the cookie |
|
14126 * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that |
|
14127 * way) |
|
14128 * |
|
14129 * @returns {Object} Hash of all cookies (if called without any parameter) |
|
14130 */ |
|
14131 self.cookies = function(name, value) { |
|
14132 var cookieLength, cookieArray, cookie, i, index; |
|
14133 |
|
14134 if (name) { |
|
14135 if (value === undefined) { |
|
14136 rawDocument.cookie = encodeURIComponent(name) + "=;path=" + cookiePath + |
|
14137 ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; |
|
14138 } else { |
|
14139 if (isString(value)) { |
|
14140 cookieLength = (rawDocument.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) + |
|
14141 ';path=' + cookiePath).length + 1; |
|
14142 |
|
14143 // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: |
|
14144 // - 300 cookies |
|
14145 // - 20 cookies per unique domain |
|
14146 // - 4096 bytes per cookie |
|
14147 if (cookieLength > 4096) { |
|
14148 $log.warn("Cookie '"+ name + |
|
14149 "' possibly not set or overflowed because it was too large ("+ |
|
14150 cookieLength + " > 4096 bytes)!"); |
|
14151 } |
|
14152 } |
|
14153 } |
|
14154 } else { |
|
14155 if (rawDocument.cookie !== lastCookieString) { |
|
14156 lastCookieString = rawDocument.cookie; |
|
14157 cookieArray = lastCookieString.split("; "); |
|
14158 lastCookies = {}; |
|
14159 |
|
14160 for (i = 0; i < cookieArray.length; i++) { |
|
14161 cookie = cookieArray[i]; |
|
14162 index = cookie.indexOf('='); |
|
14163 if (index > 0) { //ignore nameless cookies |
|
14164 name = decodeURIComponent(cookie.substring(0, index)); |
|
14165 // the first value that is seen for a cookie is the most |
|
14166 // specific one. values for the same cookie name that |
|
14167 // follow are for less specific paths. |
|
14168 if (lastCookies[name] === undefined) { |
|
14169 lastCookies[name] = decodeURIComponent(cookie.substring(index + 1)); |
|
14170 } |
|
14171 } |
|
14172 } |
|
14173 } |
|
14174 return lastCookies; |
|
14175 } |
|
14176 }; |
|
14177 |
|
14178 |
|
14179 /** |
|
14180 * @name $browser#defer |
|
14181 * @param {function()} fn A function, who's execution should be deferred. |
|
14182 * @param {number=} [delay=0] of milliseconds to defer the function execution. |
|
14183 * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. |
|
14184 * |
|
14185 * @description |
|
14186 * Executes a fn asynchronously via `setTimeout(fn, delay)`. |
|
14187 * |
|
14188 * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using |
|
14189 * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed |
|
14190 * via `$browser.defer.flush()`. |
|
14191 * |
|
14192 */ |
|
14193 self.defer = function(fn, delay) { |
|
14194 var timeoutId; |
|
14195 outstandingRequestCount++; |
|
14196 timeoutId = setTimeout(function() { |
|
14197 delete pendingDeferIds[timeoutId]; |
|
14198 completeOutstandingRequest(fn); |
|
14199 }, delay || 0); |
|
14200 pendingDeferIds[timeoutId] = true; |
|
14201 return timeoutId; |
|
14202 }; |
|
14203 |
|
14204 |
|
14205 /** |
|
14206 * @name $browser#defer.cancel |
|
14207 * |
|
14208 * @description |
|
14209 * Cancels a deferred task identified with `deferId`. |
|
14210 * |
|
14211 * @param {*} deferId Token returned by the `$browser.defer` function. |
|
14212 * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully |
|
14213 * canceled. |
|
14214 */ |
|
14215 self.defer.cancel = function(deferId) { |
|
14216 if (pendingDeferIds[deferId]) { |
|
14217 delete pendingDeferIds[deferId]; |
|
14218 clearTimeout(deferId); |
|
14219 completeOutstandingRequest(noop); |
|
14220 return true; |
|
14221 } |
|
14222 return false; |
|
14223 }; |
|
14224 |
|
14225 } |
|
14226 |
|
14227 function $BrowserProvider(){ |
|
14228 this.$get = ['$window', '$log', '$sniffer', '$document', |
|
14229 function( $window, $log, $sniffer, $document){ |
|
14230 return new Browser($window, $document, $log, $sniffer); |
|
14231 }]; |
|
14232 } |
|
14233 |
|
14234 /** |
|
14235 * @ngdoc service |
|
14236 * @name $cacheFactory |
|
14237 * |
|
14238 * @description |
|
14239 * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to |
|
14240 * them. |
|
14241 * |
|
14242 * ```js |
|
14243 * |
|
14244 * var cache = $cacheFactory('cacheId'); |
|
14245 * expect($cacheFactory.get('cacheId')).toBe(cache); |
|
14246 * expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined(); |
|
14247 * |
|
14248 * cache.put("key", "value"); |
|
14249 * cache.put("another key", "another value"); |
|
14250 * |
|
14251 * // We've specified no options on creation |
|
14252 * expect(cache.info()).toEqual({id: 'cacheId', size: 2}); |
|
14253 * |
|
14254 * ``` |
|
14255 * |
|
14256 * |
|
14257 * @param {string} cacheId Name or id of the newly created cache. |
|
14258 * @param {object=} options Options object that specifies the cache behavior. Properties: |
|
14259 * |
|
14260 * - `{number=}` `capacity` — turns the cache into LRU cache. |
|
14261 * |
|
14262 * @returns {object} Newly created cache object with the following set of methods: |
|
14263 * |
|
14264 * - `{object}` `info()` — Returns id, size, and options of cache. |
|
14265 * - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns |
|
14266 * it. |
|
14267 * - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss. |
|
14268 * - `{void}` `remove({string} key)` — Removes a key-value pair from the cache. |
|
14269 * - `{void}` `removeAll()` — Removes all cached values. |
|
14270 * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. |
|
14271 * |
|
14272 * @example |
|
14273 <example module="cacheExampleApp"> |
|
14274 <file name="index.html"> |
|
14275 <div ng-controller="CacheController"> |
|
14276 <input ng-model="newCacheKey" placeholder="Key"> |
|
14277 <input ng-model="newCacheValue" placeholder="Value"> |
|
14278 <button ng-click="put(newCacheKey, newCacheValue)">Cache</button> |
|
14279 |
|
14280 <p ng-if="keys.length">Cached Values</p> |
|
14281 <div ng-repeat="key in keys"> |
|
14282 <span ng-bind="key"></span> |
|
14283 <span>: </span> |
|
14284 <b ng-bind="cache.get(key)"></b> |
|
14285 </div> |
|
14286 |
|
14287 <p>Cache Info</p> |
|
14288 <div ng-repeat="(key, value) in cache.info()"> |
|
14289 <span ng-bind="key"></span> |
|
14290 <span>: </span> |
|
14291 <b ng-bind="value"></b> |
|
14292 </div> |
|
14293 </div> |
|
14294 </file> |
|
14295 <file name="script.js"> |
|
14296 angular.module('cacheExampleApp', []). |
|
14297 controller('CacheController', ['$scope', '$cacheFactory', function($scope, $cacheFactory) { |
|
14298 $scope.keys = []; |
|
14299 $scope.cache = $cacheFactory('cacheId'); |
|
14300 $scope.put = function(key, value) { |
|
14301 if ($scope.cache.get(key) === undefined) { |
|
14302 $scope.keys.push(key); |
|
14303 } |
|
14304 $scope.cache.put(key, value === undefined ? null : value); |
|
14305 }; |
|
14306 }]); |
|
14307 </file> |
|
14308 <file name="style.css"> |
|
14309 p { |
|
14310 margin: 10px 0 3px; |
|
14311 } |
|
14312 </file> |
|
14313 </example> |
|
14314 */ |
|
14315 function $CacheFactoryProvider() { |
|
14316 |
|
14317 this.$get = function() { |
|
14318 var caches = {}; |
|
14319 |
|
14320 function cacheFactory(cacheId, options) { |
|
14321 if (cacheId in caches) { |
|
14322 throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); |
|
14323 } |
|
14324 |
|
14325 var size = 0, |
|
14326 stats = extend({}, options, {id: cacheId}), |
|
14327 data = {}, |
|
14328 capacity = (options && options.capacity) || Number.MAX_VALUE, |
|
14329 lruHash = {}, |
|
14330 freshEnd = null, |
|
14331 staleEnd = null; |
|
14332 |
|
14333 /** |
|
14334 * @ngdoc type |
|
14335 * @name $cacheFactory.Cache |
|
14336 * |
|
14337 * @description |
|
14338 * A cache object used to store and retrieve data, primarily used by |
|
14339 * {@link $http $http} and the {@link ng.directive:script script} directive to cache |
|
14340 * templates and other data. |
|
14341 * |
|
14342 * ```js |
|
14343 * angular.module('superCache') |
|
14344 * .factory('superCache', ['$cacheFactory', function($cacheFactory) { |
|
14345 * return $cacheFactory('super-cache'); |
|
14346 * }]); |
|
14347 * ``` |
|
14348 * |
|
14349 * Example test: |
|
14350 * |
|
14351 * ```js |
|
14352 * it('should behave like a cache', inject(function(superCache) { |
|
14353 * superCache.put('key', 'value'); |
|
14354 * superCache.put('another key', 'another value'); |
|
14355 * |
|
14356 * expect(superCache.info()).toEqual({ |
|
14357 * id: 'super-cache', |
|
14358 * size: 2 |
|
14359 * }); |
|
14360 * |
|
14361 * superCache.remove('another key'); |
|
14362 * expect(superCache.get('another key')).toBeUndefined(); |
|
14363 * |
|
14364 * superCache.removeAll(); |
|
14365 * expect(superCache.info()).toEqual({ |
|
14366 * id: 'super-cache', |
|
14367 * size: 0 |
|
14368 * }); |
|
14369 * })); |
|
14370 * ``` |
|
14371 */ |
|
14372 return caches[cacheId] = { |
|
14373 |
|
14374 /** |
|
14375 * @ngdoc method |
|
14376 * @name $cacheFactory.Cache#put |
|
14377 * @kind function |
|
14378 * |
|
14379 * @description |
|
14380 * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be |
|
14381 * retrieved later, and incrementing the size of the cache if the key was not already |
|
14382 * present in the cache. If behaving like an LRU cache, it will also remove stale |
|
14383 * entries from the set. |
|
14384 * |
|
14385 * It will not insert undefined values into the cache. |
|
14386 * |
|
14387 * @param {string} key the key under which the cached data is stored. |
|
14388 * @param {*} value the value to store alongside the key. If it is undefined, the key |
|
14389 * will not be stored. |
|
14390 * @returns {*} the value stored. |
|
14391 */ |
|
14392 put: function(key, value) { |
|
14393 if (capacity < Number.MAX_VALUE) { |
|
14394 var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); |
|
14395 |
|
14396 refresh(lruEntry); |
|
14397 } |
|
14398 |
|
14399 if (isUndefined(value)) return; |
|
14400 if (!(key in data)) size++; |
|
14401 data[key] = value; |
|
14402 |
|
14403 if (size > capacity) { |
|
14404 this.remove(staleEnd.key); |
|
14405 } |
|
14406 |
|
14407 return value; |
|
14408 }, |
|
14409 |
|
14410 /** |
|
14411 * @ngdoc method |
|
14412 * @name $cacheFactory.Cache#get |
|
14413 * @kind function |
|
14414 * |
|
14415 * @description |
|
14416 * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object. |
|
14417 * |
|
14418 * @param {string} key the key of the data to be retrieved |
|
14419 * @returns {*} the value stored. |
|
14420 */ |
|
14421 get: function(key) { |
|
14422 if (capacity < Number.MAX_VALUE) { |
|
14423 var lruEntry = lruHash[key]; |
|
14424 |
|
14425 if (!lruEntry) return; |
|
14426 |
|
14427 refresh(lruEntry); |
|
14428 } |
|
14429 |
|
14430 return data[key]; |
|
14431 }, |
|
14432 |
|
14433 |
|
14434 /** |
|
14435 * @ngdoc method |
|
14436 * @name $cacheFactory.Cache#remove |
|
14437 * @kind function |
|
14438 * |
|
14439 * @description |
|
14440 * Removes an entry from the {@link $cacheFactory.Cache Cache} object. |
|
14441 * |
|
14442 * @param {string} key the key of the entry to be removed |
|
14443 */ |
|
14444 remove: function(key) { |
|
14445 if (capacity < Number.MAX_VALUE) { |
|
14446 var lruEntry = lruHash[key]; |
|
14447 |
|
14448 if (!lruEntry) return; |
|
14449 |
|
14450 if (lruEntry == freshEnd) freshEnd = lruEntry.p; |
|
14451 if (lruEntry == staleEnd) staleEnd = lruEntry.n; |
|
14452 link(lruEntry.n,lruEntry.p); |
|
14453 |
|
14454 delete lruHash[key]; |
|
14455 } |
|
14456 |
|
14457 delete data[key]; |
|
14458 size--; |
|
14459 }, |
|
14460 |
|
14461 |
|
14462 /** |
|
14463 * @ngdoc method |
|
14464 * @name $cacheFactory.Cache#removeAll |
|
14465 * @kind function |
|
14466 * |
|
14467 * @description |
|
14468 * Clears the cache object of any entries. |
|
14469 */ |
|
14470 removeAll: function() { |
|
14471 data = {}; |
|
14472 size = 0; |
|
14473 lruHash = {}; |
|
14474 freshEnd = staleEnd = null; |
|
14475 }, |
|
14476 |
|
14477 |
|
14478 /** |
|
14479 * @ngdoc method |
|
14480 * @name $cacheFactory.Cache#destroy |
|
14481 * @kind function |
|
14482 * |
|
14483 * @description |
|
14484 * Destroys the {@link $cacheFactory.Cache Cache} object entirely, |
|
14485 * removing it from the {@link $cacheFactory $cacheFactory} set. |
|
14486 */ |
|
14487 destroy: function() { |
|
14488 data = null; |
|
14489 stats = null; |
|
14490 lruHash = null; |
|
14491 delete caches[cacheId]; |
|
14492 }, |
|
14493 |
|
14494 |
|
14495 /** |
|
14496 * @ngdoc method |
|
14497 * @name $cacheFactory.Cache#info |
|
14498 * @kind function |
|
14499 * |
|
14500 * @description |
|
14501 * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}. |
|
14502 * |
|
14503 * @returns {object} an object with the following properties: |
|
14504 * <ul> |
|
14505 * <li>**id**: the id of the cache instance</li> |
|
14506 * <li>**size**: the number of entries kept in the cache instance</li> |
|
14507 * <li>**...**: any additional properties from the options object when creating the |
|
14508 * cache.</li> |
|
14509 * </ul> |
|
14510 */ |
|
14511 info: function() { |
|
14512 return extend({}, stats, {size: size}); |
|
14513 } |
|
14514 }; |
|
14515 |
|
14516 |
|
14517 /** |
|
14518 * makes the `entry` the freshEnd of the LRU linked list |
|
14519 */ |
|
14520 function refresh(entry) { |
|
14521 if (entry != freshEnd) { |
|
14522 if (!staleEnd) { |
|
14523 staleEnd = entry; |
|
14524 } else if (staleEnd == entry) { |
|
14525 staleEnd = entry.n; |
|
14526 } |
|
14527 |
|
14528 link(entry.n, entry.p); |
|
14529 link(entry, freshEnd); |
|
14530 freshEnd = entry; |
|
14531 freshEnd.n = null; |
|
14532 } |
|
14533 } |
|
14534 |
|
14535 |
|
14536 /** |
|
14537 * bidirectionally links two entries of the LRU linked list |
|
14538 */ |
|
14539 function link(nextEntry, prevEntry) { |
|
14540 if (nextEntry != prevEntry) { |
|
14541 if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify |
|
14542 if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify |
|
14543 } |
|
14544 } |
|
14545 } |
|
14546 |
|
14547 |
|
14548 /** |
|
14549 * @ngdoc method |
|
14550 * @name $cacheFactory#info |
|
14551 * |
|
14552 * @description |
|
14553 * Get information about all the caches that have been created |
|
14554 * |
|
14555 * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` |
|
14556 */ |
|
14557 cacheFactory.info = function() { |
|
14558 var info = {}; |
|
14559 forEach(caches, function(cache, cacheId) { |
|
14560 info[cacheId] = cache.info(); |
|
14561 }); |
|
14562 return info; |
|
14563 }; |
|
14564 |
|
14565 |
|
14566 /** |
|
14567 * @ngdoc method |
|
14568 * @name $cacheFactory#get |
|
14569 * |
|
14570 * @description |
|
14571 * Get access to a cache object by the `cacheId` used when it was created. |
|
14572 * |
|
14573 * @param {string} cacheId Name or id of a cache to access. |
|
14574 * @returns {object} Cache object identified by the cacheId or undefined if no such cache. |
|
14575 */ |
|
14576 cacheFactory.get = function(cacheId) { |
|
14577 return caches[cacheId]; |
|
14578 }; |
|
14579 |
|
14580 |
|
14581 return cacheFactory; |
|
14582 }; |
|
14583 } |
|
14584 |
|
14585 /** |
|
14586 * @ngdoc service |
|
14587 * @name $templateCache |
|
14588 * |
|
14589 * @description |
|
14590 * The first time a template is used, it is loaded in the template cache for quick retrieval. You |
|
14591 * can load templates directly into the cache in a `script` tag, or by consuming the |
|
14592 * `$templateCache` service directly. |
|
14593 * |
|
14594 * Adding via the `script` tag: |
|
14595 * |
|
14596 * ```html |
|
14597 * <script type="text/ng-template" id="templateId.html"> |
|
14598 * <p>This is the content of the template</p> |
|
14599 * </script> |
|
14600 * ``` |
|
14601 * |
|
14602 * **Note:** the `script` tag containing the template does not need to be included in the `head` of |
|
14603 * the document, but it must be below the `ng-app` definition. |
|
14604 * |
|
14605 * Adding via the $templateCache service: |
|
14606 * |
|
14607 * ```js |
|
14608 * var myApp = angular.module('myApp', []); |
|
14609 * myApp.run(function($templateCache) { |
|
14610 * $templateCache.put('templateId.html', 'This is the content of the template'); |
|
14611 * }); |
|
14612 * ``` |
|
14613 * |
|
14614 * To retrieve the template later, simply use it in your HTML: |
|
14615 * ```html |
|
14616 * <div ng-include=" 'templateId.html' "></div> |
|
14617 * ``` |
|
14618 * |
|
14619 * or get it via Javascript: |
|
14620 * ```js |
|
14621 * $templateCache.get('templateId.html') |
|
14622 * ``` |
|
14623 * |
|
14624 * See {@link ng.$cacheFactory $cacheFactory}. |
|
14625 * |
|
14626 */ |
|
14627 function $TemplateCacheProvider() { |
|
14628 this.$get = ['$cacheFactory', function($cacheFactory) { |
|
14629 return $cacheFactory('templates'); |
|
14630 }]; |
|
14631 } |
|
14632 |
|
14633 /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! |
|
14634 * |
|
14635 * DOM-related variables: |
|
14636 * |
|
14637 * - "node" - DOM Node |
|
14638 * - "element" - DOM Element or Node |
|
14639 * - "$node" or "$element" - jqLite-wrapped node or element |
|
14640 * |
|
14641 * |
|
14642 * Compiler related stuff: |
|
14643 * |
|
14644 * - "linkFn" - linking fn of a single directive |
|
14645 * - "nodeLinkFn" - function that aggregates all linking fns for a particular node |
|
14646 * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node |
|
14647 * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList) |
|
14648 */ |
|
14649 |
|
14650 |
|
14651 /** |
|
14652 * @ngdoc service |
|
14653 * @name $compile |
|
14654 * @kind function |
|
14655 * |
|
14656 * @description |
|
14657 * Compiles an HTML string or DOM into a template and produces a template function, which |
|
14658 * can then be used to link {@link ng.$rootScope.Scope `scope`} and the template together. |
|
14659 * |
|
14660 * The compilation is a process of walking the DOM tree and matching DOM elements to |
|
14661 * {@link ng.$compileProvider#directive directives}. |
|
14662 * |
|
14663 * <div class="alert alert-warning"> |
|
14664 * **Note:** This document is an in-depth reference of all directive options. |
|
14665 * For a gentle introduction to directives with examples of common use cases, |
|
14666 * see the {@link guide/directive directive guide}. |
|
14667 * </div> |
|
14668 * |
|
14669 * ## Comprehensive Directive API |
|
14670 * |
|
14671 * There are many different options for a directive. |
|
14672 * |
|
14673 * The difference resides in the return value of the factory function. |
|
14674 * You can either return a "Directive Definition Object" (see below) that defines the directive properties, |
|
14675 * or just the `postLink` function (all other properties will have the default values). |
|
14676 * |
|
14677 * <div class="alert alert-success"> |
|
14678 * **Best Practice:** It's recommended to use the "directive definition object" form. |
|
14679 * </div> |
|
14680 * |
|
14681 * Here's an example directive declared with a Directive Definition Object: |
|
14682 * |
|
14683 * ```js |
|
14684 * var myModule = angular.module(...); |
|
14685 * |
|
14686 * myModule.directive('directiveName', function factory(injectables) { |
|
14687 * var directiveDefinitionObject = { |
|
14688 * priority: 0, |
|
14689 * template: '<div></div>', // or // function(tElement, tAttrs) { ... }, |
|
14690 * // or |
|
14691 * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, |
|
14692 * transclude: false, |
|
14693 * restrict: 'A', |
|
14694 * scope: false, |
|
14695 * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, |
|
14696 * controllerAs: 'stringAlias', |
|
14697 * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], |
|
14698 * compile: function compile(tElement, tAttrs, transclude) { |
|
14699 * return { |
|
14700 * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, |
|
14701 * post: function postLink(scope, iElement, iAttrs, controller) { ... } |
|
14702 * } |
|
14703 * // or |
|
14704 * // return function postLink( ... ) { ... } |
|
14705 * }, |
|
14706 * // or |
|
14707 * // link: { |
|
14708 * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, |
|
14709 * // post: function postLink(scope, iElement, iAttrs, controller) { ... } |
|
14710 * // } |
|
14711 * // or |
|
14712 * // link: function postLink( ... ) { ... } |
|
14713 * }; |
|
14714 * return directiveDefinitionObject; |
|
14715 * }); |
|
14716 * ``` |
|
14717 * |
|
14718 * <div class="alert alert-warning"> |
|
14719 * **Note:** Any unspecified options will use the default value. You can see the default values below. |
|
14720 * </div> |
|
14721 * |
|
14722 * Therefore the above can be simplified as: |
|
14723 * |
|
14724 * ```js |
|
14725 * var myModule = angular.module(...); |
|
14726 * |
|
14727 * myModule.directive('directiveName', function factory(injectables) { |
|
14728 * var directiveDefinitionObject = { |
|
14729 * link: function postLink(scope, iElement, iAttrs) { ... } |
|
14730 * }; |
|
14731 * return directiveDefinitionObject; |
|
14732 * // or |
|
14733 * // return function postLink(scope, iElement, iAttrs) { ... } |
|
14734 * }); |
|
14735 * ``` |
|
14736 * |
|
14737 * |
|
14738 * |
|
14739 * ### Directive Definition Object |
|
14740 * |
|
14741 * The directive definition object provides instructions to the {@link ng.$compile |
|
14742 * compiler}. The attributes are: |
|
14743 * |
|
14744 * #### `multiElement` |
|
14745 * When this property is set to true, the HTML compiler will collect DOM nodes between |
|
14746 * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them |
|
14747 * together as the directive elements. It is recomended that this feature be used on directives |
|
14748 * which are not strictly behavioural (such as {@link api/ng.directive:ngClick ngClick}), and which |
|
14749 * do not manipulate or replace child nodes (such as {@link api/ng.directive:ngInclude ngInclude}). |
|
14750 * |
|
14751 * #### `priority` |
|
14752 * When there are multiple directives defined on a single DOM element, sometimes it |
|
14753 * is necessary to specify the order in which the directives are applied. The `priority` is used |
|
14754 * to sort the directives before their `compile` functions get called. Priority is defined as a |
|
14755 * number. Directives with greater numerical `priority` are compiled first. Pre-link functions |
|
14756 * are also run in priority order, but post-link functions are run in reverse order. The order |
|
14757 * of directives with the same priority is undefined. The default priority is `0`. |
|
14758 * |
|
14759 * #### `terminal` |
|
14760 * If set to true then the current `priority` will be the last set of directives |
|
14761 * which will execute (any directives at the current priority will still execute |
|
14762 * as the order of execution on same `priority` is undefined). |
|
14763 * |
|
14764 * #### `scope` |
|
14765 * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the |
|
14766 * same element request a new scope, only one new scope is created. The new scope rule does not |
|
14767 * apply for the root of the template since the root of the template always gets a new scope. |
|
14768 * |
|
14769 * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from |
|
14770 * normal scope in that it does not prototypically inherit from the parent scope. This is useful |
|
14771 * when creating reusable components, which should not accidentally read or modify data in the |
|
14772 * parent scope. |
|
14773 * |
|
14774 * The 'isolate' scope takes an object hash which defines a set of local scope properties |
|
14775 * derived from the parent scope. These local properties are useful for aliasing values for |
|
14776 * templates. Locals definition is a hash of local scope property to its source: |
|
14777 * |
|
14778 * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is |
|
14779 * always a string since DOM attributes are strings. If no `attr` name is specified then the |
|
14780 * attribute name is assumed to be the same as the local name. |
|
14781 * Given `<widget my-attr="hello {{name}}">` and widget definition |
|
14782 * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect |
|
14783 * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the |
|
14784 * `localName` property on the widget scope. The `name` is read from the parent scope (not |
|
14785 * component scope). |
|
14786 * |
|
14787 * * `=` or `=attr` - set up bi-directional binding between a local scope property and the |
|
14788 * parent scope property of name defined via the value of the `attr` attribute. If no `attr` |
|
14789 * name is specified then the attribute name is assumed to be the same as the local name. |
|
14790 * Given `<widget my-attr="parentModel">` and widget definition of |
|
14791 * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the |
|
14792 * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected |
|
14793 * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent |
|
14794 * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You |
|
14795 * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. |
|
14796 * |
|
14797 * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. |
|
14798 * If no `attr` name is specified then the attribute name is assumed to be the same as the |
|
14799 * local name. Given `<widget my-attr="count = count + value">` and widget definition of |
|
14800 * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to |
|
14801 * a function wrapper for the `count = count + value` expression. Often it's desirable to |
|
14802 * pass data from the isolated scope via an expression to the parent scope, this can be |
|
14803 * done by passing a map of local variable names and values into the expression wrapper fn. |
|
14804 * For example, if the expression is `increment(amount)` then we can specify the amount value |
|
14805 * by calling the `localFn` as `localFn({amount: 22})`. |
|
14806 * |
|
14807 * |
|
14808 * #### `bindToController` |
|
14809 * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController` will |
|
14810 * allow a component to have its properties bound to the controller, rather than to scope. When the controller |
|
14811 * is instantiated, the initial values of the isolate scope bindings are already available. |
|
14812 * |
|
14813 * #### `controller` |
|
14814 * Controller constructor function. The controller is instantiated before the |
|
14815 * pre-linking phase and it is shared with other directives (see |
|
14816 * `require` attribute). This allows the directives to communicate with each other and augment |
|
14817 * each other's behavior. The controller is injectable (and supports bracket notation) with the following locals: |
|
14818 * |
|
14819 * * `$scope` - Current scope associated with the element |
|
14820 * * `$element` - Current element |
|
14821 * * `$attrs` - Current attributes object for the element |
|
14822 * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope: |
|
14823 * `function([scope], cloneLinkingFn, futureParentElement)`. |
|
14824 * * `scope`: optional argument to override the scope. |
|
14825 * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content. |
|
14826 * * `futureParentElement`: |
|
14827 * * defines the parent to which the `cloneLinkingFn` will add the cloned elements. |
|
14828 * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`. |
|
14829 * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) |
|
14830 * and when the `cloneLinkinFn` is passed, |
|
14831 * as those elements need to created and cloned in a special way when they are defined outside their |
|
14832 * usual containers (e.g. like `<svg>`). |
|
14833 * * See also the `directive.templateNamespace` property. |
|
14834 * |
|
14835 * |
|
14836 * #### `require` |
|
14837 * Require another directive and inject its controller as the fourth argument to the linking function. The |
|
14838 * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the |
|
14839 * injected argument will be an array in corresponding order. If no such directive can be |
|
14840 * found, or if the directive does not have a controller, then an error is raised. The name can be prefixed with: |
|
14841 * |
|
14842 * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. |
|
14843 * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. |
|
14844 * * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found. |
|
14845 * * `^^` - Locate the required controller by searching the element's parents. Throw an error if not found. |
|
14846 * * `?^` - Attempt to locate the required controller by searching the element and its parents or pass |
|
14847 * `null` to the `link` fn if not found. |
|
14848 * * `?^^` - Attempt to locate the required controller by searching the element's parents, or pass |
|
14849 * `null` to the `link` fn if not found. |
|
14850 * |
|
14851 * |
|
14852 * #### `controllerAs` |
|
14853 * Controller alias at the directive scope. An alias for the controller so it |
|
14854 * can be referenced at the directive template. The directive needs to define a scope for this |
|
14855 * configuration to be used. Useful in the case when directive is used as component. |
|
14856 * |
|
14857 * |
|
14858 * #### `restrict` |
|
14859 * String of subset of `EACM` which restricts the directive to a specific directive |
|
14860 * declaration style. If omitted, the defaults (elements and attributes) are used. |
|
14861 * |
|
14862 * * `E` - Element name (default): `<my-directive></my-directive>` |
|
14863 * * `A` - Attribute (default): `<div my-directive="exp"></div>` |
|
14864 * * `C` - Class: `<div class="my-directive: exp;"></div>` |
|
14865 * * `M` - Comment: `<!-- directive: my-directive exp -->` |
|
14866 * |
|
14867 * |
|
14868 * #### `templateNamespace` |
|
14869 * String representing the document type used by the markup in the template. |
|
14870 * AngularJS needs this information as those elements need to be created and cloned |
|
14871 * in a special way when they are defined outside their usual containers like `<svg>` and `<math>`. |
|
14872 * |
|
14873 * * `html` - All root nodes in the template are HTML. Root nodes may also be |
|
14874 * top-level elements such as `<svg>` or `<math>`. |
|
14875 * * `svg` - The root nodes in the template are SVG elements (excluding `<math>`). |
|
14876 * * `math` - The root nodes in the template are MathML elements (excluding `<svg>`). |
|
14877 * |
|
14878 * If no `templateNamespace` is specified, then the namespace is considered to be `html`. |
|
14879 * |
|
14880 * #### `template` |
|
14881 * HTML markup that may: |
|
14882 * * Replace the contents of the directive's element (default). |
|
14883 * * Replace the directive's element itself (if `replace` is true - DEPRECATED). |
|
14884 * * Wrap the contents of the directive's element (if `transclude` is true). |
|
14885 * |
|
14886 * Value may be: |
|
14887 * |
|
14888 * * A string. For example `<div red-on-hover>{{delete_str}}</div>`. |
|
14889 * * A function which takes two arguments `tElement` and `tAttrs` (described in the `compile` |
|
14890 * function api below) and returns a string value. |
|
14891 * |
|
14892 * |
|
14893 * #### `templateUrl` |
|
14894 * This is similar to `template` but the template is loaded from the specified URL, asynchronously. |
|
14895 * |
|
14896 * Because template loading is asynchronous the compiler will suspend compilation of directives on that element |
|
14897 * for later when the template has been resolved. In the meantime it will continue to compile and link |
|
14898 * sibling and parent elements as though this element had not contained any directives. |
|
14899 * |
|
14900 * The compiler does not suspend the entire compilation to wait for templates to be loaded because this |
|
14901 * would result in the whole app "stalling" until all templates are loaded asynchronously - even in the |
|
14902 * case when only one deeply nested directive has `templateUrl`. |
|
14903 * |
|
14904 * Template loading is asynchronous even if the template has been preloaded into the {@link $templateCache} |
|
14905 * |
|
14906 * You can specify `templateUrl` as a string representing the URL or as a function which takes two |
|
14907 * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns |
|
14908 * a string value representing the url. In either case, the template URL is passed through {@link |
|
14909 * api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. |
|
14910 * |
|
14911 * |
|
14912 * #### `replace` ([*DEPRECATED*!], will be removed in next major release - i.e. v2.0) |
|
14913 * specify what the template should replace. Defaults to `false`. |
|
14914 * |
|
14915 * * `true` - the template will replace the directive's element. |
|
14916 * * `false` - the template will replace the contents of the directive's element. |
|
14917 * |
|
14918 * The replacement process migrates all of the attributes / classes from the old element to the new |
|
14919 * one. See the {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive |
|
14920 * Directives Guide} for an example. |
|
14921 * |
|
14922 * There are very few scenarios where element replacement is required for the application function, |
|
14923 * the main one being reusable custom components that are used within SVG contexts |
|
14924 * (because SVG doesn't work with custom elements in the DOM tree). |
|
14925 * |
|
14926 * #### `transclude` |
|
14927 * Extract the contents of the element where the directive appears and make it available to the directive. |
|
14928 * The contents are compiled and provided to the directive as a **transclusion function**. See the |
|
14929 * {@link $compile#transclusion Transclusion} section below. |
|
14930 * |
|
14931 * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the |
|
14932 * directive's element or the entire element: |
|
14933 * |
|
14934 * * `true` - transclude the content (i.e. the child nodes) of the directive's element. |
|
14935 * * `'element'` - transclude the whole of the directive's element including any directives on this |
|
14936 * element that defined at a lower priority than this directive. When used, the `template` |
|
14937 * property is ignored. |
|
14938 * |
|
14939 * |
|
14940 * #### `compile` |
|
14941 * |
|
14942 * ```js |
|
14943 * function compile(tElement, tAttrs, transclude) { ... } |
|
14944 * ``` |
|
14945 * |
|
14946 * The compile function deals with transforming the template DOM. Since most directives do not do |
|
14947 * template transformation, it is not used often. The compile function takes the following arguments: |
|
14948 * |
|
14949 * * `tElement` - template element - The element where the directive has been declared. It is |
|
14950 * safe to do template transformation on the element and child elements only. |
|
14951 * |
|
14952 * * `tAttrs` - template attributes - Normalized list of attributes declared on this element shared |
|
14953 * between all directive compile functions. |
|
14954 * |
|
14955 * * `transclude` - [*DEPRECATED*!] A transclude linking function: `function(scope, cloneLinkingFn)` |
|
14956 * |
|
14957 * <div class="alert alert-warning"> |
|
14958 * **Note:** The template instance and the link instance may be different objects if the template has |
|
14959 * been cloned. For this reason it is **not** safe to do anything other than DOM transformations that |
|
14960 * apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration |
|
14961 * should be done in a linking function rather than in a compile function. |
|
14962 * </div> |
|
14963 |
|
14964 * <div class="alert alert-warning"> |
|
14965 * **Note:** The compile function cannot handle directives that recursively use themselves in their |
|
14966 * own templates or compile functions. Compiling these directives results in an infinite loop and a |
|
14967 * stack overflow errors. |
|
14968 * |
|
14969 * This can be avoided by manually using $compile in the postLink function to imperatively compile |
|
14970 * a directive's template instead of relying on automatic template compilation via `template` or |
|
14971 * `templateUrl` declaration or manual compilation inside the compile function. |
|
14972 * </div> |
|
14973 * |
|
14974 * <div class="alert alert-error"> |
|
14975 * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it |
|
14976 * e.g. does not know about the right outer scope. Please use the transclude function that is passed |
|
14977 * to the link function instead. |
|
14978 * </div> |
|
14979 |
|
14980 * A compile function can have a return value which can be either a function or an object. |
|
14981 * |
|
14982 * * returning a (post-link) function - is equivalent to registering the linking function via the |
|
14983 * `link` property of the config object when the compile function is empty. |
|
14984 * |
|
14985 * * returning an object with function(s) registered via `pre` and `post` properties - allows you to |
|
14986 * control when a linking function should be called during the linking phase. See info about |
|
14987 * pre-linking and post-linking functions below. |
|
14988 * |
|
14989 * |
|
14990 * #### `link` |
|
14991 * This property is used only if the `compile` property is not defined. |
|
14992 * |
|
14993 * ```js |
|
14994 * function link(scope, iElement, iAttrs, controller, transcludeFn) { ... } |
|
14995 * ``` |
|
14996 * |
|
14997 * The link function is responsible for registering DOM listeners as well as updating the DOM. It is |
|
14998 * executed after the template has been cloned. This is where most of the directive logic will be |
|
14999 * put. |
|
15000 * |
|
15001 * * `scope` - {@link ng.$rootScope.Scope Scope} - The scope to be used by the |
|
15002 * directive for registering {@link ng.$rootScope.Scope#$watch watches}. |
|
15003 * |
|
15004 * * `iElement` - instance element - The element where the directive is to be used. It is safe to |
|
15005 * manipulate the children of the element only in `postLink` function since the children have |
|
15006 * already been linked. |
|
15007 * |
|
15008 * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared |
|
15009 * between all directive linking functions. |
|
15010 * |
|
15011 * * `controller` - a controller instance - A controller instance if at least one directive on the |
|
15012 * element defines a controller. The controller is shared among all the directives, which allows |
|
15013 * the directives to use the controllers as a communication channel. |
|
15014 * |
|
15015 * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. |
|
15016 * This is the same as the `$transclude` |
|
15017 * parameter of directive controllers, see there for details. |
|
15018 * `function([scope], cloneLinkingFn, futureParentElement)`. |
|
15019 * |
|
15020 * #### Pre-linking function |
|
15021 * |
|
15022 * Executed before the child elements are linked. Not safe to do DOM transformation since the |
|
15023 * compiler linking function will fail to locate the correct elements for linking. |
|
15024 * |
|
15025 * #### Post-linking function |
|
15026 * |
|
15027 * Executed after the child elements are linked. |
|
15028 * |
|
15029 * Note that child elements that contain `templateUrl` directives will not have been compiled |
|
15030 * and linked since they are waiting for their template to load asynchronously and their own |
|
15031 * compilation and linking has been suspended until that occurs. |
|
15032 * |
|
15033 * It is safe to do DOM transformation in the post-linking function on elements that are not waiting |
|
15034 * for their async templates to be resolved. |
|
15035 * |
|
15036 * |
|
15037 * ### Transclusion |
|
15038 * |
|
15039 * Transclusion is the process of extracting a collection of DOM element from one part of the DOM and |
|
15040 * copying them to another part of the DOM, while maintaining their connection to the original AngularJS |
|
15041 * scope from where they were taken. |
|
15042 * |
|
15043 * Transclusion is used (often with {@link ngTransclude}) to insert the |
|
15044 * original contents of a directive's element into a specified place in the template of the directive. |
|
15045 * The benefit of transclusion, over simply moving the DOM elements manually, is that the transcluded |
|
15046 * content has access to the properties on the scope from which it was taken, even if the directive |
|
15047 * has isolated scope. |
|
15048 * See the {@link guide/directive#creating-a-directive-that-wraps-other-elements Directives Guide}. |
|
15049 * |
|
15050 * This makes it possible for the widget to have private state for its template, while the transcluded |
|
15051 * content has access to its originating scope. |
|
15052 * |
|
15053 * <div class="alert alert-warning"> |
|
15054 * **Note:** When testing an element transclude directive you must not place the directive at the root of the |
|
15055 * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives |
|
15056 * Testing Transclusion Directives}. |
|
15057 * </div> |
|
15058 * |
|
15059 * #### Transclusion Functions |
|
15060 * |
|
15061 * When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion |
|
15062 * function** to the directive's `link` function and `controller`. This transclusion function is a special |
|
15063 * **linking function** that will return the compiled contents linked to a new transclusion scope. |
|
15064 * |
|
15065 * <div class="alert alert-info"> |
|
15066 * If you are just using {@link ngTransclude} then you don't need to worry about this function, since |
|
15067 * ngTransclude will deal with it for us. |
|
15068 * </div> |
|
15069 * |
|
15070 * If you want to manually control the insertion and removal of the transcluded content in your directive |
|
15071 * then you must use this transclude function. When you call a transclude function it returns a a jqLite/JQuery |
|
15072 * object that contains the compiled DOM, which is linked to the correct transclusion scope. |
|
15073 * |
|
15074 * When you call a transclusion function you can pass in a **clone attach function**. This function is accepts |
|
15075 * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded |
|
15076 * content and the `scope` is the newly created transclusion scope, to which the clone is bound. |
|
15077 * |
|
15078 * <div class="alert alert-info"> |
|
15079 * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function |
|
15080 * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope. |
|
15081 * </div> |
|
15082 * |
|
15083 * It is normal practice to attach your transcluded content (`clone`) to the DOM inside your **clone |
|
15084 * attach function**: |
|
15085 * |
|
15086 * ```js |
|
15087 * var transcludedContent, transclusionScope; |
|
15088 * |
|
15089 * $transclude(function(clone, scope) { |
|
15090 * element.append(clone); |
|
15091 * transcludedContent = clone; |
|
15092 * transclusionScope = scope; |
|
15093 * }); |
|
15094 * ``` |
|
15095 * |
|
15096 * Later, if you want to remove the transcluded content from your DOM then you should also destroy the |
|
15097 * associated transclusion scope: |
|
15098 * |
|
15099 * ```js |
|
15100 * transcludedContent.remove(); |
|
15101 * transclusionScope.$destroy(); |
|
15102 * ``` |
|
15103 * |
|
15104 * <div class="alert alert-info"> |
|
15105 * **Best Practice**: if you intend to add and remove transcluded content manually in your directive |
|
15106 * (by calling the transclude function to get the DOM and and calling `element.remove()` to remove it), |
|
15107 * then you are also responsible for calling `$destroy` on the transclusion scope. |
|
15108 * </div> |
|
15109 * |
|
15110 * The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat} |
|
15111 * automatically destroy their transluded clones as necessary so you do not need to worry about this if |
|
15112 * you are simply using {@link ngTransclude} to inject the transclusion into your directive. |
|
15113 * |
|
15114 * |
|
15115 * #### Transclusion Scopes |
|
15116 * |
|
15117 * When you call a transclude function it returns a DOM fragment that is pre-bound to a **transclusion |
|
15118 * scope**. This scope is special, in that it is a child of the directive's scope (and so gets destroyed |
|
15119 * when the directive's scope gets destroyed) but it inherits the properties of the scope from which it |
|
15120 * was taken. |
|
15121 * |
|
15122 * For example consider a directive that uses transclusion and isolated scope. The DOM hierarchy might look |
|
15123 * like this: |
|
15124 * |
|
15125 * ```html |
|
15126 * <div ng-app> |
|
15127 * <div isolate> |
|
15128 * <div transclusion> |
|
15129 * </div> |
|
15130 * </div> |
|
15131 * </div> |
|
15132 * ``` |
|
15133 * |
|
15134 * The `$parent` scope hierarchy will look like this: |
|
15135 * |
|
15136 * ``` |
|
15137 * - $rootScope |
|
15138 * - isolate |
|
15139 * - transclusion |
|
15140 * ``` |
|
15141 * |
|
15142 * but the scopes will inherit prototypically from different scopes to their `$parent`. |
|
15143 * |
|
15144 * ``` |
|
15145 * - $rootScope |
|
15146 * - transclusion |
|
15147 * - isolate |
|
15148 * ``` |
|
15149 * |
|
15150 * |
|
15151 * ### Attributes |
|
15152 * |
|
15153 * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the |
|
15154 * `link()` or `compile()` functions. It has a variety of uses. |
|
15155 * |
|
15156 * accessing *Normalized attribute names:* |
|
15157 * Directives like 'ngBind' can be expressed in many ways: 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. |
|
15158 * the attributes object allows for normalized access to |
|
15159 * the attributes. |
|
15160 * |
|
15161 * * *Directive inter-communication:* All directives share the same instance of the attributes |
|
15162 * object which allows the directives to use the attributes object as inter directive |
|
15163 * communication. |
|
15164 * |
|
15165 * * *Supports interpolation:* Interpolation attributes are assigned to the attribute object |
|
15166 * allowing other directives to read the interpolated value. |
|
15167 * |
|
15168 * * *Observing interpolated attributes:* Use `$observe` to observe the value changes of attributes |
|
15169 * that contain interpolation (e.g. `src="{{bar}}"`). Not only is this very efficient but it's also |
|
15170 * the only way to easily get the actual value because during the linking phase the interpolation |
|
15171 * hasn't been evaluated yet and so the value is at this time set to `undefined`. |
|
15172 * |
|
15173 * ```js |
|
15174 * function linkingFn(scope, elm, attrs, ctrl) { |
|
15175 * // get the attribute value |
|
15176 * console.log(attrs.ngModel); |
|
15177 * |
|
15178 * // change the attribute |
|
15179 * attrs.$set('ngModel', 'new value'); |
|
15180 * |
|
15181 * // observe changes to interpolated attribute |
|
15182 * attrs.$observe('ngModel', function(value) { |
|
15183 * console.log('ngModel has changed value to ' + value); |
|
15184 * }); |
|
15185 * } |
|
15186 * ``` |
|
15187 * |
|
15188 * ## Example |
|
15189 * |
|
15190 * <div class="alert alert-warning"> |
|
15191 * **Note**: Typically directives are registered with `module.directive`. The example below is |
|
15192 * to illustrate how `$compile` works. |
|
15193 * </div> |
|
15194 * |
|
15195 <example module="compileExample"> |
|
15196 <file name="index.html"> |
|
15197 <script> |
|
15198 angular.module('compileExample', [], function($compileProvider) { |
|
15199 // configure new 'compile' directive by passing a directive |
|
15200 // factory function. The factory function injects the '$compile' |
|
15201 $compileProvider.directive('compile', function($compile) { |
|
15202 // directive factory creates a link function |
|
15203 return function(scope, element, attrs) { |
|
15204 scope.$watch( |
|
15205 function(scope) { |
|
15206 // watch the 'compile' expression for changes |
|
15207 return scope.$eval(attrs.compile); |
|
15208 }, |
|
15209 function(value) { |
|
15210 // when the 'compile' expression changes |
|
15211 // assign it into the current DOM |
|
15212 element.html(value); |
|
15213 |
|
15214 // compile the new DOM and link it to the current |
|
15215 // scope. |
|
15216 // NOTE: we only compile .childNodes so that |
|
15217 // we don't get into infinite loop compiling ourselves |
|
15218 $compile(element.contents())(scope); |
|
15219 } |
|
15220 ); |
|
15221 }; |
|
15222 }); |
|
15223 }) |
|
15224 .controller('GreeterController', ['$scope', function($scope) { |
|
15225 $scope.name = 'Angular'; |
|
15226 $scope.html = 'Hello {{name}}'; |
|
15227 }]); |
|
15228 </script> |
|
15229 <div ng-controller="GreeterController"> |
|
15230 <input ng-model="name"> <br> |
|
15231 <textarea ng-model="html"></textarea> <br> |
|
15232 <div compile="html"></div> |
|
15233 </div> |
|
15234 </file> |
|
15235 <file name="protractor.js" type="protractor"> |
|
15236 it('should auto compile', function() { |
|
15237 var textarea = $('textarea'); |
|
15238 var output = $('div[compile]'); |
|
15239 // The initial state reads 'Hello Angular'. |
|
15240 expect(output.getText()).toBe('Hello Angular'); |
|
15241 textarea.clear(); |
|
15242 textarea.sendKeys('{{name}}!'); |
|
15243 expect(output.getText()).toBe('Angular!'); |
|
15244 }); |
|
15245 </file> |
|
15246 </example> |
|
15247 |
|
15248 * |
|
15249 * |
|
15250 * @param {string|DOMElement} element Element or HTML string to compile into a template function. |
|
15251 * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives. |
|
15252 * @param {number} maxPriority only apply directives lower than given priority (Only effects the |
|
15253 * root element(s), not their children) |
|
15254 * @returns {function(scope, cloneAttachFn=)} a link function which is used to bind template |
|
15255 * (a DOM element/tree) to a scope. Where: |
|
15256 * |
|
15257 * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. |
|
15258 * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the |
|
15259 * `template` and call the `cloneAttachFn` function allowing the caller to attach the |
|
15260 * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is |
|
15261 * called as: <br> `cloneAttachFn(clonedElement, scope)` where: |
|
15262 * |
|
15263 * * `clonedElement` - is a clone of the original `element` passed into the compiler. |
|
15264 * * `scope` - is the current scope with which the linking function is working with. |
|
15265 * |
|
15266 * Calling the linking function returns the element of the template. It is either the original |
|
15267 * element passed in, or the clone of the element if the `cloneAttachFn` is provided. |
|
15268 * |
|
15269 * After linking the view is not updated until after a call to $digest which typically is done by |
|
15270 * Angular automatically. |
|
15271 * |
|
15272 * If you need access to the bound view, there are two ways to do it: |
|
15273 * |
|
15274 * - If you are not asking the linking function to clone the template, create the DOM element(s) |
|
15275 * before you send them to the compiler and keep this reference around. |
|
15276 * ```js |
|
15277 * var element = $compile('<p>{{total}}</p>')(scope); |
|
15278 * ``` |
|
15279 * |
|
15280 * - if on the other hand, you need the element to be cloned, the view reference from the original |
|
15281 * example would not point to the clone, but rather to the original template that was cloned. In |
|
15282 * this case, you can access the clone via the cloneAttachFn: |
|
15283 * ```js |
|
15284 * var templateElement = angular.element('<p>{{total}}</p>'), |
|
15285 * scope = ....; |
|
15286 * |
|
15287 * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) { |
|
15288 * //attach the clone to DOM document at the right place |
|
15289 * }); |
|
15290 * |
|
15291 * //now we have reference to the cloned DOM via `clonedElement` |
|
15292 * ``` |
|
15293 * |
|
15294 * |
|
15295 * For information on how the compiler works, see the |
|
15296 * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. |
|
15297 */ |
|
15298 |
|
15299 var $compileMinErr = minErr('$compile'); |
|
15300 |
|
15301 /** |
|
15302 * @ngdoc provider |
|
15303 * @name $compileProvider |
|
15304 * |
|
15305 * @description |
|
15306 */ |
|
15307 $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; |
|
15308 function $CompileProvider($provide, $$sanitizeUriProvider) { |
|
15309 var hasDirectives = {}, |
|
15310 Suffix = 'Directive', |
|
15311 COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/, |
|
15312 CLASS_DIRECTIVE_REGEXP = /(([\d\w_\-]+)(?:\:([^;]+))?;?)/, |
|
15313 ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), |
|
15314 REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; |
|
15315 |
|
15316 // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes |
|
15317 // The assumption is that future DOM event attribute names will begin with |
|
15318 // 'on' and be composed of only English letters. |
|
15319 var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; |
|
15320 |
|
15321 function parseIsolateBindings(scope, directiveName) { |
|
15322 var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; |
|
15323 |
|
15324 var bindings = {}; |
|
15325 |
|
15326 forEach(scope, function(definition, scopeName) { |
|
15327 var match = definition.match(LOCAL_REGEXP); |
|
15328 |
|
15329 if (!match) { |
|
15330 throw $compileMinErr('iscp', |
|
15331 "Invalid isolate scope definition for directive '{0}'." + |
|
15332 " Definition: {... {1}: '{2}' ...}", |
|
15333 directiveName, scopeName, definition); |
|
15334 } |
|
15335 |
|
15336 bindings[scopeName] = { |
|
15337 attrName: match[3] || scopeName, |
|
15338 mode: match[1], |
|
15339 optional: match[2] === '?' |
|
15340 }; |
|
15341 }); |
|
15342 |
|
15343 return bindings; |
|
15344 } |
|
15345 |
|
15346 /** |
|
15347 * @ngdoc method |
|
15348 * @name $compileProvider#directive |
|
15349 * @kind function |
|
15350 * |
|
15351 * @description |
|
15352 * Register a new directive with the compiler. |
|
15353 * |
|
15354 * @param {string|Object} name Name of the directive in camel-case (i.e. <code>ngBind</code> which |
|
15355 * will match as <code>ng-bind</code>), or an object map of directives where the keys are the |
|
15356 * names and the values are the factories. |
|
15357 * @param {Function|Array} directiveFactory An injectable directive factory function. See |
|
15358 * {@link guide/directive} for more info. |
|
15359 * @returns {ng.$compileProvider} Self for chaining. |
|
15360 */ |
|
15361 this.directive = function registerDirective(name, directiveFactory) { |
|
15362 assertNotHasOwnProperty(name, 'directive'); |
|
15363 if (isString(name)) { |
|
15364 assertArg(directiveFactory, 'directiveFactory'); |
|
15365 if (!hasDirectives.hasOwnProperty(name)) { |
|
15366 hasDirectives[name] = []; |
|
15367 $provide.factory(name + Suffix, ['$injector', '$exceptionHandler', |
|
15368 function($injector, $exceptionHandler) { |
|
15369 var directives = []; |
|
15370 forEach(hasDirectives[name], function(directiveFactory, index) { |
|
15371 try { |
|
15372 var directive = $injector.invoke(directiveFactory); |
|
15373 if (isFunction(directive)) { |
|
15374 directive = { compile: valueFn(directive) }; |
|
15375 } else if (!directive.compile && directive.link) { |
|
15376 directive.compile = valueFn(directive.link); |
|
15377 } |
|
15378 directive.priority = directive.priority || 0; |
|
15379 directive.index = index; |
|
15380 directive.name = directive.name || name; |
|
15381 directive.require = directive.require || (directive.controller && directive.name); |
|
15382 directive.restrict = directive.restrict || 'EA'; |
|
15383 if (isObject(directive.scope)) { |
|
15384 directive.$$isolateBindings = parseIsolateBindings(directive.scope, directive.name); |
|
15385 } |
|
15386 directives.push(directive); |
|
15387 } catch (e) { |
|
15388 $exceptionHandler(e); |
|
15389 } |
|
15390 }); |
|
15391 return directives; |
|
15392 }]); |
|
15393 } |
|
15394 hasDirectives[name].push(directiveFactory); |
|
15395 } else { |
|
15396 forEach(name, reverseParams(registerDirective)); |
|
15397 } |
|
15398 return this; |
|
15399 }; |
|
15400 |
|
15401 |
|
15402 /** |
|
15403 * @ngdoc method |
|
15404 * @name $compileProvider#aHrefSanitizationWhitelist |
|
15405 * @kind function |
|
15406 * |
|
15407 * @description |
|
15408 * Retrieves or overrides the default regular expression that is used for whitelisting of safe |
|
15409 * urls during a[href] sanitization. |
|
15410 * |
|
15411 * The sanitization is a security measure aimed at prevent XSS attacks via html links. |
|
15412 * |
|
15413 * Any url about to be assigned to a[href] via data-binding is first normalized and turned into |
|
15414 * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` |
|
15415 * regular expression. If a match is found, the original url is written into the dom. Otherwise, |
|
15416 * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. |
|
15417 * |
|
15418 * @param {RegExp=} regexp New regexp to whitelist urls with. |
|
15419 * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for |
|
15420 * chaining otherwise. |
|
15421 */ |
|
15422 this.aHrefSanitizationWhitelist = function(regexp) { |
|
15423 if (isDefined(regexp)) { |
|
15424 $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp); |
|
15425 return this; |
|
15426 } else { |
|
15427 return $$sanitizeUriProvider.aHrefSanitizationWhitelist(); |
|
15428 } |
|
15429 }; |
|
15430 |
|
15431 |
|
15432 /** |
|
15433 * @ngdoc method |
|
15434 * @name $compileProvider#imgSrcSanitizationWhitelist |
|
15435 * @kind function |
|
15436 * |
|
15437 * @description |
|
15438 * Retrieves or overrides the default regular expression that is used for whitelisting of safe |
|
15439 * urls during img[src] sanitization. |
|
15440 * |
|
15441 * The sanitization is a security measure aimed at prevent XSS attacks via html links. |
|
15442 * |
|
15443 * Any url about to be assigned to img[src] via data-binding is first normalized and turned into |
|
15444 * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` |
|
15445 * regular expression. If a match is found, the original url is written into the dom. Otherwise, |
|
15446 * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. |
|
15447 * |
|
15448 * @param {RegExp=} regexp New regexp to whitelist urls with. |
|
15449 * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for |
|
15450 * chaining otherwise. |
|
15451 */ |
|
15452 this.imgSrcSanitizationWhitelist = function(regexp) { |
|
15453 if (isDefined(regexp)) { |
|
15454 $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp); |
|
15455 return this; |
|
15456 } else { |
|
15457 return $$sanitizeUriProvider.imgSrcSanitizationWhitelist(); |
|
15458 } |
|
15459 }; |
|
15460 |
|
15461 /** |
|
15462 * @ngdoc method |
|
15463 * @name $compileProvider#debugInfoEnabled |
|
15464 * |
|
15465 * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the |
|
15466 * current debugInfoEnabled state |
|
15467 * @returns {*} current value if used as getter or itself (chaining) if used as setter |
|
15468 * |
|
15469 * @kind function |
|
15470 * |
|
15471 * @description |
|
15472 * Call this method to enable/disable various debug runtime information in the compiler such as adding |
|
15473 * binding information and a reference to the current scope on to DOM elements. |
|
15474 * If enabled, the compiler will add the following to DOM elements that have been bound to the scope |
|
15475 * * `ng-binding` CSS class |
|
15476 * * `$binding` data property containing an array of the binding expressions |
|
15477 * |
|
15478 * You may want to use this in production for a significant performance boost. See |
|
15479 * {@link guide/production#disabling-debug-data Disabling Debug Data} for more. |
|
15480 * |
|
15481 * The default value is true. |
|
15482 */ |
|
15483 var debugInfoEnabled = true; |
|
15484 this.debugInfoEnabled = function(enabled) { |
|
15485 if(isDefined(enabled)) { |
|
15486 debugInfoEnabled = enabled; |
|
15487 return this; |
|
15488 } |
|
15489 return debugInfoEnabled; |
|
15490 }; |
|
15491 |
|
15492 this.$get = [ |
|
15493 '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', |
|
15494 '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', |
|
15495 function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, |
|
15496 $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { |
|
15497 |
|
15498 var Attributes = function(element, attributesToCopy) { |
|
15499 if (attributesToCopy) { |
|
15500 var keys = Object.keys(attributesToCopy); |
|
15501 var i, l, key; |
|
15502 |
|
15503 for (i = 0, l = keys.length; i < l; i++) { |
|
15504 key = keys[i]; |
|
15505 this[key] = attributesToCopy[key]; |
|
15506 } |
|
15507 } else { |
|
15508 this.$attr = {}; |
|
15509 } |
|
15510 |
|
15511 this.$$element = element; |
|
15512 }; |
|
15513 |
|
15514 Attributes.prototype = { |
|
15515 $normalize: directiveNormalize, |
|
15516 |
|
15517 |
|
15518 /** |
|
15519 * @ngdoc method |
|
15520 * @name $compile.directive.Attributes#$addClass |
|
15521 * @kind function |
|
15522 * |
|
15523 * @description |
|
15524 * Adds the CSS class value specified by the classVal parameter to the element. If animations |
|
15525 * are enabled then an animation will be triggered for the class addition. |
|
15526 * |
|
15527 * @param {string} classVal The className value that will be added to the element |
|
15528 */ |
|
15529 $addClass : function(classVal) { |
|
15530 if(classVal && classVal.length > 0) { |
|
15531 $animate.addClass(this.$$element, classVal); |
|
15532 } |
|
15533 }, |
|
15534 |
|
15535 /** |
|
15536 * @ngdoc method |
|
15537 * @name $compile.directive.Attributes#$removeClass |
|
15538 * @kind function |
|
15539 * |
|
15540 * @description |
|
15541 * Removes the CSS class value specified by the classVal parameter from the element. If |
|
15542 * animations are enabled then an animation will be triggered for the class removal. |
|
15543 * |
|
15544 * @param {string} classVal The className value that will be removed from the element |
|
15545 */ |
|
15546 $removeClass : function(classVal) { |
|
15547 if(classVal && classVal.length > 0) { |
|
15548 $animate.removeClass(this.$$element, classVal); |
|
15549 } |
|
15550 }, |
|
15551 |
|
15552 /** |
|
15553 * @ngdoc method |
|
15554 * @name $compile.directive.Attributes#$updateClass |
|
15555 * @kind function |
|
15556 * |
|
15557 * @description |
|
15558 * Adds and removes the appropriate CSS class values to the element based on the difference |
|
15559 * between the new and old CSS class values (specified as newClasses and oldClasses). |
|
15560 * |
|
15561 * @param {string} newClasses The current CSS className value |
|
15562 * @param {string} oldClasses The former CSS className value |
|
15563 */ |
|
15564 $updateClass : function(newClasses, oldClasses) { |
|
15565 var toAdd = tokenDifference(newClasses, oldClasses); |
|
15566 if (toAdd && toAdd.length) { |
|
15567 $animate.addClass(this.$$element, toAdd); |
|
15568 } |
|
15569 |
|
15570 var toRemove = tokenDifference(oldClasses, newClasses); |
|
15571 if (toRemove && toRemove.length) { |
|
15572 $animate.removeClass(this.$$element, toRemove); |
|
15573 } |
|
15574 }, |
|
15575 |
|
15576 /** |
|
15577 * Set a normalized attribute on the element in a way such that all directives |
|
15578 * can share the attribute. This function properly handles boolean attributes. |
|
15579 * @param {string} key Normalized key. (ie ngAttribute) |
|
15580 * @param {string|boolean} value The value to set. If `null` attribute will be deleted. |
|
15581 * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute. |
|
15582 * Defaults to true. |
|
15583 * @param {string=} attrName Optional none normalized name. Defaults to key. |
|
15584 */ |
|
15585 $set: function(key, value, writeAttr, attrName) { |
|
15586 // TODO: decide whether or not to throw an error if "class" |
|
15587 //is set through this function since it may cause $updateClass to |
|
15588 //become unstable. |
|
15589 |
|
15590 var node = this.$$element[0], |
|
15591 booleanKey = getBooleanAttrName(node, key), |
|
15592 aliasedKey = getAliasedAttrName(node, key), |
|
15593 observer = key, |
|
15594 normalizedVal, |
|
15595 nodeName; |
|
15596 |
|
15597 if (booleanKey) { |
|
15598 this.$$element.prop(key, value); |
|
15599 attrName = booleanKey; |
|
15600 } else if(aliasedKey) { |
|
15601 this[aliasedKey] = value; |
|
15602 observer = aliasedKey; |
|
15603 } |
|
15604 |
|
15605 this[key] = value; |
|
15606 |
|
15607 // translate normalized key to actual key |
|
15608 if (attrName) { |
|
15609 this.$attr[key] = attrName; |
|
15610 } else { |
|
15611 attrName = this.$attr[key]; |
|
15612 if (!attrName) { |
|
15613 this.$attr[key] = attrName = snake_case(key, '-'); |
|
15614 } |
|
15615 } |
|
15616 |
|
15617 nodeName = nodeName_(this.$$element); |
|
15618 |
|
15619 if ((nodeName === 'a' && key === 'href') || |
|
15620 (nodeName === 'img' && key === 'src')) { |
|
15621 // sanitize a[href] and img[src] values |
|
15622 this[key] = value = $$sanitizeUri(value, key === 'src'); |
|
15623 } else if (nodeName === 'img' && key === 'srcset') { |
|
15624 // sanitize img[srcset] values |
|
15625 var result = ""; |
|
15626 |
|
15627 // first check if there are spaces because it's not the same pattern |
|
15628 var trimmedSrcset = trim(value); |
|
15629 // ( 999x ,| 999w ,| ,|, ) |
|
15630 var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/; |
|
15631 var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/; |
|
15632 |
|
15633 // split srcset into tuple of uri and descriptor except for the last item |
|
15634 var rawUris = trimmedSrcset.split(pattern); |
|
15635 |
|
15636 // for each tuples |
|
15637 var nbrUrisWith2parts = Math.floor(rawUris.length / 2); |
|
15638 for (var i=0; i<nbrUrisWith2parts; i++) { |
|
15639 var innerIdx = i*2; |
|
15640 // sanitize the uri |
|
15641 result += $$sanitizeUri(trim( rawUris[innerIdx]), true); |
|
15642 // add the descriptor |
|
15643 result += ( " " + trim(rawUris[innerIdx+1])); |
|
15644 } |
|
15645 |
|
15646 // split the last item into uri and descriptor |
|
15647 var lastTuple = trim(rawUris[i*2]).split(/\s/); |
|
15648 |
|
15649 // sanitize the last uri |
|
15650 result += $$sanitizeUri(trim(lastTuple[0]), true); |
|
15651 |
|
15652 // and add the last descriptor if any |
|
15653 if( lastTuple.length === 2) { |
|
15654 result += (" " + trim(lastTuple[1])); |
|
15655 } |
|
15656 this[key] = value = result; |
|
15657 } |
|
15658 |
|
15659 if (writeAttr !== false) { |
|
15660 if (value === null || value === undefined) { |
|
15661 this.$$element.removeAttr(attrName); |
|
15662 } else { |
|
15663 this.$$element.attr(attrName, value); |
|
15664 } |
|
15665 } |
|
15666 |
|
15667 // fire observers |
|
15668 var $$observers = this.$$observers; |
|
15669 $$observers && forEach($$observers[observer], function(fn) { |
|
15670 try { |
|
15671 fn(value); |
|
15672 } catch (e) { |
|
15673 $exceptionHandler(e); |
|
15674 } |
|
15675 }); |
|
15676 }, |
|
15677 |
|
15678 |
|
15679 /** |
|
15680 * @ngdoc method |
|
15681 * @name $compile.directive.Attributes#$observe |
|
15682 * @kind function |
|
15683 * |
|
15684 * @description |
|
15685 * Observes an interpolated attribute. |
|
15686 * |
|
15687 * The observer function will be invoked once during the next `$digest` following |
|
15688 * compilation. The observer is then invoked whenever the interpolated value |
|
15689 * changes. |
|
15690 * |
|
15691 * @param {string} key Normalized key. (ie ngAttribute) . |
|
15692 * @param {function(interpolatedValue)} fn Function that will be called whenever |
|
15693 the interpolated value of the attribute changes. |
|
15694 * See {@link ng.$compile#attributes $compile} for more info. |
|
15695 * @returns {function()} Returns a deregistration function for this observer. |
|
15696 */ |
|
15697 $observe: function(key, fn) { |
|
15698 var attrs = this, |
|
15699 $$observers = (attrs.$$observers || (attrs.$$observers = Object.create(null))), |
|
15700 listeners = ($$observers[key] || ($$observers[key] = [])); |
|
15701 |
|
15702 listeners.push(fn); |
|
15703 $rootScope.$evalAsync(function() { |
|
15704 if (!listeners.$$inter) { |
|
15705 // no one registered attribute interpolation function, so lets call it manually |
|
15706 fn(attrs[key]); |
|
15707 } |
|
15708 }); |
|
15709 |
|
15710 return function() { |
|
15711 arrayRemove(listeners, fn); |
|
15712 }; |
|
15713 } |
|
15714 }; |
|
15715 |
|
15716 |
|
15717 function safeAddClass($element, className) { |
|
15718 try { |
|
15719 $element.addClass(className); |
|
15720 } catch(e) { |
|
15721 // ignore, since it means that we are trying to set class on |
|
15722 // SVG element, where class name is read-only. |
|
15723 } |
|
15724 } |
|
15725 |
|
15726 |
|
15727 var startSymbol = $interpolate.startSymbol(), |
|
15728 endSymbol = $interpolate.endSymbol(), |
|
15729 denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') |
|
15730 ? identity |
|
15731 : function denormalizeTemplate(template) { |
|
15732 return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); |
|
15733 }, |
|
15734 NG_ATTR_BINDING = /^ngAttr[A-Z]/; |
|
15735 |
|
15736 compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { |
|
15737 var bindings = $element.data('$binding') || []; |
|
15738 |
|
15739 if (isArray(binding)) { |
|
15740 bindings = bindings.concat(binding); |
|
15741 } else { |
|
15742 bindings.push(binding); |
|
15743 } |
|
15744 |
|
15745 $element.data('$binding', bindings); |
|
15746 } : noop; |
|
15747 |
|
15748 compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) { |
|
15749 safeAddClass($element, 'ng-binding'); |
|
15750 } : noop; |
|
15751 |
|
15752 compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) { |
|
15753 var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; |
|
15754 $element.data(dataName, scope); |
|
15755 } : noop; |
|
15756 |
|
15757 compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) { |
|
15758 safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); |
|
15759 } : noop; |
|
15760 |
|
15761 return compile; |
|
15762 |
|
15763 //================================ |
|
15764 |
|
15765 function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, |
|
15766 previousCompileContext) { |
|
15767 if (!($compileNodes instanceof jqLite)) { |
|
15768 // jquery always rewraps, whereas we need to preserve the original selector so that we can |
|
15769 // modify it. |
|
15770 $compileNodes = jqLite($compileNodes); |
|
15771 } |
|
15772 // We can not compile top level text elements since text nodes can be merged and we will |
|
15773 // not be able to attach scope data to them, so we will wrap them in <span> |
|
15774 forEach($compileNodes, function(node, index){ |
|
15775 if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */ ) { |
|
15776 $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0]; |
|
15777 } |
|
15778 }); |
|
15779 var compositeLinkFn = |
|
15780 compileNodes($compileNodes, transcludeFn, $compileNodes, |
|
15781 maxPriority, ignoreDirective, previousCompileContext); |
|
15782 compile.$$addScopeClass($compileNodes); |
|
15783 var namespace = null; |
|
15784 return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn, futureParentElement){ |
|
15785 assertArg(scope, 'scope'); |
|
15786 if (!namespace) { |
|
15787 namespace = detectNamespaceForChildElements(futureParentElement); |
|
15788 } |
|
15789 var $linkNode; |
|
15790 if (namespace !== 'html') { |
|
15791 // When using a directive with replace:true and templateUrl the $compileNodes |
|
15792 // (or a child element inside of them) |
|
15793 // might change, so we need to recreate the namespace adapted compileNodes |
|
15794 // for call to the link function. |
|
15795 // Note: This will already clone the nodes... |
|
15796 $linkNode = jqLite( |
|
15797 wrapTemplate(namespace, jqLite('<div>').append($compileNodes).html()) |
|
15798 ); |
|
15799 } else if (cloneConnectFn) { |
|
15800 // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart |
|
15801 // and sometimes changes the structure of the DOM. |
|
15802 $linkNode = JQLitePrototype.clone.call($compileNodes); |
|
15803 } else { |
|
15804 $linkNode = $compileNodes; |
|
15805 } |
|
15806 |
|
15807 if (transcludeControllers) { |
|
15808 for (var controllerName in transcludeControllers) { |
|
15809 $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance); |
|
15810 } |
|
15811 } |
|
15812 |
|
15813 compile.$$addScopeInfo($linkNode, scope); |
|
15814 |
|
15815 if (cloneConnectFn) cloneConnectFn($linkNode, scope); |
|
15816 if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); |
|
15817 return $linkNode; |
|
15818 }; |
|
15819 } |
|
15820 |
|
15821 function detectNamespaceForChildElements(parentElement) { |
|
15822 // TODO: Make this detect MathML as well... |
|
15823 var node = parentElement && parentElement[0]; |
|
15824 if (!node) { |
|
15825 return 'html'; |
|
15826 } else { |
|
15827 return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg': 'html'; |
|
15828 } |
|
15829 } |
|
15830 |
|
15831 /** |
|
15832 * Compile function matches each node in nodeList against the directives. Once all directives |
|
15833 * for a particular node are collected their compile functions are executed. The compile |
|
15834 * functions return values - the linking functions - are combined into a composite linking |
|
15835 * function, which is the a linking function for the node. |
|
15836 * |
|
15837 * @param {NodeList} nodeList an array of nodes or NodeList to compile |
|
15838 * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the |
|
15839 * scope argument is auto-generated to the new child of the transcluded parent scope. |
|
15840 * @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then |
|
15841 * the rootElement must be set the jqLite collection of the compile root. This is |
|
15842 * needed so that the jqLite collection items can be replaced with widgets. |
|
15843 * @param {number=} maxPriority Max directive priority. |
|
15844 * @returns {Function} A composite linking function of all of the matched directives or null. |
|
15845 */ |
|
15846 function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, |
|
15847 previousCompileContext) { |
|
15848 var linkFns = [], |
|
15849 attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; |
|
15850 |
|
15851 for (var i = 0; i < nodeList.length; i++) { |
|
15852 attrs = new Attributes(); |
|
15853 |
|
15854 // we must always refer to nodeList[i] since the nodes can be replaced underneath us. |
|
15855 directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, |
|
15856 ignoreDirective); |
|
15857 |
|
15858 nodeLinkFn = (directives.length) |
|
15859 ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, |
|
15860 null, [], [], previousCompileContext) |
|
15861 : null; |
|
15862 |
|
15863 if (nodeLinkFn && nodeLinkFn.scope) { |
|
15864 compile.$$addScopeClass(attrs.$$element); |
|
15865 } |
|
15866 |
|
15867 childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || |
|
15868 !(childNodes = nodeList[i].childNodes) || |
|
15869 !childNodes.length) |
|
15870 ? null |
|
15871 : compileNodes(childNodes, |
|
15872 nodeLinkFn ? ( |
|
15873 (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement) |
|
15874 && nodeLinkFn.transclude) : transcludeFn); |
|
15875 |
|
15876 if (nodeLinkFn || childLinkFn) { |
|
15877 linkFns.push(i, nodeLinkFn, childLinkFn); |
|
15878 linkFnFound = true; |
|
15879 nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn; |
|
15880 } |
|
15881 |
|
15882 //use the previous context only for the first element in the virtual group |
|
15883 previousCompileContext = null; |
|
15884 } |
|
15885 |
|
15886 // return a linking function if we have found anything, null otherwise |
|
15887 return linkFnFound ? compositeLinkFn : null; |
|
15888 |
|
15889 function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { |
|
15890 var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn; |
|
15891 var stableNodeList; |
|
15892 |
|
15893 |
|
15894 if (nodeLinkFnFound) { |
|
15895 // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our |
|
15896 // offsets don't get screwed up |
|
15897 var nodeListLength = nodeList.length; |
|
15898 stableNodeList = new Array(nodeListLength); |
|
15899 |
|
15900 // create a sparse array by only copying the elements which have a linkFn |
|
15901 for (i = 0; i < linkFns.length; i+=3) { |
|
15902 idx = linkFns[i]; |
|
15903 stableNodeList[idx] = nodeList[idx]; |
|
15904 } |
|
15905 } else { |
|
15906 stableNodeList = nodeList; |
|
15907 } |
|
15908 |
|
15909 for(i = 0, ii = linkFns.length; i < ii;) { |
|
15910 node = stableNodeList[linkFns[i++]]; |
|
15911 nodeLinkFn = linkFns[i++]; |
|
15912 childLinkFn = linkFns[i++]; |
|
15913 |
|
15914 if (nodeLinkFn) { |
|
15915 if (nodeLinkFn.scope) { |
|
15916 childScope = scope.$new(); |
|
15917 compile.$$addScopeInfo(jqLite(node), childScope); |
|
15918 } else { |
|
15919 childScope = scope; |
|
15920 } |
|
15921 |
|
15922 if ( nodeLinkFn.transcludeOnThisElement ) { |
|
15923 childBoundTranscludeFn = createBoundTranscludeFn( |
|
15924 scope, nodeLinkFn.transclude, parentBoundTranscludeFn, |
|
15925 nodeLinkFn.elementTranscludeOnThisElement); |
|
15926 |
|
15927 } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { |
|
15928 childBoundTranscludeFn = parentBoundTranscludeFn; |
|
15929 |
|
15930 } else if (!parentBoundTranscludeFn && transcludeFn) { |
|
15931 childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn); |
|
15932 |
|
15933 } else { |
|
15934 childBoundTranscludeFn = null; |
|
15935 } |
|
15936 |
|
15937 nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn); |
|
15938 |
|
15939 } else if (childLinkFn) { |
|
15940 childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn); |
|
15941 } |
|
15942 } |
|
15943 } |
|
15944 } |
|
15945 |
|
15946 function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) { |
|
15947 |
|
15948 var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { |
|
15949 |
|
15950 if (!transcludedScope) { |
|
15951 transcludedScope = scope.$new(false, containingScope); |
|
15952 transcludedScope.$$transcluded = true; |
|
15953 } |
|
15954 |
|
15955 return transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn, futureParentElement); |
|
15956 }; |
|
15957 |
|
15958 return boundTranscludeFn; |
|
15959 } |
|
15960 |
|
15961 /** |
|
15962 * Looks for directives on the given node and adds them to the directive collection which is |
|
15963 * sorted. |
|
15964 * |
|
15965 * @param node Node to search. |
|
15966 * @param directives An array to which the directives are added to. This array is sorted before |
|
15967 * the function returns. |
|
15968 * @param attrs The shared attrs object which is used to populate the normalized attributes. |
|
15969 * @param {number=} maxPriority Max directive priority. |
|
15970 */ |
|
15971 function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) { |
|
15972 var nodeType = node.nodeType, |
|
15973 attrsMap = attrs.$attr, |
|
15974 match, |
|
15975 className; |
|
15976 |
|
15977 switch(nodeType) { |
|
15978 case NODE_TYPE_ELEMENT: /* Element */ |
|
15979 // use the node name: <directive> |
|
15980 addDirective(directives, |
|
15981 directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); |
|
15982 |
|
15983 // iterate over the attributes |
|
15984 for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, |
|
15985 j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { |
|
15986 var attrStartName = false; |
|
15987 var attrEndName = false; |
|
15988 |
|
15989 attr = nAttrs[j]; |
|
15990 name = attr.name; |
|
15991 value = trim(attr.value); |
|
15992 |
|
15993 // support ngAttr attribute binding |
|
15994 ngAttrName = directiveNormalize(name); |
|
15995 if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { |
|
15996 name = snake_case(ngAttrName.substr(6), '-'); |
|
15997 } |
|
15998 |
|
15999 var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); |
|
16000 if (directiveIsMultiElement(directiveNName)) { |
|
16001 if (ngAttrName === directiveNName + 'Start') { |
|
16002 attrStartName = name; |
|
16003 attrEndName = name.substr(0, name.length - 5) + 'end'; |
|
16004 name = name.substr(0, name.length - 6); |
|
16005 } |
|
16006 } |
|
16007 |
|
16008 nName = directiveNormalize(name.toLowerCase()); |
|
16009 attrsMap[nName] = name; |
|
16010 if (isNgAttr || !attrs.hasOwnProperty(nName)) { |
|
16011 attrs[nName] = value; |
|
16012 if (getBooleanAttrName(node, nName)) { |
|
16013 attrs[nName] = true; // presence means true |
|
16014 } |
|
16015 } |
|
16016 addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); |
|
16017 addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, |
|
16018 attrEndName); |
|
16019 } |
|
16020 |
|
16021 // use class as directive |
|
16022 className = node.className; |
|
16023 if (isString(className) && className !== '') { |
|
16024 while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { |
|
16025 nName = directiveNormalize(match[2]); |
|
16026 if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { |
|
16027 attrs[nName] = trim(match[3]); |
|
16028 } |
|
16029 className = className.substr(match.index + match[0].length); |
|
16030 } |
|
16031 } |
|
16032 break; |
|
16033 case NODE_TYPE_TEXT: /* Text Node */ |
|
16034 addTextInterpolateDirective(directives, node.nodeValue); |
|
16035 break; |
|
16036 case NODE_TYPE_COMMENT: /* Comment */ |
|
16037 try { |
|
16038 match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); |
|
16039 if (match) { |
|
16040 nName = directiveNormalize(match[1]); |
|
16041 if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { |
|
16042 attrs[nName] = trim(match[2]); |
|
16043 } |
|
16044 } |
|
16045 } catch (e) { |
|
16046 // turns out that under some circumstances IE9 throws errors when one attempts to read |
|
16047 // comment's node value. |
|
16048 // Just ignore it and continue. (Can't seem to reproduce in test case.) |
|
16049 } |
|
16050 break; |
|
16051 } |
|
16052 |
|
16053 directives.sort(byPriority); |
|
16054 return directives; |
|
16055 } |
|
16056 |
|
16057 /** |
|
16058 * Given a node with an directive-start it collects all of the siblings until it finds |
|
16059 * directive-end. |
|
16060 * @param node |
|
16061 * @param attrStart |
|
16062 * @param attrEnd |
|
16063 * @returns {*} |
|
16064 */ |
|
16065 function groupScan(node, attrStart, attrEnd) { |
|
16066 var nodes = []; |
|
16067 var depth = 0; |
|
16068 if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { |
|
16069 var startNode = node; |
|
16070 do { |
|
16071 if (!node) { |
|
16072 throw $compileMinErr('uterdir', |
|
16073 "Unterminated attribute, found '{0}' but no matching '{1}' found.", |
|
16074 attrStart, attrEnd); |
|
16075 } |
|
16076 if (node.nodeType == NODE_TYPE_ELEMENT) { |
|
16077 if (node.hasAttribute(attrStart)) depth++; |
|
16078 if (node.hasAttribute(attrEnd)) depth--; |
|
16079 } |
|
16080 nodes.push(node); |
|
16081 node = node.nextSibling; |
|
16082 } while (depth > 0); |
|
16083 } else { |
|
16084 nodes.push(node); |
|
16085 } |
|
16086 |
|
16087 return jqLite(nodes); |
|
16088 } |
|
16089 |
|
16090 /** |
|
16091 * Wrapper for linking function which converts normal linking function into a grouped |
|
16092 * linking function. |
|
16093 * @param linkFn |
|
16094 * @param attrStart |
|
16095 * @param attrEnd |
|
16096 * @returns {Function} |
|
16097 */ |
|
16098 function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { |
|
16099 return function(scope, element, attrs, controllers, transcludeFn) { |
|
16100 element = groupScan(element[0], attrStart, attrEnd); |
|
16101 return linkFn(scope, element, attrs, controllers, transcludeFn); |
|
16102 }; |
|
16103 } |
|
16104 |
|
16105 /** |
|
16106 * Once the directives have been collected, their compile functions are executed. This method |
|
16107 * is responsible for inlining directive templates as well as terminating the application |
|
16108 * of the directives if the terminal directive has been reached. |
|
16109 * |
|
16110 * @param {Array} directives Array of collected directives to execute their compile function. |
|
16111 * this needs to be pre-sorted by priority order. |
|
16112 * @param {Node} compileNode The raw DOM node to apply the compile functions to |
|
16113 * @param {Object} templateAttrs The shared attribute function |
|
16114 * @param {function(angular.Scope, cloneAttachFn=)} transcludeFn A linking function, where the |
|
16115 * scope argument is auto-generated to the new |
|
16116 * child of the transcluded parent scope. |
|
16117 * @param {JQLite} jqCollection If we are working on the root of the compile tree then this |
|
16118 * argument has the root jqLite array so that we can replace nodes |
|
16119 * on it. |
|
16120 * @param {Object=} originalReplaceDirective An optional directive that will be ignored when |
|
16121 * compiling the transclusion. |
|
16122 * @param {Array.<Function>} preLinkFns |
|
16123 * @param {Array.<Function>} postLinkFns |
|
16124 * @param {Object} previousCompileContext Context used for previous compilation of the current |
|
16125 * node |
|
16126 * @returns {Function} linkFn |
|
16127 */ |
|
16128 function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, |
|
16129 jqCollection, originalReplaceDirective, preLinkFns, postLinkFns, |
|
16130 previousCompileContext) { |
|
16131 previousCompileContext = previousCompileContext || {}; |
|
16132 |
|
16133 var terminalPriority = -Number.MAX_VALUE, |
|
16134 newScopeDirective, |
|
16135 controllerDirectives = previousCompileContext.controllerDirectives, |
|
16136 controllers, |
|
16137 newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, |
|
16138 templateDirective = previousCompileContext.templateDirective, |
|
16139 nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, |
|
16140 hasTranscludeDirective = false, |
|
16141 hasTemplate = false, |
|
16142 hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective, |
|
16143 $compileNode = templateAttrs.$$element = jqLite(compileNode), |
|
16144 directive, |
|
16145 directiveName, |
|
16146 $template, |
|
16147 replaceDirective = originalReplaceDirective, |
|
16148 childTranscludeFn = transcludeFn, |
|
16149 linkFn, |
|
16150 directiveValue; |
|
16151 |
|
16152 // executes all directives on the current element |
|
16153 for(var i = 0, ii = directives.length; i < ii; i++) { |
|
16154 directive = directives[i]; |
|
16155 var attrStart = directive.$$start; |
|
16156 var attrEnd = directive.$$end; |
|
16157 |
|
16158 // collect multiblock sections |
|
16159 if (attrStart) { |
|
16160 $compileNode = groupScan(compileNode, attrStart, attrEnd); |
|
16161 } |
|
16162 $template = undefined; |
|
16163 |
|
16164 if (terminalPriority > directive.priority) { |
|
16165 break; // prevent further processing of directives |
|
16166 } |
|
16167 |
|
16168 if (directiveValue = directive.scope) { |
|
16169 |
|
16170 // skip the check for directives with async templates, we'll check the derived sync |
|
16171 // directive when the template arrives |
|
16172 if (!directive.templateUrl) { |
|
16173 if (isObject(directiveValue)) { |
|
16174 // This directive is trying to add an isolated scope. |
|
16175 // Check that there is no scope of any kind already |
|
16176 assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective, |
|
16177 directive, $compileNode); |
|
16178 newIsolateScopeDirective = directive; |
|
16179 } else { |
|
16180 // This directive is trying to add a child scope. |
|
16181 // Check that there is no isolated scope already |
|
16182 assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, |
|
16183 $compileNode); |
|
16184 } |
|
16185 } |
|
16186 |
|
16187 newScopeDirective = newScopeDirective || directive; |
|
16188 } |
|
16189 |
|
16190 directiveName = directive.name; |
|
16191 |
|
16192 if (!directive.templateUrl && directive.controller) { |
|
16193 directiveValue = directive.controller; |
|
16194 controllerDirectives = controllerDirectives || {}; |
|
16195 assertNoDuplicate("'" + directiveName + "' controller", |
|
16196 controllerDirectives[directiveName], directive, $compileNode); |
|
16197 controllerDirectives[directiveName] = directive; |
|
16198 } |
|
16199 |
|
16200 if (directiveValue = directive.transclude) { |
|
16201 hasTranscludeDirective = true; |
|
16202 |
|
16203 // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. |
|
16204 // This option should only be used by directives that know how to safely handle element transclusion, |
|
16205 // where the transcluded nodes are added or replaced after linking. |
|
16206 if (!directive.$$tlb) { |
|
16207 assertNoDuplicate('transclusion', nonTlbTranscludeDirective, directive, $compileNode); |
|
16208 nonTlbTranscludeDirective = directive; |
|
16209 } |
|
16210 |
|
16211 if (directiveValue == 'element') { |
|
16212 hasElementTranscludeDirective = true; |
|
16213 terminalPriority = directive.priority; |
|
16214 $template = $compileNode; |
|
16215 $compileNode = templateAttrs.$$element = |
|
16216 jqLite(document.createComment(' ' + directiveName + ': ' + |
|
16217 templateAttrs[directiveName] + ' ')); |
|
16218 compileNode = $compileNode[0]; |
|
16219 replaceWith(jqCollection, sliceArgs($template), compileNode); |
|
16220 |
|
16221 childTranscludeFn = compile($template, transcludeFn, terminalPriority, |
|
16222 replaceDirective && replaceDirective.name, { |
|
16223 // Don't pass in: |
|
16224 // - controllerDirectives - otherwise we'll create duplicates controllers |
|
16225 // - newIsolateScopeDirective or templateDirective - combining templates with |
|
16226 // element transclusion doesn't make sense. |
|
16227 // |
|
16228 // We need only nonTlbTranscludeDirective so that we prevent putting transclusion |
|
16229 // on the same element more than once. |
|
16230 nonTlbTranscludeDirective: nonTlbTranscludeDirective |
|
16231 }); |
|
16232 } else { |
|
16233 $template = jqLite(jqLiteClone(compileNode)).contents(); |
|
16234 $compileNode.empty(); // clear contents |
|
16235 childTranscludeFn = compile($template, transcludeFn); |
|
16236 } |
|
16237 } |
|
16238 |
|
16239 if (directive.template) { |
|
16240 hasTemplate = true; |
|
16241 assertNoDuplicate('template', templateDirective, directive, $compileNode); |
|
16242 templateDirective = directive; |
|
16243 |
|
16244 directiveValue = (isFunction(directive.template)) |
|
16245 ? directive.template($compileNode, templateAttrs) |
|
16246 : directive.template; |
|
16247 |
|
16248 directiveValue = denormalizeTemplate(directiveValue); |
|
16249 |
|
16250 if (directive.replace) { |
|
16251 replaceDirective = directive; |
|
16252 if (jqLiteIsTextNode(directiveValue)) { |
|
16253 $template = []; |
|
16254 } else { |
|
16255 $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue))); |
|
16256 } |
|
16257 compileNode = $template[0]; |
|
16258 |
|
16259 if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { |
|
16260 throw $compileMinErr('tplrt', |
|
16261 "Template for directive '{0}' must have exactly one root element. {1}", |
|
16262 directiveName, ''); |
|
16263 } |
|
16264 |
|
16265 replaceWith(jqCollection, $compileNode, compileNode); |
|
16266 |
|
16267 var newTemplateAttrs = {$attr: {}}; |
|
16268 |
|
16269 // combine directives from the original node and from the template: |
|
16270 // - take the array of directives for this element |
|
16271 // - split it into two parts, those that already applied (processed) and those that weren't (unprocessed) |
|
16272 // - collect directives from the template and sort them by priority |
|
16273 // - combine directives as: processed + template + unprocessed |
|
16274 var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs); |
|
16275 var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1)); |
|
16276 |
|
16277 if (newIsolateScopeDirective) { |
|
16278 markDirectivesAsIsolate(templateDirectives); |
|
16279 } |
|
16280 directives = directives.concat(templateDirectives).concat(unprocessedDirectives); |
|
16281 mergeTemplateAttributes(templateAttrs, newTemplateAttrs); |
|
16282 |
|
16283 ii = directives.length; |
|
16284 } else { |
|
16285 $compileNode.html(directiveValue); |
|
16286 } |
|
16287 } |
|
16288 |
|
16289 if (directive.templateUrl) { |
|
16290 hasTemplate = true; |
|
16291 assertNoDuplicate('template', templateDirective, directive, $compileNode); |
|
16292 templateDirective = directive; |
|
16293 |
|
16294 if (directive.replace) { |
|
16295 replaceDirective = directive; |
|
16296 } |
|
16297 |
|
16298 nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, |
|
16299 templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { |
|
16300 controllerDirectives: controllerDirectives, |
|
16301 newIsolateScopeDirective: newIsolateScopeDirective, |
|
16302 templateDirective: templateDirective, |
|
16303 nonTlbTranscludeDirective: nonTlbTranscludeDirective |
|
16304 }); |
|
16305 ii = directives.length; |
|
16306 } else if (directive.compile) { |
|
16307 try { |
|
16308 linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); |
|
16309 if (isFunction(linkFn)) { |
|
16310 addLinkFns(null, linkFn, attrStart, attrEnd); |
|
16311 } else if (linkFn) { |
|
16312 addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); |
|
16313 } |
|
16314 } catch (e) { |
|
16315 $exceptionHandler(e, startingTag($compileNode)); |
|
16316 } |
|
16317 } |
|
16318 |
|
16319 if (directive.terminal) { |
|
16320 nodeLinkFn.terminal = true; |
|
16321 terminalPriority = Math.max(terminalPriority, directive.priority); |
|
16322 } |
|
16323 |
|
16324 } |
|
16325 |
|
16326 nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; |
|
16327 nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; |
|
16328 nodeLinkFn.elementTranscludeOnThisElement = hasElementTranscludeDirective; |
|
16329 nodeLinkFn.templateOnThisElement = hasTemplate; |
|
16330 nodeLinkFn.transclude = childTranscludeFn; |
|
16331 |
|
16332 previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective; |
|
16333 |
|
16334 // might be normal or delayed nodeLinkFn depending on if templateUrl is present |
|
16335 return nodeLinkFn; |
|
16336 |
|
16337 //////////////////// |
|
16338 |
|
16339 function addLinkFns(pre, post, attrStart, attrEnd) { |
|
16340 if (pre) { |
|
16341 if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); |
|
16342 pre.require = directive.require; |
|
16343 pre.directiveName = directiveName; |
|
16344 if (newIsolateScopeDirective === directive || directive.$$isolateScope) { |
|
16345 pre = cloneAndAnnotateFn(pre, {isolateScope: true}); |
|
16346 } |
|
16347 preLinkFns.push(pre); |
|
16348 } |
|
16349 if (post) { |
|
16350 if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); |
|
16351 post.require = directive.require; |
|
16352 post.directiveName = directiveName; |
|
16353 if (newIsolateScopeDirective === directive || directive.$$isolateScope) { |
|
16354 post = cloneAndAnnotateFn(post, {isolateScope: true}); |
|
16355 } |
|
16356 postLinkFns.push(post); |
|
16357 } |
|
16358 } |
|
16359 |
|
16360 |
|
16361 function getControllers(directiveName, require, $element, elementControllers) { |
|
16362 var value, retrievalMethod = 'data', optional = false; |
|
16363 var $searchElement = $element; |
|
16364 var match; |
|
16365 if (isString(require)) { |
|
16366 match = require.match(REQUIRE_PREFIX_REGEXP); |
|
16367 require = require.substring(match[0].length); |
|
16368 |
|
16369 if (match[3]) { |
|
16370 if (match[1]) match[3] = null; |
|
16371 else match[1] = match[3]; |
|
16372 } |
|
16373 if (match[1] === '^') { |
|
16374 retrievalMethod = 'inheritedData'; |
|
16375 } else if (match[1] === '^^') { |
|
16376 retrievalMethod = 'inheritedData'; |
|
16377 $searchElement = $element.parent(); |
|
16378 } |
|
16379 if (match[2] === '?') { |
|
16380 optional = true; |
|
16381 } |
|
16382 |
|
16383 value = null; |
|
16384 |
|
16385 if (elementControllers && retrievalMethod === 'data') { |
|
16386 if (value = elementControllers[require]) { |
|
16387 value = value.instance; |
|
16388 } |
|
16389 } |
|
16390 value = value || $searchElement[retrievalMethod]('$' + require + 'Controller'); |
|
16391 |
|
16392 if (!value && !optional) { |
|
16393 throw $compileMinErr('ctreq', |
|
16394 "Controller '{0}', required by directive '{1}', can't be found!", |
|
16395 require, directiveName); |
|
16396 } |
|
16397 return value; |
|
16398 } else if (isArray(require)) { |
|
16399 value = []; |
|
16400 forEach(require, function(require) { |
|
16401 value.push(getControllers(directiveName, require, $element, elementControllers)); |
|
16402 }); |
|
16403 } |
|
16404 return value; |
|
16405 } |
|
16406 |
|
16407 |
|
16408 function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { |
|
16409 var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element, |
|
16410 attrs; |
|
16411 |
|
16412 if (compileNode === linkNode) { |
|
16413 attrs = templateAttrs; |
|
16414 $element = templateAttrs.$$element; |
|
16415 } else { |
|
16416 $element = jqLite(linkNode); |
|
16417 attrs = new Attributes($element, templateAttrs); |
|
16418 } |
|
16419 |
|
16420 if (newIsolateScopeDirective) { |
|
16421 isolateScope = scope.$new(true); |
|
16422 } |
|
16423 |
|
16424 transcludeFn = boundTranscludeFn && controllersBoundTransclude; |
|
16425 if (controllerDirectives) { |
|
16426 // TODO: merge `controllers` and `elementControllers` into single object. |
|
16427 controllers = {}; |
|
16428 elementControllers = {}; |
|
16429 forEach(controllerDirectives, function(directive) { |
|
16430 var locals = { |
|
16431 $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, |
|
16432 $element: $element, |
|
16433 $attrs: attrs, |
|
16434 $transclude: transcludeFn |
|
16435 }, controllerInstance; |
|
16436 |
|
16437 controller = directive.controller; |
|
16438 if (controller == '@') { |
|
16439 controller = attrs[directive.name]; |
|
16440 } |
|
16441 |
|
16442 controllerInstance = $controller(controller, locals, true, directive.controllerAs); |
|
16443 |
|
16444 // For directives with element transclusion the element is a comment, |
|
16445 // but jQuery .data doesn't support attaching data to comment nodes as it's hard to |
|
16446 // clean up (http://bugs.jquery.com/ticket/8335). |
|
16447 // Instead, we save the controllers for the element in a local hash and attach to .data |
|
16448 // later, once we have the actual element. |
|
16449 elementControllers[directive.name] = controllerInstance; |
|
16450 if (!hasElementTranscludeDirective) { |
|
16451 $element.data('$' + directive.name + 'Controller', controllerInstance.instance); |
|
16452 } |
|
16453 |
|
16454 controllers[directive.name] = controllerInstance; |
|
16455 }); |
|
16456 } |
|
16457 |
|
16458 if (newIsolateScopeDirective) { |
|
16459 var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; |
|
16460 |
|
16461 compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective || |
|
16462 templateDirective === newIsolateScopeDirective.$$originalDirective))); |
|
16463 compile.$$addScopeClass($element, true); |
|
16464 |
|
16465 var isolateScopeController = controllers && controllers[newIsolateScopeDirective.name]; |
|
16466 var isolateBindingContext = isolateScope; |
|
16467 if (isolateScopeController && isolateScopeController.identifier && |
|
16468 newIsolateScopeDirective.bindToController === true) { |
|
16469 isolateBindingContext = isolateScopeController.instance; |
|
16470 } |
|
16471 |
|
16472 forEach(isolateScope.$$isolateBindings = newIsolateScopeDirective.$$isolateBindings, function(definition, scopeName) { |
|
16473 var attrName = definition.attrName, |
|
16474 optional = definition.optional, |
|
16475 mode = definition.mode, // @, =, or & |
|
16476 lastValue, |
|
16477 parentGet, parentSet, compare; |
|
16478 |
|
16479 switch (mode) { |
|
16480 |
|
16481 case '@': |
|
16482 attrs.$observe(attrName, function(value) { |
|
16483 isolateBindingContext[scopeName] = value; |
|
16484 }); |
|
16485 attrs.$$observers[attrName].$$scope = scope; |
|
16486 if( attrs[attrName] ) { |
|
16487 // If the attribute has been provided then we trigger an interpolation to ensure |
|
16488 // the value is there for use in the link fn |
|
16489 isolateBindingContext[scopeName] = $interpolate(attrs[attrName])(scope); |
|
16490 } |
|
16491 break; |
|
16492 |
|
16493 case '=': |
|
16494 if (optional && !attrs[attrName]) { |
|
16495 return; |
|
16496 } |
|
16497 parentGet = $parse(attrs[attrName]); |
|
16498 if (parentGet.literal) { |
|
16499 compare = equals; |
|
16500 } else { |
|
16501 compare = function(a,b) { return a === b || (a !== a && b !== b); }; |
|
16502 } |
|
16503 parentSet = parentGet.assign || function() { |
|
16504 // reset the change, or we will throw this exception on every $digest |
|
16505 lastValue = isolateBindingContext[scopeName] = parentGet(scope); |
|
16506 throw $compileMinErr('nonassign', |
|
16507 "Expression '{0}' used with directive '{1}' is non-assignable!", |
|
16508 attrs[attrName], newIsolateScopeDirective.name); |
|
16509 }; |
|
16510 lastValue = isolateBindingContext[scopeName] = parentGet(scope); |
|
16511 var parentValueWatch = function parentValueWatch(parentValue) { |
|
16512 if (!compare(parentValue, isolateBindingContext[scopeName])) { |
|
16513 // we are out of sync and need to copy |
|
16514 if (!compare(parentValue, lastValue)) { |
|
16515 // parent changed and it has precedence |
|
16516 isolateBindingContext[scopeName] = parentValue; |
|
16517 } else { |
|
16518 // if the parent can be assigned then do so |
|
16519 parentSet(scope, parentValue = isolateBindingContext[scopeName]); |
|
16520 } |
|
16521 } |
|
16522 return lastValue = parentValue; |
|
16523 }; |
|
16524 parentValueWatch.$stateful = true; |
|
16525 var unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal); |
|
16526 isolateScope.$on('$destroy', unwatch); |
|
16527 break; |
|
16528 |
|
16529 case '&': |
|
16530 parentGet = $parse(attrs[attrName]); |
|
16531 isolateBindingContext[scopeName] = function(locals) { |
|
16532 return parentGet(scope, locals); |
|
16533 }; |
|
16534 break; |
|
16535 } |
|
16536 }); |
|
16537 } |
|
16538 if (controllers) { |
|
16539 forEach(controllers, function(controller) { |
|
16540 controller(); |
|
16541 }); |
|
16542 controllers = null; |
|
16543 } |
|
16544 |
|
16545 // PRELINKING |
|
16546 for(i = 0, ii = preLinkFns.length; i < ii; i++) { |
|
16547 linkFn = preLinkFns[i]; |
|
16548 invokeLinkFn(linkFn, |
|
16549 linkFn.isolateScope ? isolateScope : scope, |
|
16550 $element, |
|
16551 attrs, |
|
16552 linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), |
|
16553 transcludeFn |
|
16554 ); |
|
16555 } |
|
16556 |
|
16557 // RECURSION |
|
16558 // We only pass the isolate scope, if the isolate directive has a template, |
|
16559 // otherwise the child elements do not belong to the isolate directive. |
|
16560 var scopeToChild = scope; |
|
16561 if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { |
|
16562 scopeToChild = isolateScope; |
|
16563 } |
|
16564 childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); |
|
16565 |
|
16566 // POSTLINKING |
|
16567 for(i = postLinkFns.length - 1; i >= 0; i--) { |
|
16568 linkFn = postLinkFns[i]; |
|
16569 invokeLinkFn(linkFn, |
|
16570 linkFn.isolateScope ? isolateScope : scope, |
|
16571 $element, |
|
16572 attrs, |
|
16573 linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), |
|
16574 transcludeFn |
|
16575 ); |
|
16576 } |
|
16577 |
|
16578 // This is the function that is injected as `$transclude`. |
|
16579 // Note: all arguments are optional! |
|
16580 function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) { |
|
16581 var transcludeControllers; |
|
16582 |
|
16583 // No scope passed in: |
|
16584 if (!isScope(scope)) { |
|
16585 futureParentElement = cloneAttachFn; |
|
16586 cloneAttachFn = scope; |
|
16587 scope = undefined; |
|
16588 } |
|
16589 |
|
16590 if (hasElementTranscludeDirective) { |
|
16591 transcludeControllers = elementControllers; |
|
16592 } |
|
16593 if (!futureParentElement) { |
|
16594 futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; |
|
16595 } |
|
16596 return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); |
|
16597 } |
|
16598 } |
|
16599 } |
|
16600 |
|
16601 function markDirectivesAsIsolate(directives) { |
|
16602 // mark all directives as needing isolate scope. |
|
16603 for (var j = 0, jj = directives.length; j < jj; j++) { |
|
16604 directives[j] = inherit(directives[j], {$$isolateScope: true}); |
|
16605 } |
|
16606 } |
|
16607 |
|
16608 /** |
|
16609 * looks up the directive and decorates it with exception handling and proper parameters. We |
|
16610 * call this the boundDirective. |
|
16611 * |
|
16612 * @param {string} name name of the directive to look up. |
|
16613 * @param {string} location The directive must be found in specific format. |
|
16614 * String containing any of theses characters: |
|
16615 * |
|
16616 * * `E`: element name |
|
16617 * * `A': attribute |
|
16618 * * `C`: class |
|
16619 * * `M`: comment |
|
16620 * @returns {boolean} true if directive was added. |
|
16621 */ |
|
16622 function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, |
|
16623 endAttrName) { |
|
16624 if (name === ignoreDirective) return null; |
|
16625 var match = null; |
|
16626 if (hasDirectives.hasOwnProperty(name)) { |
|
16627 for(var directive, directives = $injector.get(name + Suffix), |
|
16628 i = 0, ii = directives.length; i<ii; i++) { |
|
16629 try { |
|
16630 directive = directives[i]; |
|
16631 if ( (maxPriority === undefined || maxPriority > directive.priority) && |
|
16632 directive.restrict.indexOf(location) != -1) { |
|
16633 if (startAttrName) { |
|
16634 directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); |
|
16635 } |
|
16636 tDirectives.push(directive); |
|
16637 match = directive; |
|
16638 } |
|
16639 } catch(e) { $exceptionHandler(e); } |
|
16640 } |
|
16641 } |
|
16642 return match; |
|
16643 } |
|
16644 |
|
16645 |
|
16646 /** |
|
16647 * looks up the directive and returns true if it is a multi-element directive, |
|
16648 * and therefore requires DOM nodes between -start and -end markers to be grouped |
|
16649 * together. |
|
16650 * |
|
16651 * @param {string} name name of the directive to look up. |
|
16652 * @returns true if directive was registered as multi-element. |
|
16653 */ |
|
16654 function directiveIsMultiElement(name) { |
|
16655 if (hasDirectives.hasOwnProperty(name)) { |
|
16656 for(var directive, directives = $injector.get(name + Suffix), |
|
16657 i = 0, ii = directives.length; i<ii; i++) { |
|
16658 directive = directives[i]; |
|
16659 if (directive.multiElement) { |
|
16660 return true; |
|
16661 } |
|
16662 } |
|
16663 } |
|
16664 return false; |
|
16665 } |
|
16666 |
|
16667 /** |
|
16668 * When the element is replaced with HTML template then the new attributes |
|
16669 * on the template need to be merged with the existing attributes in the DOM. |
|
16670 * The desired effect is to have both of the attributes present. |
|
16671 * |
|
16672 * @param {object} dst destination attributes (original DOM) |
|
16673 * @param {object} src source attributes (from the directive template) |
|
16674 */ |
|
16675 function mergeTemplateAttributes(dst, src) { |
|
16676 var srcAttr = src.$attr, |
|
16677 dstAttr = dst.$attr, |
|
16678 $element = dst.$$element; |
|
16679 |
|
16680 // reapply the old attributes to the new element |
|
16681 forEach(dst, function(value, key) { |
|
16682 if (key.charAt(0) != '$') { |
|
16683 if (src[key] && src[key] !== value) { |
|
16684 value += (key === 'style' ? ';' : ' ') + src[key]; |
|
16685 } |
|
16686 dst.$set(key, value, true, srcAttr[key]); |
|
16687 } |
|
16688 }); |
|
16689 |
|
16690 // copy the new attributes on the old attrs object |
|
16691 forEach(src, function(value, key) { |
|
16692 if (key == 'class') { |
|
16693 safeAddClass($element, value); |
|
16694 dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; |
|
16695 } else if (key == 'style') { |
|
16696 $element.attr('style', $element.attr('style') + ';' + value); |
|
16697 dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value; |
|
16698 // `dst` will never contain hasOwnProperty as DOM parser won't let it. |
|
16699 // You will get an "InvalidCharacterError: DOM Exception 5" error if you |
|
16700 // have an attribute like "has-own-property" or "data-has-own-property", etc. |
|
16701 } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { |
|
16702 dst[key] = value; |
|
16703 dstAttr[key] = srcAttr[key]; |
|
16704 } |
|
16705 }); |
|
16706 } |
|
16707 |
|
16708 |
|
16709 function compileTemplateUrl(directives, $compileNode, tAttrs, |
|
16710 $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) { |
|
16711 var linkQueue = [], |
|
16712 afterTemplateNodeLinkFn, |
|
16713 afterTemplateChildLinkFn, |
|
16714 beforeTemplateCompileNode = $compileNode[0], |
|
16715 origAsyncDirective = directives.shift(), |
|
16716 // The fact that we have to copy and patch the directive seems wrong! |
|
16717 derivedSyncDirective = extend({}, origAsyncDirective, { |
|
16718 templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective |
|
16719 }), |
|
16720 templateUrl = (isFunction(origAsyncDirective.templateUrl)) |
|
16721 ? origAsyncDirective.templateUrl($compileNode, tAttrs) |
|
16722 : origAsyncDirective.templateUrl, |
|
16723 templateNamespace = origAsyncDirective.templateNamespace; |
|
16724 |
|
16725 $compileNode.empty(); |
|
16726 |
|
16727 $templateRequest($sce.getTrustedResourceUrl(templateUrl)) |
|
16728 .then(function(content) { |
|
16729 var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; |
|
16730 |
|
16731 content = denormalizeTemplate(content); |
|
16732 |
|
16733 if (origAsyncDirective.replace) { |
|
16734 if (jqLiteIsTextNode(content)) { |
|
16735 $template = []; |
|
16736 } else { |
|
16737 $template = removeComments(wrapTemplate(templateNamespace, trim(content))); |
|
16738 } |
|
16739 compileNode = $template[0]; |
|
16740 |
|
16741 if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { |
|
16742 throw $compileMinErr('tplrt', |
|
16743 "Template for directive '{0}' must have exactly one root element. {1}", |
|
16744 origAsyncDirective.name, templateUrl); |
|
16745 } |
|
16746 |
|
16747 tempTemplateAttrs = {$attr: {}}; |
|
16748 replaceWith($rootElement, $compileNode, compileNode); |
|
16749 var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs); |
|
16750 |
|
16751 if (isObject(origAsyncDirective.scope)) { |
|
16752 markDirectivesAsIsolate(templateDirectives); |
|
16753 } |
|
16754 directives = templateDirectives.concat(directives); |
|
16755 mergeTemplateAttributes(tAttrs, tempTemplateAttrs); |
|
16756 } else { |
|
16757 compileNode = beforeTemplateCompileNode; |
|
16758 $compileNode.html(content); |
|
16759 } |
|
16760 |
|
16761 directives.unshift(derivedSyncDirective); |
|
16762 |
|
16763 afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, |
|
16764 childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, |
|
16765 previousCompileContext); |
|
16766 forEach($rootElement, function(node, i) { |
|
16767 if (node == compileNode) { |
|
16768 $rootElement[i] = $compileNode[0]; |
|
16769 } |
|
16770 }); |
|
16771 afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); |
|
16772 |
|
16773 while(linkQueue.length) { |
|
16774 var scope = linkQueue.shift(), |
|
16775 beforeTemplateLinkNode = linkQueue.shift(), |
|
16776 linkRootElement = linkQueue.shift(), |
|
16777 boundTranscludeFn = linkQueue.shift(), |
|
16778 linkNode = $compileNode[0]; |
|
16779 |
|
16780 if (scope.$$destroyed) continue; |
|
16781 |
|
16782 if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { |
|
16783 var oldClasses = beforeTemplateLinkNode.className; |
|
16784 |
|
16785 if (!(previousCompileContext.hasElementTranscludeDirective && |
|
16786 origAsyncDirective.replace)) { |
|
16787 // it was cloned therefore we have to clone as well. |
|
16788 linkNode = jqLiteClone(compileNode); |
|
16789 } |
|
16790 replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); |
|
16791 |
|
16792 // Copy in CSS classes from original node |
|
16793 safeAddClass(jqLite(linkNode), oldClasses); |
|
16794 } |
|
16795 if (afterTemplateNodeLinkFn.transcludeOnThisElement) { |
|
16796 childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); |
|
16797 } else { |
|
16798 childBoundTranscludeFn = boundTranscludeFn; |
|
16799 } |
|
16800 afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, |
|
16801 childBoundTranscludeFn); |
|
16802 } |
|
16803 linkQueue = null; |
|
16804 }); |
|
16805 |
|
16806 return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { |
|
16807 var childBoundTranscludeFn = boundTranscludeFn; |
|
16808 if (scope.$$destroyed) return; |
|
16809 if (linkQueue) { |
|
16810 linkQueue.push(scope); |
|
16811 linkQueue.push(node); |
|
16812 linkQueue.push(rootElement); |
|
16813 linkQueue.push(childBoundTranscludeFn); |
|
16814 } else { |
|
16815 if (afterTemplateNodeLinkFn.transcludeOnThisElement) { |
|
16816 childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); |
|
16817 } |
|
16818 afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn); |
|
16819 } |
|
16820 }; |
|
16821 } |
|
16822 |
|
16823 |
|
16824 /** |
|
16825 * Sorting function for bound directives. |
|
16826 */ |
|
16827 function byPriority(a, b) { |
|
16828 var diff = b.priority - a.priority; |
|
16829 if (diff !== 0) return diff; |
|
16830 if (a.name !== b.name) return (a.name < b.name) ? -1 : 1; |
|
16831 return a.index - b.index; |
|
16832 } |
|
16833 |
|
16834 |
|
16835 function assertNoDuplicate(what, previousDirective, directive, element) { |
|
16836 if (previousDirective) { |
|
16837 throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}', |
|
16838 previousDirective.name, directive.name, what, startingTag(element)); |
|
16839 } |
|
16840 } |
|
16841 |
|
16842 |
|
16843 function addTextInterpolateDirective(directives, text) { |
|
16844 var interpolateFn = $interpolate(text, true); |
|
16845 if (interpolateFn) { |
|
16846 directives.push({ |
|
16847 priority: 0, |
|
16848 compile: function textInterpolateCompileFn(templateNode) { |
|
16849 var templateNodeParent = templateNode.parent(), |
|
16850 hasCompileParent = !!templateNodeParent.length; |
|
16851 |
|
16852 // When transcluding a template that has bindings in the root |
|
16853 // we don't have a parent and thus need to add the class during linking fn. |
|
16854 if (hasCompileParent) compile.$$addBindingClass(templateNodeParent); |
|
16855 |
|
16856 return function textInterpolateLinkFn(scope, node) { |
|
16857 var parent = node.parent(); |
|
16858 if (!hasCompileParent) compile.$$addBindingClass(parent); |
|
16859 compile.$$addBindingInfo(parent, interpolateFn.expressions); |
|
16860 scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { |
|
16861 node[0].nodeValue = value; |
|
16862 }); |
|
16863 }; |
|
16864 } |
|
16865 }); |
|
16866 } |
|
16867 } |
|
16868 |
|
16869 |
|
16870 function wrapTemplate(type, template) { |
|
16871 type = lowercase(type || 'html'); |
|
16872 switch(type) { |
|
16873 case 'svg': |
|
16874 case 'math': |
|
16875 var wrapper = document.createElement('div'); |
|
16876 wrapper.innerHTML = '<'+type+'>'+template+'</'+type+'>'; |
|
16877 return wrapper.childNodes[0].childNodes; |
|
16878 default: |
|
16879 return template; |
|
16880 } |
|
16881 } |
|
16882 |
|
16883 |
|
16884 function getTrustedContext(node, attrNormalizedName) { |
|
16885 if (attrNormalizedName == "srcdoc") { |
|
16886 return $sce.HTML; |
|
16887 } |
|
16888 var tag = nodeName_(node); |
|
16889 // maction[xlink:href] can source SVG. It's not limited to <maction>. |
|
16890 if (attrNormalizedName == "xlinkHref" || |
|
16891 (tag == "form" && attrNormalizedName == "action") || |
|
16892 (tag != "img" && (attrNormalizedName == "src" || |
|
16893 attrNormalizedName == "ngSrc"))) { |
|
16894 return $sce.RESOURCE_URL; |
|
16895 } |
|
16896 } |
|
16897 |
|
16898 |
|
16899 function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { |
|
16900 var interpolateFn = $interpolate(value, true); |
|
16901 |
|
16902 // no interpolation found -> ignore |
|
16903 if (!interpolateFn) return; |
|
16904 |
|
16905 |
|
16906 if (name === "multiple" && nodeName_(node) === "select") { |
|
16907 throw $compileMinErr("selmulti", |
|
16908 "Binding to the 'multiple' attribute is not supported. Element: {0}", |
|
16909 startingTag(node)); |
|
16910 } |
|
16911 |
|
16912 directives.push({ |
|
16913 priority: 100, |
|
16914 compile: function() { |
|
16915 return { |
|
16916 pre: function attrInterpolatePreLinkFn(scope, element, attr) { |
|
16917 var $$observers = (attr.$$observers || (attr.$$observers = {})); |
|
16918 |
|
16919 if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { |
|
16920 throw $compileMinErr('nodomevents', |
|
16921 "Interpolations for HTML DOM event attributes are disallowed. Please use the " + |
|
16922 "ng- versions (such as ng-click instead of onclick) instead."); |
|
16923 } |
|
16924 |
|
16925 // If the attribute was removed, then we are done |
|
16926 if (!attr[name]) { |
|
16927 return; |
|
16928 } |
|
16929 |
|
16930 // we need to interpolate again, in case the attribute value has been updated |
|
16931 // (e.g. by another directive's compile function) |
|
16932 interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name), |
|
16933 ALL_OR_NOTHING_ATTRS[name] || allOrNothing); |
|
16934 |
|
16935 // if attribute was updated so that there is no interpolation going on we don't want to |
|
16936 // register any observers |
|
16937 if (!interpolateFn) return; |
|
16938 |
|
16939 // initialize attr object so that it's ready in case we need the value for isolate |
|
16940 // scope initialization, otherwise the value would not be available from isolate |
|
16941 // directive's linking fn during linking phase |
|
16942 attr[name] = interpolateFn(scope); |
|
16943 |
|
16944 ($$observers[name] || ($$observers[name] = [])).$$inter = true; |
|
16945 (attr.$$observers && attr.$$observers[name].$$scope || scope). |
|
16946 $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { |
|
16947 //special case for class attribute addition + removal |
|
16948 //so that class changes can tap into the animation |
|
16949 //hooks provided by the $animate service. Be sure to |
|
16950 //skip animations when the first digest occurs (when |
|
16951 //both the new and the old values are the same) since |
|
16952 //the CSS classes are the non-interpolated values |
|
16953 if(name === 'class' && newValue != oldValue) { |
|
16954 attr.$updateClass(newValue, oldValue); |
|
16955 } else { |
|
16956 attr.$set(name, newValue); |
|
16957 } |
|
16958 }); |
|
16959 } |
|
16960 }; |
|
16961 } |
|
16962 }); |
|
16963 } |
|
16964 |
|
16965 |
|
16966 /** |
|
16967 * This is a special jqLite.replaceWith, which can replace items which |
|
16968 * have no parents, provided that the containing jqLite collection is provided. |
|
16969 * |
|
16970 * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes |
|
16971 * in the root of the tree. |
|
16972 * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep |
|
16973 * the shell, but replace its DOM node reference. |
|
16974 * @param {Node} newNode The new DOM node. |
|
16975 */ |
|
16976 function replaceWith($rootElement, elementsToRemove, newNode) { |
|
16977 var firstElementToRemove = elementsToRemove[0], |
|
16978 removeCount = elementsToRemove.length, |
|
16979 parent = firstElementToRemove.parentNode, |
|
16980 i, ii; |
|
16981 |
|
16982 if ($rootElement) { |
|
16983 for(i = 0, ii = $rootElement.length; i < ii; i++) { |
|
16984 if ($rootElement[i] == firstElementToRemove) { |
|
16985 $rootElement[i++] = newNode; |
|
16986 for (var j = i, j2 = j + removeCount - 1, |
|
16987 jj = $rootElement.length; |
|
16988 j < jj; j++, j2++) { |
|
16989 if (j2 < jj) { |
|
16990 $rootElement[j] = $rootElement[j2]; |
|
16991 } else { |
|
16992 delete $rootElement[j]; |
|
16993 } |
|
16994 } |
|
16995 $rootElement.length -= removeCount - 1; |
|
16996 |
|
16997 // If the replaced element is also the jQuery .context then replace it |
|
16998 // .context is a deprecated jQuery api, so we should set it only when jQuery set it |
|
16999 // http://api.jquery.com/context/ |
|
17000 if ($rootElement.context === firstElementToRemove) { |
|
17001 $rootElement.context = newNode; |
|
17002 } |
|
17003 break; |
|
17004 } |
|
17005 } |
|
17006 } |
|
17007 |
|
17008 if (parent) { |
|
17009 parent.replaceChild(newNode, firstElementToRemove); |
|
17010 } |
|
17011 |
|
17012 // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it? |
|
17013 var fragment = document.createDocumentFragment(); |
|
17014 fragment.appendChild(firstElementToRemove); |
|
17015 |
|
17016 // Copy over user data (that includes Angular's $scope etc.). Don't copy private |
|
17017 // data here because there's no public interface in jQuery to do that and copying over |
|
17018 // event listeners (which is the main use of private data) wouldn't work anyway. |
|
17019 jqLite(newNode).data(jqLite(firstElementToRemove).data()); |
|
17020 |
|
17021 // Remove data of the replaced element. We cannot just call .remove() |
|
17022 // on the element it since that would deallocate scope that is needed |
|
17023 // for the new node. Instead, remove the data "manually". |
|
17024 if (!jQuery) { |
|
17025 delete jqLite.cache[firstElementToRemove[jqLite.expando]]; |
|
17026 } else { |
|
17027 // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after |
|
17028 // the replaced element. The cleanData version monkey-patched by Angular would cause |
|
17029 // the scope to be trashed and we do need the very same scope to work with the new |
|
17030 // element. However, we cannot just cache the non-patched version and use it here as |
|
17031 // that would break if another library patches the method after Angular does (one |
|
17032 // example is jQuery UI). Instead, set a flag indicating scope destroying should be |
|
17033 // skipped this one time. |
|
17034 skipDestroyOnNextJQueryCleanData = true; |
|
17035 jQuery.cleanData([firstElementToRemove]); |
|
17036 } |
|
17037 |
|
17038 for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { |
|
17039 var element = elementsToRemove[k]; |
|
17040 jqLite(element).remove(); // must do this way to clean up expando |
|
17041 fragment.appendChild(element); |
|
17042 delete elementsToRemove[k]; |
|
17043 } |
|
17044 |
|
17045 elementsToRemove[0] = newNode; |
|
17046 elementsToRemove.length = 1; |
|
17047 } |
|
17048 |
|
17049 |
|
17050 function cloneAndAnnotateFn(fn, annotation) { |
|
17051 return extend(function() { return fn.apply(null, arguments); }, fn, annotation); |
|
17052 } |
|
17053 |
|
17054 |
|
17055 function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) { |
|
17056 try { |
|
17057 linkFn(scope, $element, attrs, controllers, transcludeFn); |
|
17058 } catch(e) { |
|
17059 $exceptionHandler(e, startingTag($element)); |
|
17060 } |
|
17061 } |
|
17062 }]; |
|
17063 } |
|
17064 |
|
17065 var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; |
|
17066 /** |
|
17067 * Converts all accepted directives format into proper directive name. |
|
17068 * All of these will become 'myDirective': |
|
17069 * my:Directive |
|
17070 * my-directive |
|
17071 * x-my-directive |
|
17072 * data-my:directive |
|
17073 * |
|
17074 * Also there is special case for Moz prefix starting with upper case letter. |
|
17075 * @param name Name to normalize |
|
17076 */ |
|
17077 function directiveNormalize(name) { |
|
17078 return camelCase(name.replace(PREFIX_REGEXP, '')); |
|
17079 } |
|
17080 |
|
17081 /** |
|
17082 * @ngdoc type |
|
17083 * @name $compile.directive.Attributes |
|
17084 * |
|
17085 * @description |
|
17086 * A shared object between directive compile / linking functions which contains normalized DOM |
|
17087 * element attributes. The values reflect current binding state `{{ }}`. The normalization is |
|
17088 * needed since all of these are treated as equivalent in Angular: |
|
17089 * |
|
17090 * ``` |
|
17091 * <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a"> |
|
17092 * ``` |
|
17093 */ |
|
17094 |
|
17095 /** |
|
17096 * @ngdoc property |
|
17097 * @name $compile.directive.Attributes#$attr |
|
17098 * |
|
17099 * @description |
|
17100 * A map of DOM element attribute names to the normalized name. This is |
|
17101 * needed to do reverse lookup from normalized name back to actual name. |
|
17102 */ |
|
17103 |
|
17104 |
|
17105 /** |
|
17106 * @ngdoc method |
|
17107 * @name $compile.directive.Attributes#$set |
|
17108 * @kind function |
|
17109 * |
|
17110 * @description |
|
17111 * Set DOM element attribute value. |
|
17112 * |
|
17113 * |
|
17114 * @param {string} name Normalized element attribute name of the property to modify. The name is |
|
17115 * reverse-translated using the {@link ng.$compile.directive.Attributes#$attr $attr} |
|
17116 * property to the original name. |
|
17117 * @param {string} value Value to set the attribute to. The value can be an interpolated string. |
|
17118 */ |
|
17119 |
|
17120 |
|
17121 |
|
17122 /** |
|
17123 * Closure compiler type information |
|
17124 */ |
|
17125 |
|
17126 function nodesetLinkingFn( |
|
17127 /* angular.Scope */ scope, |
|
17128 /* NodeList */ nodeList, |
|
17129 /* Element */ rootElement, |
|
17130 /* function(Function) */ boundTranscludeFn |
|
17131 ){} |
|
17132 |
|
17133 function directiveLinkingFn( |
|
17134 /* nodesetLinkingFn */ nodesetLinkingFn, |
|
17135 /* angular.Scope */ scope, |
|
17136 /* Node */ node, |
|
17137 /* Element */ rootElement, |
|
17138 /* function(Function) */ boundTranscludeFn |
|
17139 ){} |
|
17140 |
|
17141 function tokenDifference(str1, str2) { |
|
17142 var values = '', |
|
17143 tokens1 = str1.split(/\s+/), |
|
17144 tokens2 = str2.split(/\s+/); |
|
17145 |
|
17146 outer: |
|
17147 for(var i = 0; i < tokens1.length; i++) { |
|
17148 var token = tokens1[i]; |
|
17149 for(var j = 0; j < tokens2.length; j++) { |
|
17150 if(token == tokens2[j]) continue outer; |
|
17151 } |
|
17152 values += (values.length > 0 ? ' ' : '') + token; |
|
17153 } |
|
17154 return values; |
|
17155 } |
|
17156 |
|
17157 function removeComments(jqNodes) { |
|
17158 jqNodes = jqLite(jqNodes); |
|
17159 var i = jqNodes.length; |
|
17160 |
|
17161 if (i <= 1) { |
|
17162 return jqNodes; |
|
17163 } |
|
17164 |
|
17165 while (i--) { |
|
17166 var node = jqNodes[i]; |
|
17167 if (node.nodeType === NODE_TYPE_COMMENT) { |
|
17168 splice.call(jqNodes, i, 1); |
|
17169 } |
|
17170 } |
|
17171 return jqNodes; |
|
17172 } |
|
17173 |
|
17174 /** |
|
17175 * @ngdoc provider |
|
17176 * @name $controllerProvider |
|
17177 * @description |
|
17178 * The {@link ng.$controller $controller service} is used by Angular to create new |
|
17179 * controllers. |
|
17180 * |
|
17181 * This provider allows controller registration via the |
|
17182 * {@link ng.$controllerProvider#register register} method. |
|
17183 */ |
|
17184 function $ControllerProvider() { |
|
17185 var controllers = {}, |
|
17186 globals = false, |
|
17187 CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; |
|
17188 |
|
17189 |
|
17190 /** |
|
17191 * @ngdoc method |
|
17192 * @name $controllerProvider#register |
|
17193 * @param {string|Object} name Controller name, or an object map of controllers where the keys are |
|
17194 * the names and the values are the constructors. |
|
17195 * @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI |
|
17196 * annotations in the array notation). |
|
17197 */ |
|
17198 this.register = function(name, constructor) { |
|
17199 assertNotHasOwnProperty(name, 'controller'); |
|
17200 if (isObject(name)) { |
|
17201 extend(controllers, name); |
|
17202 } else { |
|
17203 controllers[name] = constructor; |
|
17204 } |
|
17205 }; |
|
17206 |
|
17207 /** |
|
17208 * @ngdoc method |
|
17209 * @name $controllerProvider#allowGlobals |
|
17210 * @description If called, allows `$controller` to find controller constructors on `window` |
|
17211 */ |
|
17212 this.allowGlobals = function() { |
|
17213 globals = true; |
|
17214 }; |
|
17215 |
|
17216 |
|
17217 this.$get = ['$injector', '$window', function($injector, $window) { |
|
17218 |
|
17219 /** |
|
17220 * @ngdoc service |
|
17221 * @name $controller |
|
17222 * @requires $injector |
|
17223 * |
|
17224 * @param {Function|string} constructor If called with a function then it's considered to be the |
|
17225 * controller constructor function. Otherwise it's considered to be a string which is used |
|
17226 * to retrieve the controller constructor using the following steps: |
|
17227 * |
|
17228 * * check if a controller with given name is registered via `$controllerProvider` |
|
17229 * * check if evaluating the string on the current scope returns a constructor |
|
17230 * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global |
|
17231 * `window` object (not recommended) |
|
17232 * |
|
17233 * @param {Object} locals Injection locals for Controller. |
|
17234 * @return {Object} Instance of given controller. |
|
17235 * |
|
17236 * @description |
|
17237 * `$controller` service is responsible for instantiating controllers. |
|
17238 * |
|
17239 * It's just a simple call to {@link auto.$injector $injector}, but extracted into |
|
17240 * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). |
|
17241 */ |
|
17242 return function(expression, locals, later, ident) { |
|
17243 // PRIVATE API: |
|
17244 // param `later` --- indicates that the controller's constructor is invoked at a later time. |
|
17245 // If true, $controller will allocate the object with the correct |
|
17246 // prototype chain, but will not invoke the controller until a returned |
|
17247 // callback is invoked. |
|
17248 // param `ident` --- An optional label which overrides the label parsed from the controller |
|
17249 // expression, if any. |
|
17250 var instance, match, constructor, identifier; |
|
17251 later = later === true; |
|
17252 if (ident && isString(ident)) { |
|
17253 identifier = ident; |
|
17254 } |
|
17255 |
|
17256 if(isString(expression)) { |
|
17257 match = expression.match(CNTRL_REG), |
|
17258 constructor = match[1], |
|
17259 identifier = identifier || match[3]; |
|
17260 expression = controllers.hasOwnProperty(constructor) |
|
17261 ? controllers[constructor] |
|
17262 : getter(locals.$scope, constructor, true) || |
|
17263 (globals ? getter($window, constructor, true) : undefined); |
|
17264 |
|
17265 assertArgFn(expression, constructor, true); |
|
17266 } |
|
17267 |
|
17268 if (later) { |
|
17269 // Instantiate controller later: |
|
17270 // This machinery is used to create an instance of the object before calling the |
|
17271 // controller's constructor itself. |
|
17272 // |
|
17273 // This allows properties to be added to the controller before the constructor is |
|
17274 // invoked. Primarily, this is used for isolate scope bindings in $compile. |
|
17275 // |
|
17276 // This feature is not intended for use by applications, and is thus not documented |
|
17277 // publicly. |
|
17278 var Constructor = function() {}; |
|
17279 Constructor.prototype = (isArray(expression) ? |
|
17280 expression[expression.length - 1] : expression).prototype; |
|
17281 instance = new Constructor(); |
|
17282 |
|
17283 if (identifier) { |
|
17284 addIdentifier(locals, identifier, instance, constructor || expression.name); |
|
17285 } |
|
17286 |
|
17287 return extend(function() { |
|
17288 $injector.invoke(expression, instance, locals, constructor); |
|
17289 return instance; |
|
17290 }, { |
|
17291 instance: instance, |
|
17292 identifier: identifier |
|
17293 }); |
|
17294 } |
|
17295 |
|
17296 instance = $injector.instantiate(expression, locals, constructor); |
|
17297 |
|
17298 if (identifier) { |
|
17299 addIdentifier(locals, identifier, instance, constructor || expression.name); |
|
17300 } |
|
17301 |
|
17302 return instance; |
|
17303 }; |
|
17304 |
|
17305 function addIdentifier(locals, identifier, instance, name) { |
|
17306 if (!(locals && isObject(locals.$scope))) { |
|
17307 throw minErr('$controller')('noscp', |
|
17308 "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", |
|
17309 name, identifier); |
|
17310 } |
|
17311 |
|
17312 locals.$scope[identifier] = instance; |
|
17313 } |
|
17314 }]; |
|
17315 } |
|
17316 |
|
17317 /** |
|
17318 * @ngdoc service |
|
17319 * @name $document |
|
17320 * @requires $window |
|
17321 * |
|
17322 * @description |
|
17323 * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. |
|
17324 * |
|
17325 * @example |
|
17326 <example module="documentExample"> |
|
17327 <file name="index.html"> |
|
17328 <div ng-controller="ExampleController"> |
|
17329 <p>$document title: <b ng-bind="title"></b></p> |
|
17330 <p>window.document title: <b ng-bind="windowTitle"></b></p> |
|
17331 </div> |
|
17332 </file> |
|
17333 <file name="script.js"> |
|
17334 angular.module('documentExample', []) |
|
17335 .controller('ExampleController', ['$scope', '$document', function($scope, $document) { |
|
17336 $scope.title = $document[0].title; |
|
17337 $scope.windowTitle = angular.element(window.document)[0].title; |
|
17338 }]); |
|
17339 </file> |
|
17340 </example> |
|
17341 */ |
|
17342 function $DocumentProvider(){ |
|
17343 this.$get = ['$window', function(window){ |
|
17344 return jqLite(window.document); |
|
17345 }]; |
|
17346 } |
|
17347 |
|
17348 /** |
|
17349 * @ngdoc service |
|
17350 * @name $exceptionHandler |
|
17351 * @requires ng.$log |
|
17352 * |
|
17353 * @description |
|
17354 * Any uncaught exception in angular expressions is delegated to this service. |
|
17355 * The default implementation simply delegates to `$log.error` which logs it into |
|
17356 * the browser console. |
|
17357 * |
|
17358 * In unit tests, if `angular-mocks.js` is loaded, this service is overridden by |
|
17359 * {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing. |
|
17360 * |
|
17361 * ## Example: |
|
17362 * |
|
17363 * ```js |
|
17364 * angular.module('exceptionOverride', []).factory('$exceptionHandler', function () { |
|
17365 * return function (exception, cause) { |
|
17366 * exception.message += ' (caused by "' + cause + '")'; |
|
17367 * throw exception; |
|
17368 * }; |
|
17369 * }); |
|
17370 * ``` |
|
17371 * |
|
17372 * This example will override the normal action of `$exceptionHandler`, to make angular |
|
17373 * exceptions fail hard when they happen, instead of just logging to the console. |
|
17374 * |
|
17375 * @param {Error} exception Exception associated with the error. |
|
17376 * @param {string=} cause optional information about the context in which |
|
17377 * the error was thrown. |
|
17378 * |
|
17379 */ |
|
17380 function $ExceptionHandlerProvider() { |
|
17381 this.$get = ['$log', function($log) { |
|
17382 return function(exception, cause) { |
|
17383 $log.error.apply($log, arguments); |
|
17384 }; |
|
17385 }]; |
|
17386 } |
|
17387 |
|
17388 /** |
|
17389 * Parse headers into key value object |
|
17390 * |
|
17391 * @param {string} headers Raw headers as a string |
|
17392 * @returns {Object} Parsed headers as key value object |
|
17393 */ |
|
17394 function parseHeaders(headers) { |
|
17395 var parsed = {}, key, val, i; |
|
17396 |
|
17397 if (!headers) return parsed; |
|
17398 |
|
17399 forEach(headers.split('\n'), function(line) { |
|
17400 i = line.indexOf(':'); |
|
17401 key = lowercase(trim(line.substr(0, i))); |
|
17402 val = trim(line.substr(i + 1)); |
|
17403 |
|
17404 if (key) { |
|
17405 parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; |
|
17406 } |
|
17407 }); |
|
17408 |
|
17409 return parsed; |
|
17410 } |
|
17411 |
|
17412 |
|
17413 /** |
|
17414 * Returns a function that provides access to parsed headers. |
|
17415 * |
|
17416 * Headers are lazy parsed when first requested. |
|
17417 * @see parseHeaders |
|
17418 * |
|
17419 * @param {(string|Object)} headers Headers to provide access to. |
|
17420 * @returns {function(string=)} Returns a getter function which if called with: |
|
17421 * |
|
17422 * - if called with single an argument returns a single header value or null |
|
17423 * - if called with no arguments returns an object containing all headers. |
|
17424 */ |
|
17425 function headersGetter(headers) { |
|
17426 var headersObj = isObject(headers) ? headers : undefined; |
|
17427 |
|
17428 return function(name) { |
|
17429 if (!headersObj) headersObj = parseHeaders(headers); |
|
17430 |
|
17431 if (name) { |
|
17432 return headersObj[lowercase(name)] || null; |
|
17433 } |
|
17434 |
|
17435 return headersObj; |
|
17436 }; |
|
17437 } |
|
17438 |
|
17439 |
|
17440 /** |
|
17441 * Chain all given functions |
|
17442 * |
|
17443 * This function is used for both request and response transforming |
|
17444 * |
|
17445 * @param {*} data Data to transform. |
|
17446 * @param {function(string=)} headers Http headers getter fn. |
|
17447 * @param {(Function|Array.<Function>)} fns Function or an array of functions. |
|
17448 * @returns {*} Transformed data. |
|
17449 */ |
|
17450 function transformData(data, headers, fns) { |
|
17451 if (isFunction(fns)) |
|
17452 return fns(data, headers); |
|
17453 |
|
17454 forEach(fns, function(fn) { |
|
17455 data = fn(data, headers); |
|
17456 }); |
|
17457 |
|
17458 return data; |
|
17459 } |
|
17460 |
|
17461 |
|
17462 function isSuccess(status) { |
|
17463 return 200 <= status && status < 300; |
|
17464 } |
|
17465 |
|
17466 |
|
17467 /** |
|
17468 * @ngdoc provider |
|
17469 * @name $httpProvider |
|
17470 * @description |
|
17471 * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. |
|
17472 * */ |
|
17473 function $HttpProvider() { |
|
17474 var JSON_START = /^\s*(\[|\{[^\{])/, |
|
17475 JSON_END = /[\}\]]\s*$/, |
|
17476 PROTECTION_PREFIX = /^\)\]\}',?\n/, |
|
17477 APPLICATION_JSON = 'application/json', |
|
17478 CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'}; |
|
17479 |
|
17480 /** |
|
17481 * @ngdoc property |
|
17482 * @name $httpProvider#defaults |
|
17483 * @description |
|
17484 * |
|
17485 * Object containing default values for all {@link ng.$http $http} requests. |
|
17486 * |
|
17487 * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. |
|
17488 * Defaults value is `'XSRF-TOKEN'`. |
|
17489 * |
|
17490 * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the |
|
17491 * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. |
|
17492 * |
|
17493 * - **`defaults.headers`** - {Object} - Default headers for all $http requests. |
|
17494 * Refer to {@link ng.$http#setting-http-headers $http} for documentation on |
|
17495 * setting default headers. |
|
17496 * - **`defaults.headers.common`** |
|
17497 * - **`defaults.headers.post`** |
|
17498 * - **`defaults.headers.put`** |
|
17499 * - **`defaults.headers.patch`** |
|
17500 **/ |
|
17501 var defaults = this.defaults = { |
|
17502 // transform incoming response data |
|
17503 transformResponse: [function defaultHttpResponseTransform(data, headers) { |
|
17504 if (isString(data)) { |
|
17505 // strip json vulnerability protection prefix |
|
17506 data = data.replace(PROTECTION_PREFIX, ''); |
|
17507 var contentType = headers('Content-Type'); |
|
17508 if ((contentType && contentType.indexOf(APPLICATION_JSON) === 0) || |
|
17509 (JSON_START.test(data) && JSON_END.test(data))) { |
|
17510 data = fromJson(data); |
|
17511 } |
|
17512 } |
|
17513 return data; |
|
17514 }], |
|
17515 |
|
17516 // transform outgoing request data |
|
17517 transformRequest: [function(d) { |
|
17518 return isObject(d) && !isFile(d) && !isBlob(d) ? toJson(d) : d; |
|
17519 }], |
|
17520 |
|
17521 // default headers |
|
17522 headers: { |
|
17523 common: { |
|
17524 'Accept': 'application/json, text/plain, */*' |
|
17525 }, |
|
17526 post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), |
|
17527 put: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), |
|
17528 patch: shallowCopy(CONTENT_TYPE_APPLICATION_JSON) |
|
17529 }, |
|
17530 |
|
17531 xsrfCookieName: 'XSRF-TOKEN', |
|
17532 xsrfHeaderName: 'X-XSRF-TOKEN' |
|
17533 }; |
|
17534 |
|
17535 var useApplyAsync = false; |
|
17536 /** |
|
17537 * @ngdoc method |
|
17538 * @name $httpProvider#useApplyAsync |
|
17539 * @description |
|
17540 * |
|
17541 * Configure $http service to combine processing of multiple http responses received at around |
|
17542 * the same time via {@link ng.$rootScope#applyAsync $rootScope.$applyAsync}. This can result in |
|
17543 * significant performance improvement for bigger applications that make many HTTP requests |
|
17544 * concurrently (common during application bootstrap). |
|
17545 * |
|
17546 * Defaults to false. If no value is specifed, returns the current configured value. |
|
17547 * |
|
17548 * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred |
|
17549 * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window |
|
17550 * to load and share the same digest cycle. |
|
17551 * |
|
17552 * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. |
|
17553 * otherwise, returns the current configured value. |
|
17554 **/ |
|
17555 this.useApplyAsync = function(value) { |
|
17556 if (isDefined(value)) { |
|
17557 useApplyAsync = !!value; |
|
17558 return this; |
|
17559 } |
|
17560 return useApplyAsync; |
|
17561 }; |
|
17562 |
|
17563 /** |
|
17564 * Are ordered by request, i.e. they are applied in the same order as the |
|
17565 * array, on request, but reverse order, on response. |
|
17566 */ |
|
17567 var interceptorFactories = this.interceptors = []; |
|
17568 |
|
17569 this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', |
|
17570 function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { |
|
17571 |
|
17572 var defaultCache = $cacheFactory('$http'); |
|
17573 |
|
17574 /** |
|
17575 * Interceptors stored in reverse order. Inner interceptors before outer interceptors. |
|
17576 * The reversal is needed so that we can build up the interception chain around the |
|
17577 * server request. |
|
17578 */ |
|
17579 var reversedInterceptors = []; |
|
17580 |
|
17581 forEach(interceptorFactories, function(interceptorFactory) { |
|
17582 reversedInterceptors.unshift(isString(interceptorFactory) |
|
17583 ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); |
|
17584 }); |
|
17585 |
|
17586 /** |
|
17587 * @ngdoc service |
|
17588 * @kind function |
|
17589 * @name $http |
|
17590 * @requires ng.$httpBackend |
|
17591 * @requires $cacheFactory |
|
17592 * @requires $rootScope |
|
17593 * @requires $q |
|
17594 * @requires $injector |
|
17595 * |
|
17596 * @description |
|
17597 * The `$http` service is a core Angular service that facilitates communication with the remote |
|
17598 * HTTP servers via the browser's [XMLHttpRequest](https://developer.mozilla.org/en/xmlhttprequest) |
|
17599 * object or via [JSONP](http://en.wikipedia.org/wiki/JSONP). |
|
17600 * |
|
17601 * For unit testing applications that use `$http` service, see |
|
17602 * {@link ngMock.$httpBackend $httpBackend mock}. |
|
17603 * |
|
17604 * For a higher level of abstraction, please check out the {@link ngResource.$resource |
|
17605 * $resource} service. |
|
17606 * |
|
17607 * The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by |
|
17608 * the $q service. While for simple usage patterns this doesn't matter much, for advanced usage |
|
17609 * it is important to familiarize yourself with these APIs and the guarantees they provide. |
|
17610 * |
|
17611 * |
|
17612 * ## General usage |
|
17613 * The `$http` service is a function which takes a single argument — a configuration object — |
|
17614 * that is used to generate an HTTP request and returns a {@link ng.$q promise} |
|
17615 * with two $http specific methods: `success` and `error`. |
|
17616 * |
|
17617 * ```js |
|
17618 * $http({method: 'GET', url: '/someUrl'}). |
|
17619 * success(function(data, status, headers, config) { |
|
17620 * // this callback will be called asynchronously |
|
17621 * // when the response is available |
|
17622 * }). |
|
17623 * error(function(data, status, headers, config) { |
|
17624 * // called asynchronously if an error occurs |
|
17625 * // or server returns response with an error status. |
|
17626 * }); |
|
17627 * ``` |
|
17628 * |
|
17629 * Since the returned value of calling the $http function is a `promise`, you can also use |
|
17630 * the `then` method to register callbacks, and these callbacks will receive a single argument – |
|
17631 * an object representing the response. See the API signature and type info below for more |
|
17632 * details. |
|
17633 * |
|
17634 * A response status code between 200 and 299 is considered a success status and |
|
17635 * will result in the success callback being called. Note that if the response is a redirect, |
|
17636 * XMLHttpRequest will transparently follow it, meaning that the error callback will not be |
|
17637 * called for such responses. |
|
17638 * |
|
17639 * ## Writing Unit Tests that use $http |
|
17640 * When unit testing (using {@link ngMock ngMock}), it is necessary to call |
|
17641 * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending |
|
17642 * request using trained responses. |
|
17643 * |
|
17644 * ``` |
|
17645 * $httpBackend.expectGET(...); |
|
17646 * $http.get(...); |
|
17647 * $httpBackend.flush(); |
|
17648 * ``` |
|
17649 * |
|
17650 * ## Shortcut methods |
|
17651 * |
|
17652 * Shortcut methods are also available. All shortcut methods require passing in the URL, and |
|
17653 * request data must be passed in for POST/PUT requests. |
|
17654 * |
|
17655 * ```js |
|
17656 * $http.get('/someUrl').success(successCallback); |
|
17657 * $http.post('/someUrl', data).success(successCallback); |
|
17658 * ``` |
|
17659 * |
|
17660 * Complete list of shortcut methods: |
|
17661 * |
|
17662 * - {@link ng.$http#get $http.get} |
|
17663 * - {@link ng.$http#head $http.head} |
|
17664 * - {@link ng.$http#post $http.post} |
|
17665 * - {@link ng.$http#put $http.put} |
|
17666 * - {@link ng.$http#delete $http.delete} |
|
17667 * - {@link ng.$http#jsonp $http.jsonp} |
|
17668 * - {@link ng.$http#patch $http.patch} |
|
17669 * |
|
17670 * |
|
17671 * ## Setting HTTP Headers |
|
17672 * |
|
17673 * The $http service will automatically add certain HTTP headers to all requests. These defaults |
|
17674 * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration |
|
17675 * object, which currently contains this default configuration: |
|
17676 * |
|
17677 * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): |
|
17678 * - `Accept: application/json, text/plain, * / *` |
|
17679 * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) |
|
17680 * - `Content-Type: application/json` |
|
17681 * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) |
|
17682 * - `Content-Type: application/json` |
|
17683 * |
|
17684 * To add or overwrite these defaults, simply add or remove a property from these configuration |
|
17685 * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object |
|
17686 * with the lowercased HTTP method name as the key, e.g. |
|
17687 * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }. |
|
17688 * |
|
17689 * The defaults can also be set at runtime via the `$http.defaults` object in the same |
|
17690 * fashion. For example: |
|
17691 * |
|
17692 * ``` |
|
17693 * module.run(function($http) { |
|
17694 * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w' |
|
17695 * }); |
|
17696 * ``` |
|
17697 * |
|
17698 * In addition, you can supply a `headers` property in the config object passed when |
|
17699 * calling `$http(config)`, which overrides the defaults without changing them globally. |
|
17700 * |
|
17701 * |
|
17702 * ## Transforming Requests and Responses |
|
17703 * |
|
17704 * Both requests and responses can be transformed using transformation functions: `transformRequest` |
|
17705 * and `transformResponse`. These properties can be a single function that returns |
|
17706 * the transformed value (`{function(data, headersGetter)`) or an array of such transformation functions, |
|
17707 * which allows you to `push` or `unshift` a new transformation function into the transformation chain. |
|
17708 * |
|
17709 * ### Default Transformations |
|
17710 * |
|
17711 * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and |
|
17712 * `defaults.transformResponse` properties. If a request does not provide its own transformations |
|
17713 * then these will be applied. |
|
17714 * |
|
17715 * You can augment or replace the default transformations by modifying these properties by adding to or |
|
17716 * replacing the array. |
|
17717 * |
|
17718 * Angular provides the following default transformations: |
|
17719 * |
|
17720 * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`): |
|
17721 * |
|
17722 * - If the `data` property of the request configuration object contains an object, serialize it |
|
17723 * into JSON format. |
|
17724 * |
|
17725 * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`): |
|
17726 * |
|
17727 * - If XSRF prefix is detected, strip it (see Security Considerations section below). |
|
17728 * - If JSON response is detected, deserialize it using a JSON parser. |
|
17729 * |
|
17730 * |
|
17731 * ### Overriding the Default Transformations Per Request |
|
17732 * |
|
17733 * If you wish override the request/response transformations only for a single request then provide |
|
17734 * `transformRequest` and/or `transformResponse` properties on the configuration object passed |
|
17735 * into `$http`. |
|
17736 * |
|
17737 * Note that if you provide these properties on the config object the default transformations will be |
|
17738 * overwritten. If you wish to augment the default transformations then you must include them in your |
|
17739 * local transformation array. |
|
17740 * |
|
17741 * The following code demonstrates adding a new response transformation to be run after the default response |
|
17742 * transformations have been run. |
|
17743 * |
|
17744 * ```js |
|
17745 * function appendTransform(defaults, transform) { |
|
17746 * |
|
17747 * // We can't guarantee that the default transformation is an array |
|
17748 * defaults = angular.isArray(defaults) ? defaults : [defaults]; |
|
17749 * |
|
17750 * // Append the new transformation to the defaults |
|
17751 * return defaults.concat(transform); |
|
17752 * } |
|
17753 * |
|
17754 * $http({ |
|
17755 * url: '...', |
|
17756 * method: 'GET', |
|
17757 * transformResponse: appendTransform($http.defaults.transformResponse, function(value) { |
|
17758 * return doTransform(value); |
|
17759 * }) |
|
17760 * }); |
|
17761 * ``` |
|
17762 * |
|
17763 * |
|
17764 * ## Caching |
|
17765 * |
|
17766 * To enable caching, set the request configuration `cache` property to `true` (to use default |
|
17767 * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). |
|
17768 * When the cache is enabled, `$http` stores the response from the server in the specified |
|
17769 * cache. The next time the same request is made, the response is served from the cache without |
|
17770 * sending a request to the server. |
|
17771 * |
|
17772 * Note that even if the response is served from cache, delivery of the data is asynchronous in |
|
17773 * the same way that real requests are. |
|
17774 * |
|
17775 * If there are multiple GET requests for the same URL that should be cached using the same |
|
17776 * cache, but the cache is not populated yet, only one request to the server will be made and |
|
17777 * the remaining requests will be fulfilled using the response from the first request. |
|
17778 * |
|
17779 * You can change the default cache to a new object (built with |
|
17780 * {@link ng.$cacheFactory `$cacheFactory`}) by updating the |
|
17781 * {@link ng.$http#properties_defaults `$http.defaults.cache`} property. All requests who set |
|
17782 * their `cache` property to `true` will now use this cache object. |
|
17783 * |
|
17784 * If you set the default cache to `false` then only requests that specify their own custom |
|
17785 * cache object will be cached. |
|
17786 * |
|
17787 * ## Interceptors |
|
17788 * |
|
17789 * Before you start creating interceptors, be sure to understand the |
|
17790 * {@link ng.$q $q and deferred/promise APIs}. |
|
17791 * |
|
17792 * For purposes of global error handling, authentication, or any kind of synchronous or |
|
17793 * asynchronous pre-processing of request or postprocessing of responses, it is desirable to be |
|
17794 * able to intercept requests before they are handed to the server and |
|
17795 * responses before they are handed over to the application code that |
|
17796 * initiated these requests. The interceptors leverage the {@link ng.$q |
|
17797 * promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing. |
|
17798 * |
|
17799 * The interceptors are service factories that are registered with the `$httpProvider` by |
|
17800 * adding them to the `$httpProvider.interceptors` array. The factory is called and |
|
17801 * injected with dependencies (if specified) and returns the interceptor. |
|
17802 * |
|
17803 * There are two kinds of interceptors (and two kinds of rejection interceptors): |
|
17804 * |
|
17805 * * `request`: interceptors get called with a http `config` object. The function is free to |
|
17806 * modify the `config` object or create a new one. The function needs to return the `config` |
|
17807 * object directly, or a promise containing the `config` or a new `config` object. |
|
17808 * * `requestError`: interceptor gets called when a previous interceptor threw an error or |
|
17809 * resolved with a rejection. |
|
17810 * * `response`: interceptors get called with http `response` object. The function is free to |
|
17811 * modify the `response` object or create a new one. The function needs to return the `response` |
|
17812 * object directly, or as a promise containing the `response` or a new `response` object. |
|
17813 * * `responseError`: interceptor gets called when a previous interceptor threw an error or |
|
17814 * resolved with a rejection. |
|
17815 * |
|
17816 * |
|
17817 * ```js |
|
17818 * // register the interceptor as a service |
|
17819 * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { |
|
17820 * return { |
|
17821 * // optional method |
|
17822 * 'request': function(config) { |
|
17823 * // do something on success |
|
17824 * return config; |
|
17825 * }, |
|
17826 * |
|
17827 * // optional method |
|
17828 * 'requestError': function(rejection) { |
|
17829 * // do something on error |
|
17830 * if (canRecover(rejection)) { |
|
17831 * return responseOrNewPromise |
|
17832 * } |
|
17833 * return $q.reject(rejection); |
|
17834 * }, |
|
17835 * |
|
17836 * |
|
17837 * |
|
17838 * // optional method |
|
17839 * 'response': function(response) { |
|
17840 * // do something on success |
|
17841 * return response; |
|
17842 * }, |
|
17843 * |
|
17844 * // optional method |
|
17845 * 'responseError': function(rejection) { |
|
17846 * // do something on error |
|
17847 * if (canRecover(rejection)) { |
|
17848 * return responseOrNewPromise |
|
17849 * } |
|
17850 * return $q.reject(rejection); |
|
17851 * } |
|
17852 * }; |
|
17853 * }); |
|
17854 * |
|
17855 * $httpProvider.interceptors.push('myHttpInterceptor'); |
|
17856 * |
|
17857 * |
|
17858 * // alternatively, register the interceptor via an anonymous factory |
|
17859 * $httpProvider.interceptors.push(function($q, dependency1, dependency2) { |
|
17860 * return { |
|
17861 * 'request': function(config) { |
|
17862 * // same as above |
|
17863 * }, |
|
17864 * |
|
17865 * 'response': function(response) { |
|
17866 * // same as above |
|
17867 * } |
|
17868 * }; |
|
17869 * }); |
|
17870 * ``` |
|
17871 * |
|
17872 * ## Security Considerations |
|
17873 * |
|
17874 * When designing web applications, consider security threats from: |
|
17875 * |
|
17876 * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) |
|
17877 * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) |
|
17878 * |
|
17879 * Both server and the client must cooperate in order to eliminate these threats. Angular comes |
|
17880 * pre-configured with strategies that address these issues, but for this to work backend server |
|
17881 * cooperation is required. |
|
17882 * |
|
17883 * ### JSON Vulnerability Protection |
|
17884 * |
|
17885 * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) |
|
17886 * allows third party website to turn your JSON resource URL into |
|
17887 * [JSONP](http://en.wikipedia.org/wiki/JSONP) request under some conditions. To |
|
17888 * counter this your server can prefix all JSON requests with following string `")]}',\n"`. |
|
17889 * Angular will automatically strip the prefix before processing it as JSON. |
|
17890 * |
|
17891 * For example if your server needs to return: |
|
17892 * ```js |
|
17893 * ['one','two'] |
|
17894 * ``` |
|
17895 * |
|
17896 * which is vulnerable to attack, your server can return: |
|
17897 * ```js |
|
17898 * )]}', |
|
17899 * ['one','two'] |
|
17900 * ``` |
|
17901 * |
|
17902 * Angular will strip the prefix, before processing the JSON. |
|
17903 * |
|
17904 * |
|
17905 * ### Cross Site Request Forgery (XSRF) Protection |
|
17906 * |
|
17907 * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which |
|
17908 * an unauthorized site can gain your user's private data. Angular provides a mechanism |
|
17909 * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie |
|
17910 * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only |
|
17911 * JavaScript that runs on your domain could read the cookie, your server can be assured that |
|
17912 * the XHR came from JavaScript running on your domain. The header will not be set for |
|
17913 * cross-domain requests. |
|
17914 * |
|
17915 * To take advantage of this, your server needs to set a token in a JavaScript readable session |
|
17916 * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the |
|
17917 * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure |
|
17918 * that only JavaScript running on your domain could have sent the request. The token must be |
|
17919 * unique for each user and must be verifiable by the server (to prevent the JavaScript from |
|
17920 * making up its own tokens). We recommend that the token is a digest of your site's |
|
17921 * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) |
|
17922 * for added security. |
|
17923 * |
|
17924 * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName |
|
17925 * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, |
|
17926 * or the per-request config object. |
|
17927 * |
|
17928 * |
|
17929 * @param {object} config Object describing the request to be made and how it should be |
|
17930 * processed. The object has following properties: |
|
17931 * |
|
17932 * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) |
|
17933 * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. |
|
17934 * - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be turned |
|
17935 * to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be |
|
17936 * JSONified. |
|
17937 * - **data** – `{string|Object}` – Data to be sent as the request message data. |
|
17938 * - **headers** – `{Object}` – Map of strings or functions which return strings representing |
|
17939 * HTTP headers to send to the server. If the return value of a function is null, the |
|
17940 * header will not be sent. |
|
17941 * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. |
|
17942 * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. |
|
17943 * - **transformRequest** – |
|
17944 * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – |
|
17945 * transform function or an array of such functions. The transform function takes the http |
|
17946 * request body and headers and returns its transformed (typically serialized) version. |
|
17947 * See {@link #overriding-the-default-transformations-per-request Overriding the Default Transformations} |
|
17948 * - **transformResponse** – |
|
17949 * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – |
|
17950 * transform function or an array of such functions. The transform function takes the http |
|
17951 * response body and headers and returns its transformed (typically deserialized) version. |
|
17952 * See {@link #overriding-the-default-transformations-per-request Overriding the Default Transformations} |
|
17953 * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the |
|
17954 * GET request, otherwise if a cache instance built with |
|
17955 * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for |
|
17956 * caching. |
|
17957 * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} |
|
17958 * that should abort the request when resolved. |
|
17959 * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the |
|
17960 * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials) |
|
17961 * for more information. |
|
17962 * - **responseType** - `{string}` - see |
|
17963 * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). |
|
17964 * |
|
17965 * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the |
|
17966 * standard `then` method and two http specific methods: `success` and `error`. The `then` |
|
17967 * method takes two arguments a success and an error callback which will be called with a |
|
17968 * response object. The `success` and `error` methods take a single argument - a function that |
|
17969 * will be called when the request succeeds or fails respectively. The arguments passed into |
|
17970 * these functions are destructured representation of the response object passed into the |
|
17971 * `then` method. The response object has these properties: |
|
17972 * |
|
17973 * - **data** – `{string|Object}` – The response body transformed with the transform |
|
17974 * functions. |
|
17975 * - **status** – `{number}` – HTTP status code of the response. |
|
17976 * - **headers** – `{function([headerName])}` – Header getter function. |
|
17977 * - **config** – `{Object}` – The configuration object that was used to generate the request. |
|
17978 * - **statusText** – `{string}` – HTTP status text of the response. |
|
17979 * |
|
17980 * @property {Array.<Object>} pendingRequests Array of config objects for currently pending |
|
17981 * requests. This is primarily meant to be used for debugging purposes. |
|
17982 * |
|
17983 * |
|
17984 * @example |
|
17985 <example module="httpExample"> |
|
17986 <file name="index.html"> |
|
17987 <div ng-controller="FetchController"> |
|
17988 <select ng-model="method"> |
|
17989 <option>GET</option> |
|
17990 <option>JSONP</option> |
|
17991 </select> |
|
17992 <input type="text" ng-model="url" size="80"/> |
|
17993 <button id="fetchbtn" ng-click="fetch()">fetch</button><br> |
|
17994 <button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button> |
|
17995 <button id="samplejsonpbtn" |
|
17996 ng-click="updateModel('JSONP', |
|
17997 'https://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')"> |
|
17998 Sample JSONP |
|
17999 </button> |
|
18000 <button id="invalidjsonpbtn" |
|
18001 ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist&callback=JSON_CALLBACK')"> |
|
18002 Invalid JSONP |
|
18003 </button> |
|
18004 <pre>http status code: {{status}}</pre> |
|
18005 <pre>http response data: {{data}}</pre> |
|
18006 </div> |
|
18007 </file> |
|
18008 <file name="script.js"> |
|
18009 angular.module('httpExample', []) |
|
18010 .controller('FetchController', ['$scope', '$http', '$templateCache', |
|
18011 function($scope, $http, $templateCache) { |
|
18012 $scope.method = 'GET'; |
|
18013 $scope.url = 'http-hello.html'; |
|
18014 |
|
18015 $scope.fetch = function() { |
|
18016 $scope.code = null; |
|
18017 $scope.response = null; |
|
18018 |
|
18019 $http({method: $scope.method, url: $scope.url, cache: $templateCache}). |
|
18020 success(function(data, status) { |
|
18021 $scope.status = status; |
|
18022 $scope.data = data; |
|
18023 }). |
|
18024 error(function(data, status) { |
|
18025 $scope.data = data || "Request failed"; |
|
18026 $scope.status = status; |
|
18027 }); |
|
18028 }; |
|
18029 |
|
18030 $scope.updateModel = function(method, url) { |
|
18031 $scope.method = method; |
|
18032 $scope.url = url; |
|
18033 }; |
|
18034 }]); |
|
18035 </file> |
|
18036 <file name="http-hello.html"> |
|
18037 Hello, $http! |
|
18038 </file> |
|
18039 <file name="protractor.js" type="protractor"> |
|
18040 var status = element(by.binding('status')); |
|
18041 var data = element(by.binding('data')); |
|
18042 var fetchBtn = element(by.id('fetchbtn')); |
|
18043 var sampleGetBtn = element(by.id('samplegetbtn')); |
|
18044 var sampleJsonpBtn = element(by.id('samplejsonpbtn')); |
|
18045 var invalidJsonpBtn = element(by.id('invalidjsonpbtn')); |
|
18046 |
|
18047 it('should make an xhr GET request', function() { |
|
18048 sampleGetBtn.click(); |
|
18049 fetchBtn.click(); |
|
18050 expect(status.getText()).toMatch('200'); |
|
18051 expect(data.getText()).toMatch(/Hello, \$http!/); |
|
18052 }); |
|
18053 |
|
18054 // Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185 |
|
18055 // it('should make a JSONP request to angularjs.org', function() { |
|
18056 // sampleJsonpBtn.click(); |
|
18057 // fetchBtn.click(); |
|
18058 // expect(status.getText()).toMatch('200'); |
|
18059 // expect(data.getText()).toMatch(/Super Hero!/); |
|
18060 // }); |
|
18061 |
|
18062 it('should make JSONP request to invalid URL and invoke the error handler', |
|
18063 function() { |
|
18064 invalidJsonpBtn.click(); |
|
18065 fetchBtn.click(); |
|
18066 expect(status.getText()).toMatch('0'); |
|
18067 expect(data.getText()).toMatch('Request failed'); |
|
18068 }); |
|
18069 </file> |
|
18070 </example> |
|
18071 */ |
|
18072 function $http(requestConfig) { |
|
18073 var config = { |
|
18074 method: 'get', |
|
18075 transformRequest: defaults.transformRequest, |
|
18076 transformResponse: defaults.transformResponse |
|
18077 }; |
|
18078 var headers = mergeHeaders(requestConfig); |
|
18079 |
|
18080 extend(config, requestConfig); |
|
18081 config.headers = headers; |
|
18082 config.method = uppercase(config.method); |
|
18083 |
|
18084 var serverRequest = function(config) { |
|
18085 headers = config.headers; |
|
18086 var reqData = transformData(config.data, headersGetter(headers), config.transformRequest); |
|
18087 |
|
18088 // strip content-type if data is undefined |
|
18089 if (isUndefined(reqData)) { |
|
18090 forEach(headers, function(value, header) { |
|
18091 if (lowercase(header) === 'content-type') { |
|
18092 delete headers[header]; |
|
18093 } |
|
18094 }); |
|
18095 } |
|
18096 |
|
18097 if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) { |
|
18098 config.withCredentials = defaults.withCredentials; |
|
18099 } |
|
18100 |
|
18101 // send request |
|
18102 return sendReq(config, reqData, headers).then(transformResponse, transformResponse); |
|
18103 }; |
|
18104 |
|
18105 var chain = [serverRequest, undefined]; |
|
18106 var promise = $q.when(config); |
|
18107 |
|
18108 // apply interceptors |
|
18109 forEach(reversedInterceptors, function(interceptor) { |
|
18110 if (interceptor.request || interceptor.requestError) { |
|
18111 chain.unshift(interceptor.request, interceptor.requestError); |
|
18112 } |
|
18113 if (interceptor.response || interceptor.responseError) { |
|
18114 chain.push(interceptor.response, interceptor.responseError); |
|
18115 } |
|
18116 }); |
|
18117 |
|
18118 while(chain.length) { |
|
18119 var thenFn = chain.shift(); |
|
18120 var rejectFn = chain.shift(); |
|
18121 |
|
18122 promise = promise.then(thenFn, rejectFn); |
|
18123 } |
|
18124 |
|
18125 promise.success = function(fn) { |
|
18126 promise.then(function(response) { |
|
18127 fn(response.data, response.status, response.headers, config); |
|
18128 }); |
|
18129 return promise; |
|
18130 }; |
|
18131 |
|
18132 promise.error = function(fn) { |
|
18133 promise.then(null, function(response) { |
|
18134 fn(response.data, response.status, response.headers, config); |
|
18135 }); |
|
18136 return promise; |
|
18137 }; |
|
18138 |
|
18139 return promise; |
|
18140 |
|
18141 function transformResponse(response) { |
|
18142 // make a copy since the response must be cacheable |
|
18143 var resp = extend({}, response, { |
|
18144 data: transformData(response.data, response.headers, config.transformResponse) |
|
18145 }); |
|
18146 return (isSuccess(response.status)) |
|
18147 ? resp |
|
18148 : $q.reject(resp); |
|
18149 } |
|
18150 |
|
18151 function mergeHeaders(config) { |
|
18152 var defHeaders = defaults.headers, |
|
18153 reqHeaders = extend({}, config.headers), |
|
18154 defHeaderName, lowercaseDefHeaderName, reqHeaderName; |
|
18155 |
|
18156 defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); |
|
18157 |
|
18158 // using for-in instead of forEach to avoid unecessary iteration after header has been found |
|
18159 defaultHeadersIteration: |
|
18160 for (defHeaderName in defHeaders) { |
|
18161 lowercaseDefHeaderName = lowercase(defHeaderName); |
|
18162 |
|
18163 for (reqHeaderName in reqHeaders) { |
|
18164 if (lowercase(reqHeaderName) === lowercaseDefHeaderName) { |
|
18165 continue defaultHeadersIteration; |
|
18166 } |
|
18167 } |
|
18168 |
|
18169 reqHeaders[defHeaderName] = defHeaders[defHeaderName]; |
|
18170 } |
|
18171 |
|
18172 // execute if header value is a function for merged headers |
|
18173 execHeaders(reqHeaders); |
|
18174 return reqHeaders; |
|
18175 |
|
18176 function execHeaders(headers) { |
|
18177 var headerContent; |
|
18178 |
|
18179 forEach(headers, function(headerFn, header) { |
|
18180 if (isFunction(headerFn)) { |
|
18181 headerContent = headerFn(); |
|
18182 if (headerContent != null) { |
|
18183 headers[header] = headerContent; |
|
18184 } else { |
|
18185 delete headers[header]; |
|
18186 } |
|
18187 } |
|
18188 }); |
|
18189 } |
|
18190 } |
|
18191 } |
|
18192 |
|
18193 $http.pendingRequests = []; |
|
18194 |
|
18195 /** |
|
18196 * @ngdoc method |
|
18197 * @name $http#get |
|
18198 * |
|
18199 * @description |
|
18200 * Shortcut method to perform `GET` request. |
|
18201 * |
|
18202 * @param {string} url Relative or absolute URL specifying the destination of the request |
|
18203 * @param {Object=} config Optional configuration object |
|
18204 * @returns {HttpPromise} Future object |
|
18205 */ |
|
18206 |
|
18207 /** |
|
18208 * @ngdoc method |
|
18209 * @name $http#delete |
|
18210 * |
|
18211 * @description |
|
18212 * Shortcut method to perform `DELETE` request. |
|
18213 * |
|
18214 * @param {string} url Relative or absolute URL specifying the destination of the request |
|
18215 * @param {Object=} config Optional configuration object |
|
18216 * @returns {HttpPromise} Future object |
|
18217 */ |
|
18218 |
|
18219 /** |
|
18220 * @ngdoc method |
|
18221 * @name $http#head |
|
18222 * |
|
18223 * @description |
|
18224 * Shortcut method to perform `HEAD` request. |
|
18225 * |
|
18226 * @param {string} url Relative or absolute URL specifying the destination of the request |
|
18227 * @param {Object=} config Optional configuration object |
|
18228 * @returns {HttpPromise} Future object |
|
18229 */ |
|
18230 |
|
18231 /** |
|
18232 * @ngdoc method |
|
18233 * @name $http#jsonp |
|
18234 * |
|
18235 * @description |
|
18236 * Shortcut method to perform `JSONP` request. |
|
18237 * |
|
18238 * @param {string} url Relative or absolute URL specifying the destination of the request. |
|
18239 * The name of the callback should be the string `JSON_CALLBACK`. |
|
18240 * @param {Object=} config Optional configuration object |
|
18241 * @returns {HttpPromise} Future object |
|
18242 */ |
|
18243 createShortMethods('get', 'delete', 'head', 'jsonp'); |
|
18244 |
|
18245 /** |
|
18246 * @ngdoc method |
|
18247 * @name $http#post |
|
18248 * |
|
18249 * @description |
|
18250 * Shortcut method to perform `POST` request. |
|
18251 * |
|
18252 * @param {string} url Relative or absolute URL specifying the destination of the request |
|
18253 * @param {*} data Request content |
|
18254 * @param {Object=} config Optional configuration object |
|
18255 * @returns {HttpPromise} Future object |
|
18256 */ |
|
18257 |
|
18258 /** |
|
18259 * @ngdoc method |
|
18260 * @name $http#put |
|
18261 * |
|
18262 * @description |
|
18263 * Shortcut method to perform `PUT` request. |
|
18264 * |
|
18265 * @param {string} url Relative or absolute URL specifying the destination of the request |
|
18266 * @param {*} data Request content |
|
18267 * @param {Object=} config Optional configuration object |
|
18268 * @returns {HttpPromise} Future object |
|
18269 */ |
|
18270 |
|
18271 /** |
|
18272 * @ngdoc method |
|
18273 * @name $http#patch |
|
18274 * |
|
18275 * @description |
|
18276 * Shortcut method to perform `PATCH` request. |
|
18277 * |
|
18278 * @param {string} url Relative or absolute URL specifying the destination of the request |
|
18279 * @param {*} data Request content |
|
18280 * @param {Object=} config Optional configuration object |
|
18281 * @returns {HttpPromise} Future object |
|
18282 */ |
|
18283 createShortMethodsWithData('post', 'put', 'patch'); |
|
18284 |
|
18285 /** |
|
18286 * @ngdoc property |
|
18287 * @name $http#defaults |
|
18288 * |
|
18289 * @description |
|
18290 * Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of |
|
18291 * default headers, withCredentials as well as request and response transformations. |
|
18292 * |
|
18293 * See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above. |
|
18294 */ |
|
18295 $http.defaults = defaults; |
|
18296 |
|
18297 |
|
18298 return $http; |
|
18299 |
|
18300 |
|
18301 function createShortMethods(names) { |
|
18302 forEach(arguments, function(name) { |
|
18303 $http[name] = function(url, config) { |
|
18304 return $http(extend(config || {}, { |
|
18305 method: name, |
|
18306 url: url |
|
18307 })); |
|
18308 }; |
|
18309 }); |
|
18310 } |
|
18311 |
|
18312 |
|
18313 function createShortMethodsWithData(name) { |
|
18314 forEach(arguments, function(name) { |
|
18315 $http[name] = function(url, data, config) { |
|
18316 return $http(extend(config || {}, { |
|
18317 method: name, |
|
18318 url: url, |
|
18319 data: data |
|
18320 })); |
|
18321 }; |
|
18322 }); |
|
18323 } |
|
18324 |
|
18325 |
|
18326 /** |
|
18327 * Makes the request. |
|
18328 * |
|
18329 * !!! ACCESSES CLOSURE VARS: |
|
18330 * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests |
|
18331 */ |
|
18332 function sendReq(config, reqData, reqHeaders) { |
|
18333 var deferred = $q.defer(), |
|
18334 promise = deferred.promise, |
|
18335 cache, |
|
18336 cachedResp, |
|
18337 url = buildUrl(config.url, config.params); |
|
18338 |
|
18339 $http.pendingRequests.push(config); |
|
18340 promise.then(removePendingReq, removePendingReq); |
|
18341 |
|
18342 |
|
18343 if ((config.cache || defaults.cache) && config.cache !== false && |
|
18344 (config.method === 'GET' || config.method === 'JSONP')) { |
|
18345 cache = isObject(config.cache) ? config.cache |
|
18346 : isObject(defaults.cache) ? defaults.cache |
|
18347 : defaultCache; |
|
18348 } |
|
18349 |
|
18350 if (cache) { |
|
18351 cachedResp = cache.get(url); |
|
18352 if (isDefined(cachedResp)) { |
|
18353 if (isPromiseLike(cachedResp)) { |
|
18354 // cached request has already been sent, but there is no response yet |
|
18355 cachedResp.then(removePendingReq, removePendingReq); |
|
18356 return cachedResp; |
|
18357 } else { |
|
18358 // serving from cache |
|
18359 if (isArray(cachedResp)) { |
|
18360 resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]); |
|
18361 } else { |
|
18362 resolvePromise(cachedResp, 200, {}, 'OK'); |
|
18363 } |
|
18364 } |
|
18365 } else { |
|
18366 // put the promise for the non-transformed response into cache as a placeholder |
|
18367 cache.put(url, promise); |
|
18368 } |
|
18369 } |
|
18370 |
|
18371 |
|
18372 // if we won't have the response in cache, set the xsrf headers and |
|
18373 // send the request to the backend |
|
18374 if (isUndefined(cachedResp)) { |
|
18375 var xsrfValue = urlIsSameOrigin(config.url) |
|
18376 ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] |
|
18377 : undefined; |
|
18378 if (xsrfValue) { |
|
18379 reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; |
|
18380 } |
|
18381 |
|
18382 $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, |
|
18383 config.withCredentials, config.responseType); |
|
18384 } |
|
18385 |
|
18386 return promise; |
|
18387 |
|
18388 |
|
18389 /** |
|
18390 * Callback registered to $httpBackend(): |
|
18391 * - caches the response if desired |
|
18392 * - resolves the raw $http promise |
|
18393 * - calls $apply |
|
18394 */ |
|
18395 function done(status, response, headersString, statusText) { |
|
18396 if (cache) { |
|
18397 if (isSuccess(status)) { |
|
18398 cache.put(url, [status, response, parseHeaders(headersString), statusText]); |
|
18399 } else { |
|
18400 // remove promise from the cache |
|
18401 cache.remove(url); |
|
18402 } |
|
18403 } |
|
18404 |
|
18405 function resolveHttpPromise() { |
|
18406 resolvePromise(response, status, headersString, statusText); |
|
18407 } |
|
18408 |
|
18409 if (useApplyAsync) { |
|
18410 $rootScope.$applyAsync(resolveHttpPromise); |
|
18411 } else { |
|
18412 resolveHttpPromise(); |
|
18413 if (!$rootScope.$$phase) $rootScope.$apply(); |
|
18414 } |
|
18415 } |
|
18416 |
|
18417 |
|
18418 /** |
|
18419 * Resolves the raw $http promise. |
|
18420 */ |
|
18421 function resolvePromise(response, status, headers, statusText) { |
|
18422 // normalize internal statuses to 0 |
|
18423 status = Math.max(status, 0); |
|
18424 |
|
18425 (isSuccess(status) ? deferred.resolve : deferred.reject)({ |
|
18426 data: response, |
|
18427 status: status, |
|
18428 headers: headersGetter(headers), |
|
18429 config: config, |
|
18430 statusText : statusText |
|
18431 }); |
|
18432 } |
|
18433 |
|
18434 |
|
18435 function removePendingReq() { |
|
18436 var idx = $http.pendingRequests.indexOf(config); |
|
18437 if (idx !== -1) $http.pendingRequests.splice(idx, 1); |
|
18438 } |
|
18439 } |
|
18440 |
|
18441 |
|
18442 function buildUrl(url, params) { |
|
18443 if (!params) return url; |
|
18444 var parts = []; |
|
18445 forEachSorted(params, function(value, key) { |
|
18446 if (value === null || isUndefined(value)) return; |
|
18447 if (!isArray(value)) value = [value]; |
|
18448 |
|
18449 forEach(value, function(v) { |
|
18450 if (isObject(v)) { |
|
18451 if (isDate(v)){ |
|
18452 v = v.toISOString(); |
|
18453 } else { |
|
18454 v = toJson(v); |
|
18455 } |
|
18456 } |
|
18457 parts.push(encodeUriQuery(key) + '=' + |
|
18458 encodeUriQuery(v)); |
|
18459 }); |
|
18460 }); |
|
18461 if(parts.length > 0) { |
|
18462 url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); |
|
18463 } |
|
18464 return url; |
|
18465 } |
|
18466 }]; |
|
18467 } |
|
18468 |
|
18469 function createXhr() { |
|
18470 return new window.XMLHttpRequest(); |
|
18471 } |
|
18472 |
|
18473 /** |
|
18474 * @ngdoc service |
|
18475 * @name $httpBackend |
|
18476 * @requires $window |
|
18477 * @requires $document |
|
18478 * |
|
18479 * @description |
|
18480 * HTTP backend used by the {@link ng.$http service} that delegates to |
|
18481 * XMLHttpRequest object or JSONP and deals with browser incompatibilities. |
|
18482 * |
|
18483 * You should never need to use this service directly, instead use the higher-level abstractions: |
|
18484 * {@link ng.$http $http} or {@link ngResource.$resource $resource}. |
|
18485 * |
|
18486 * During testing this implementation is swapped with {@link ngMock.$httpBackend mock |
|
18487 * $httpBackend} which can be trained with responses. |
|
18488 */ |
|
18489 function $HttpBackendProvider() { |
|
18490 this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { |
|
18491 return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]); |
|
18492 }]; |
|
18493 } |
|
18494 |
|
18495 function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { |
|
18496 // TODO(vojta): fix the signature |
|
18497 return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { |
|
18498 $browser.$$incOutstandingRequestCount(); |
|
18499 url = url || $browser.url(); |
|
18500 |
|
18501 if (lowercase(method) == 'jsonp') { |
|
18502 var callbackId = '_' + (callbacks.counter++).toString(36); |
|
18503 callbacks[callbackId] = function(data) { |
|
18504 callbacks[callbackId].data = data; |
|
18505 callbacks[callbackId].called = true; |
|
18506 }; |
|
18507 |
|
18508 var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), |
|
18509 callbackId, function(status, text) { |
|
18510 completeRequest(callback, status, callbacks[callbackId].data, "", text); |
|
18511 callbacks[callbackId] = noop; |
|
18512 }); |
|
18513 } else { |
|
18514 |
|
18515 var xhr = createXhr(); |
|
18516 |
|
18517 xhr.open(method, url, true); |
|
18518 forEach(headers, function(value, key) { |
|
18519 if (isDefined(value)) { |
|
18520 xhr.setRequestHeader(key, value); |
|
18521 } |
|
18522 }); |
|
18523 |
|
18524 xhr.onload = function requestLoaded() { |
|
18525 var statusText = xhr.statusText || ''; |
|
18526 |
|
18527 // responseText is the old-school way of retrieving response (supported by IE8 & 9) |
|
18528 // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) |
|
18529 var response = ('response' in xhr) ? xhr.response : xhr.responseText; |
|
18530 |
|
18531 // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) |
|
18532 var status = xhr.status === 1223 ? 204 : xhr.status; |
|
18533 |
|
18534 // fix status code when it is 0 (0 status is undocumented). |
|
18535 // Occurs when accessing file resources or on Android 4.1 stock browser |
|
18536 // while retrieving files from application cache. |
|
18537 if (status === 0) { |
|
18538 status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; |
|
18539 } |
|
18540 |
|
18541 completeRequest(callback, |
|
18542 status, |
|
18543 response, |
|
18544 xhr.getAllResponseHeaders(), |
|
18545 statusText); |
|
18546 }; |
|
18547 |
|
18548 var requestError = function () { |
|
18549 // The response is always empty |
|
18550 // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error |
|
18551 completeRequest(callback, -1, null, null, ''); |
|
18552 }; |
|
18553 |
|
18554 xhr.onerror = requestError; |
|
18555 xhr.onabort = requestError; |
|
18556 |
|
18557 if (withCredentials) { |
|
18558 xhr.withCredentials = true; |
|
18559 } |
|
18560 |
|
18561 if (responseType) { |
|
18562 try { |
|
18563 xhr.responseType = responseType; |
|
18564 } catch (e) { |
|
18565 // WebKit added support for the json responseType value on 09/03/2013 |
|
18566 // https://bugs.webkit.org/show_bug.cgi?id=73648. Versions of Safari prior to 7 are |
|
18567 // known to throw when setting the value "json" as the response type. Other older |
|
18568 // browsers implementing the responseType |
|
18569 // |
|
18570 // The json response type can be ignored if not supported, because JSON payloads are |
|
18571 // parsed on the client-side regardless. |
|
18572 if (responseType !== 'json') { |
|
18573 throw e; |
|
18574 } |
|
18575 } |
|
18576 } |
|
18577 |
|
18578 xhr.send(post || null); |
|
18579 } |
|
18580 |
|
18581 if (timeout > 0) { |
|
18582 var timeoutId = $browserDefer(timeoutRequest, timeout); |
|
18583 } else if (isPromiseLike(timeout)) { |
|
18584 timeout.then(timeoutRequest); |
|
18585 } |
|
18586 |
|
18587 |
|
18588 function timeoutRequest() { |
|
18589 jsonpDone && jsonpDone(); |
|
18590 xhr && xhr.abort(); |
|
18591 } |
|
18592 |
|
18593 function completeRequest(callback, status, response, headersString, statusText) { |
|
18594 // cancel timeout and subsequent timeout promise resolution |
|
18595 timeoutId && $browserDefer.cancel(timeoutId); |
|
18596 jsonpDone = xhr = null; |
|
18597 |
|
18598 callback(status, response, headersString, statusText); |
|
18599 $browser.$$completeOutstandingRequest(noop); |
|
18600 } |
|
18601 }; |
|
18602 |
|
18603 function jsonpReq(url, callbackId, done) { |
|
18604 // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: |
|
18605 // - fetches local scripts via XHR and evals them |
|
18606 // - adds and immediately removes script elements from the document |
|
18607 var script = rawDocument.createElement('script'), callback = null; |
|
18608 script.type = "text/javascript"; |
|
18609 script.src = url; |
|
18610 script.async = true; |
|
18611 |
|
18612 callback = function(event) { |
|
18613 removeEventListenerFn(script, "load", callback); |
|
18614 removeEventListenerFn(script, "error", callback); |
|
18615 rawDocument.body.removeChild(script); |
|
18616 script = null; |
|
18617 var status = -1; |
|
18618 var text = "unknown"; |
|
18619 |
|
18620 if (event) { |
|
18621 if (event.type === "load" && !callbacks[callbackId].called) { |
|
18622 event = { type: "error" }; |
|
18623 } |
|
18624 text = event.type; |
|
18625 status = event.type === "error" ? 404 : 200; |
|
18626 } |
|
18627 |
|
18628 if (done) { |
|
18629 done(status, text); |
|
18630 } |
|
18631 }; |
|
18632 |
|
18633 addEventListenerFn(script, "load", callback); |
|
18634 addEventListenerFn(script, "error", callback); |
|
18635 rawDocument.body.appendChild(script); |
|
18636 return callback; |
|
18637 } |
|
18638 } |
|
18639 |
|
18640 var $interpolateMinErr = minErr('$interpolate'); |
|
18641 |
|
18642 /** |
|
18643 * @ngdoc provider |
|
18644 * @name $interpolateProvider |
|
18645 * |
|
18646 * @description |
|
18647 * |
|
18648 * Used for configuring the interpolation markup. Defaults to `{{` and `}}`. |
|
18649 * |
|
18650 * @example |
|
18651 <example module="customInterpolationApp"> |
|
18652 <file name="index.html"> |
|
18653 <script> |
|
18654 var customInterpolationApp = angular.module('customInterpolationApp', []); |
|
18655 |
|
18656 customInterpolationApp.config(function($interpolateProvider) { |
|
18657 $interpolateProvider.startSymbol('//'); |
|
18658 $interpolateProvider.endSymbol('//'); |
|
18659 }); |
|
18660 |
|
18661 |
|
18662 customInterpolationApp.controller('DemoController', function() { |
|
18663 this.label = "This binding is brought you by // interpolation symbols."; |
|
18664 }); |
|
18665 </script> |
|
18666 <div ng-app="App" ng-controller="DemoController as demo"> |
|
18667 //demo.label// |
|
18668 </div> |
|
18669 </file> |
|
18670 <file name="protractor.js" type="protractor"> |
|
18671 it('should interpolate binding with custom symbols', function() { |
|
18672 expect(element(by.binding('demo.label')).getText()).toBe('This binding is brought you by // interpolation symbols.'); |
|
18673 }); |
|
18674 </file> |
|
18675 </example> |
|
18676 */ |
|
18677 function $InterpolateProvider() { |
|
18678 var startSymbol = '{{'; |
|
18679 var endSymbol = '}}'; |
|
18680 |
|
18681 /** |
|
18682 * @ngdoc method |
|
18683 * @name $interpolateProvider#startSymbol |
|
18684 * @description |
|
18685 * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. |
|
18686 * |
|
18687 * @param {string=} value new value to set the starting symbol to. |
|
18688 * @returns {string|self} Returns the symbol when used as getter and self if used as setter. |
|
18689 */ |
|
18690 this.startSymbol = function(value){ |
|
18691 if (value) { |
|
18692 startSymbol = value; |
|
18693 return this; |
|
18694 } else { |
|
18695 return startSymbol; |
|
18696 } |
|
18697 }; |
|
18698 |
|
18699 /** |
|
18700 * @ngdoc method |
|
18701 * @name $interpolateProvider#endSymbol |
|
18702 * @description |
|
18703 * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. |
|
18704 * |
|
18705 * @param {string=} value new value to set the ending symbol to. |
|
18706 * @returns {string|self} Returns the symbol when used as getter and self if used as setter. |
|
18707 */ |
|
18708 this.endSymbol = function(value){ |
|
18709 if (value) { |
|
18710 endSymbol = value; |
|
18711 return this; |
|
18712 } else { |
|
18713 return endSymbol; |
|
18714 } |
|
18715 }; |
|
18716 |
|
18717 |
|
18718 this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { |
|
18719 var startSymbolLength = startSymbol.length, |
|
18720 endSymbolLength = endSymbol.length, |
|
18721 escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'), |
|
18722 escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g'); |
|
18723 |
|
18724 function escape(ch) { |
|
18725 return '\\\\\\' + ch; |
|
18726 } |
|
18727 |
|
18728 /** |
|
18729 * @ngdoc service |
|
18730 * @name $interpolate |
|
18731 * @kind function |
|
18732 * |
|
18733 * @requires $parse |
|
18734 * @requires $sce |
|
18735 * |
|
18736 * @description |
|
18737 * |
|
18738 * Compiles a string with markup into an interpolation function. This service is used by the |
|
18739 * HTML {@link ng.$compile $compile} service for data binding. See |
|
18740 * {@link ng.$interpolateProvider $interpolateProvider} for configuring the |
|
18741 * interpolation markup. |
|
18742 * |
|
18743 * |
|
18744 * ```js |
|
18745 * var $interpolate = ...; // injected |
|
18746 * var exp = $interpolate('Hello {{name | uppercase}}!'); |
|
18747 * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); |
|
18748 * ``` |
|
18749 * |
|
18750 * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is |
|
18751 * `true`, the interpolation function will return `undefined` unless all embedded expressions |
|
18752 * evaluate to a value other than `undefined`. |
|
18753 * |
|
18754 * ```js |
|
18755 * var $interpolate = ...; // injected |
|
18756 * var context = {greeting: 'Hello', name: undefined }; |
|
18757 * |
|
18758 * // default "forgiving" mode |
|
18759 * var exp = $interpolate('{{greeting}} {{name}}!'); |
|
18760 * expect(exp(context)).toEqual('Hello !'); |
|
18761 * |
|
18762 * // "allOrNothing" mode |
|
18763 * exp = $interpolate('{{greeting}} {{name}}!', false, null, true); |
|
18764 * expect(exp(context)).toBeUndefined(); |
|
18765 * context.name = 'Angular'; |
|
18766 * expect(exp(context)).toEqual('Hello Angular!'); |
|
18767 * ``` |
|
18768 * |
|
18769 * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior. |
|
18770 * |
|
18771 * ####Escaped Interpolation |
|
18772 * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers |
|
18773 * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash). |
|
18774 * It will be rendered as a regular start/end marker, and will not be interpreted as an expression |
|
18775 * or binding. |
|
18776 * |
|
18777 * This enables web-servers to prevent script injection attacks and defacing attacks, to some |
|
18778 * degree, while also enabling code examples to work without relying on the |
|
18779 * {@link ng.directive:ngNonBindable ngNonBindable} directive. |
|
18780 * |
|
18781 * **For security purposes, it is strongly encouraged that web servers escape user-supplied data, |
|
18782 * replacing angle brackets (<, >) with &lt; and &gt; respectively, and replacing all |
|
18783 * interpolation start/end markers with their escaped counterparts.** |
|
18784 * |
|
18785 * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered |
|
18786 * output when the $interpolate service processes the text. So, for HTML elements interpolated |
|
18787 * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter |
|
18788 * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such, |
|
18789 * this is typically useful only when user-data is used in rendering a template from the server, or |
|
18790 * when otherwise untrusted data is used by a directive. |
|
18791 * |
|
18792 * <example> |
|
18793 * <file name="index.html"> |
|
18794 * <div ng-init="username='A user'"> |
|
18795 * <p ng-init="apptitle='Escaping demo'">{{apptitle}}: \{\{ username = "defaced value"; \}\} |
|
18796 * </p> |
|
18797 * <p><strong>{{username}}</strong> attempts to inject code which will deface the |
|
18798 * application, but fails to accomplish their task, because the server has correctly |
|
18799 * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash) |
|
18800 * characters.</p> |
|
18801 * <p>Instead, the result of the attempted script injection is visible, and can be removed |
|
18802 * from the database by an administrator.</p> |
|
18803 * </div> |
|
18804 * </file> |
|
18805 * </example> |
|
18806 * |
|
18807 * @param {string} text The text with markup to interpolate. |
|
18808 * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have |
|
18809 * embedded expression in order to return an interpolation function. Strings with no |
|
18810 * embedded expression will return null for the interpolation function. |
|
18811 * @param {string=} trustedContext when provided, the returned function passes the interpolated |
|
18812 * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, |
|
18813 * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that |
|
18814 * provides Strict Contextual Escaping for details. |
|
18815 * @param {boolean=} allOrNothing if `true`, then the returned function returns undefined |
|
18816 * unless all embedded expressions evaluate to a value other than `undefined`. |
|
18817 * @returns {function(context)} an interpolation function which is used to compute the |
|
18818 * interpolated string. The function has these parameters: |
|
18819 * |
|
18820 * - `context`: evaluation context for all expressions embedded in the interpolated text |
|
18821 */ |
|
18822 function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) { |
|
18823 allOrNothing = !!allOrNothing; |
|
18824 var startIndex, |
|
18825 endIndex, |
|
18826 index = 0, |
|
18827 expressions = [], |
|
18828 parseFns = [], |
|
18829 textLength = text.length, |
|
18830 exp, |
|
18831 concat = [], |
|
18832 expressionPositions = []; |
|
18833 |
|
18834 while(index < textLength) { |
|
18835 if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && |
|
18836 ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { |
|
18837 if (index !== startIndex) { |
|
18838 concat.push(unescapeText(text.substring(index, startIndex))); |
|
18839 } |
|
18840 exp = text.substring(startIndex + startSymbolLength, endIndex); |
|
18841 expressions.push(exp); |
|
18842 parseFns.push($parse(exp, parseStringifyInterceptor)); |
|
18843 index = endIndex + endSymbolLength; |
|
18844 expressionPositions.push(concat.length); |
|
18845 concat.push(''); |
|
18846 } else { |
|
18847 // we did not find an interpolation, so we have to add the remainder to the separators array |
|
18848 if (index !== textLength) { |
|
18849 concat.push(unescapeText(text.substring(index))); |
|
18850 } |
|
18851 break; |
|
18852 } |
|
18853 } |
|
18854 |
|
18855 // Concatenating expressions makes it hard to reason about whether some combination of |
|
18856 // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a |
|
18857 // single expression be used for iframe[src], object[src], etc., we ensure that the value |
|
18858 // that's used is assigned or constructed by some JS code somewhere that is more testable or |
|
18859 // make it obvious that you bound the value to some user controlled value. This helps reduce |
|
18860 // the load when auditing for XSS issues. |
|
18861 if (trustedContext && concat.length > 1) { |
|
18862 throw $interpolateMinErr('noconcat', |
|
18863 "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + |
|
18864 "interpolations that concatenate multiple expressions when a trusted value is " + |
|
18865 "required. See http://docs.angularjs.org/api/ng.$sce", text); |
|
18866 } |
|
18867 |
|
18868 if (!mustHaveExpression || expressions.length) { |
|
18869 var compute = function(values) { |
|
18870 for(var i = 0, ii = expressions.length; i < ii; i++) { |
|
18871 if (allOrNothing && isUndefined(values[i])) return; |
|
18872 concat[expressionPositions[i]] = values[i]; |
|
18873 } |
|
18874 return concat.join(''); |
|
18875 }; |
|
18876 |
|
18877 var getValue = function (value) { |
|
18878 return trustedContext ? |
|
18879 $sce.getTrusted(trustedContext, value) : |
|
18880 $sce.valueOf(value); |
|
18881 }; |
|
18882 |
|
18883 var stringify = function (value) { |
|
18884 if (value == null) { // null || undefined |
|
18885 return ''; |
|
18886 } |
|
18887 switch (typeof value) { |
|
18888 case 'string': { |
|
18889 break; |
|
18890 } |
|
18891 case 'number': { |
|
18892 value = '' + value; |
|
18893 break; |
|
18894 } |
|
18895 default: { |
|
18896 value = toJson(value); |
|
18897 } |
|
18898 } |
|
18899 |
|
18900 return value; |
|
18901 }; |
|
18902 |
|
18903 return extend(function interpolationFn(context) { |
|
18904 var i = 0; |
|
18905 var ii = expressions.length; |
|
18906 var values = new Array(ii); |
|
18907 |
|
18908 try { |
|
18909 for (; i < ii; i++) { |
|
18910 values[i] = parseFns[i](context); |
|
18911 } |
|
18912 |
|
18913 return compute(values); |
|
18914 } catch(err) { |
|
18915 var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, |
|
18916 err.toString()); |
|
18917 $exceptionHandler(newErr); |
|
18918 } |
|
18919 |
|
18920 }, { |
|
18921 // all of these properties are undocumented for now |
|
18922 exp: text, //just for compatibility with regular watchers created via $watch |
|
18923 expressions: expressions, |
|
18924 $$watchDelegate: function (scope, listener, objectEquality) { |
|
18925 var lastValue; |
|
18926 return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) { |
|
18927 var currValue = compute(values); |
|
18928 if (isFunction(listener)) { |
|
18929 listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope); |
|
18930 } |
|
18931 lastValue = currValue; |
|
18932 }, objectEquality); |
|
18933 } |
|
18934 }); |
|
18935 } |
|
18936 |
|
18937 function unescapeText(text) { |
|
18938 return text.replace(escapedStartRegexp, startSymbol). |
|
18939 replace(escapedEndRegexp, endSymbol); |
|
18940 } |
|
18941 |
|
18942 function parseStringifyInterceptor(value) { |
|
18943 try { |
|
18944 return stringify(getValue(value)); |
|
18945 } catch(err) { |
|
18946 var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, |
|
18947 err.toString()); |
|
18948 $exceptionHandler(newErr); |
|
18949 } |
|
18950 } |
|
18951 } |
|
18952 |
|
18953 |
|
18954 /** |
|
18955 * @ngdoc method |
|
18956 * @name $interpolate#startSymbol |
|
18957 * @description |
|
18958 * Symbol to denote the start of expression in the interpolated string. Defaults to `{{`. |
|
18959 * |
|
18960 * Use {@link ng.$interpolateProvider#startSymbol `$interpolateProvider.startSymbol`} to change |
|
18961 * the symbol. |
|
18962 * |
|
18963 * @returns {string} start symbol. |
|
18964 */ |
|
18965 $interpolate.startSymbol = function() { |
|
18966 return startSymbol; |
|
18967 }; |
|
18968 |
|
18969 |
|
18970 /** |
|
18971 * @ngdoc method |
|
18972 * @name $interpolate#endSymbol |
|
18973 * @description |
|
18974 * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. |
|
18975 * |
|
18976 * Use {@link ng.$interpolateProvider#endSymbol `$interpolateProvider.endSymbol`} to change |
|
18977 * the symbol. |
|
18978 * |
|
18979 * @returns {string} end symbol. |
|
18980 */ |
|
18981 $interpolate.endSymbol = function() { |
|
18982 return endSymbol; |
|
18983 }; |
|
18984 |
|
18985 return $interpolate; |
|
18986 }]; |
|
18987 } |
|
18988 |
|
18989 function $IntervalProvider() { |
|
18990 this.$get = ['$rootScope', '$window', '$q', '$$q', |
|
18991 function($rootScope, $window, $q, $$q) { |
|
18992 var intervals = {}; |
|
18993 |
|
18994 |
|
18995 /** |
|
18996 * @ngdoc service |
|
18997 * @name $interval |
|
18998 * |
|
18999 * @description |
|
19000 * Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay` |
|
19001 * milliseconds. |
|
19002 * |
|
19003 * The return value of registering an interval function is a promise. This promise will be |
|
19004 * notified upon each tick of the interval, and will be resolved after `count` iterations, or |
|
19005 * run indefinitely if `count` is not defined. The value of the notification will be the |
|
19006 * number of iterations that have run. |
|
19007 * To cancel an interval, call `$interval.cancel(promise)`. |
|
19008 * |
|
19009 * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to |
|
19010 * move forward by `millis` milliseconds and trigger any functions scheduled to run in that |
|
19011 * time. |
|
19012 * |
|
19013 * <div class="alert alert-warning"> |
|
19014 * **Note**: Intervals created by this service must be explicitly destroyed when you are finished |
|
19015 * with them. In particular they are not automatically destroyed when a controller's scope or a |
|
19016 * directive's element are destroyed. |
|
19017 * You should take this into consideration and make sure to always cancel the interval at the |
|
19018 * appropriate moment. See the example below for more details on how and when to do this. |
|
19019 * </div> |
|
19020 * |
|
19021 * @param {function()} fn A function that should be called repeatedly. |
|
19022 * @param {number} delay Number of milliseconds between each function call. |
|
19023 * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat |
|
19024 * indefinitely. |
|
19025 * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise |
|
19026 * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. |
|
19027 * @returns {promise} A promise which will be notified on each iteration. |
|
19028 * |
|
19029 * @example |
|
19030 * <example module="intervalExample"> |
|
19031 * <file name="index.html"> |
|
19032 * <script> |
|
19033 * angular.module('intervalExample', []) |
|
19034 * .controller('ExampleController', ['$scope', '$interval', |
|
19035 * function($scope, $interval) { |
|
19036 * $scope.format = 'M/d/yy h:mm:ss a'; |
|
19037 * $scope.blood_1 = 100; |
|
19038 * $scope.blood_2 = 120; |
|
19039 * |
|
19040 * var stop; |
|
19041 * $scope.fight = function() { |
|
19042 * // Don't start a new fight if we are already fighting |
|
19043 * if ( angular.isDefined(stop) ) return; |
|
19044 * |
|
19045 * stop = $interval(function() { |
|
19046 * if ($scope.blood_1 > 0 && $scope.blood_2 > 0) { |
|
19047 * $scope.blood_1 = $scope.blood_1 - 3; |
|
19048 * $scope.blood_2 = $scope.blood_2 - 4; |
|
19049 * } else { |
|
19050 * $scope.stopFight(); |
|
19051 * } |
|
19052 * }, 100); |
|
19053 * }; |
|
19054 * |
|
19055 * $scope.stopFight = function() { |
|
19056 * if (angular.isDefined(stop)) { |
|
19057 * $interval.cancel(stop); |
|
19058 * stop = undefined; |
|
19059 * } |
|
19060 * }; |
|
19061 * |
|
19062 * $scope.resetFight = function() { |
|
19063 * $scope.blood_1 = 100; |
|
19064 * $scope.blood_2 = 120; |
|
19065 * }; |
|
19066 * |
|
19067 * $scope.$on('$destroy', function() { |
|
19068 * // Make sure that the interval is destroyed too |
|
19069 * $scope.stopFight(); |
|
19070 * }); |
|
19071 * }]) |
|
19072 * // Register the 'myCurrentTime' directive factory method. |
|
19073 * // We inject $interval and dateFilter service since the factory method is DI. |
|
19074 * .directive('myCurrentTime', ['$interval', 'dateFilter', |
|
19075 * function($interval, dateFilter) { |
|
19076 * // return the directive link function. (compile function not needed) |
|
19077 * return function(scope, element, attrs) { |
|
19078 * var format, // date format |
|
19079 * stopTime; // so that we can cancel the time updates |
|
19080 * |
|
19081 * // used to update the UI |
|
19082 * function updateTime() { |
|
19083 * element.text(dateFilter(new Date(), format)); |
|
19084 * } |
|
19085 * |
|
19086 * // watch the expression, and update the UI on change. |
|
19087 * scope.$watch(attrs.myCurrentTime, function(value) { |
|
19088 * format = value; |
|
19089 * updateTime(); |
|
19090 * }); |
|
19091 * |
|
19092 * stopTime = $interval(updateTime, 1000); |
|
19093 * |
|
19094 * // listen on DOM destroy (removal) event, and cancel the next UI update |
|
19095 * // to prevent updating time after the DOM element was removed. |
|
19096 * element.on('$destroy', function() { |
|
19097 * $interval.cancel(stopTime); |
|
19098 * }); |
|
19099 * } |
|
19100 * }]); |
|
19101 * </script> |
|
19102 * |
|
19103 * <div> |
|
19104 * <div ng-controller="ExampleController"> |
|
19105 * Date format: <input ng-model="format"> <hr/> |
|
19106 * Current time is: <span my-current-time="format"></span> |
|
19107 * <hr/> |
|
19108 * Blood 1 : <font color='red'>{{blood_1}}</font> |
|
19109 * Blood 2 : <font color='red'>{{blood_2}}</font> |
|
19110 * <button type="button" data-ng-click="fight()">Fight</button> |
|
19111 * <button type="button" data-ng-click="stopFight()">StopFight</button> |
|
19112 * <button type="button" data-ng-click="resetFight()">resetFight</button> |
|
19113 * </div> |
|
19114 * </div> |
|
19115 * |
|
19116 * </file> |
|
19117 * </example> |
|
19118 */ |
|
19119 function interval(fn, delay, count, invokeApply) { |
|
19120 var setInterval = $window.setInterval, |
|
19121 clearInterval = $window.clearInterval, |
|
19122 iteration = 0, |
|
19123 skipApply = (isDefined(invokeApply) && !invokeApply), |
|
19124 deferred = (skipApply ? $$q : $q).defer(), |
|
19125 promise = deferred.promise; |
|
19126 |
|
19127 count = isDefined(count) ? count : 0; |
|
19128 |
|
19129 promise.then(null, null, fn); |
|
19130 |
|
19131 promise.$$intervalId = setInterval(function tick() { |
|
19132 deferred.notify(iteration++); |
|
19133 |
|
19134 if (count > 0 && iteration >= count) { |
|
19135 deferred.resolve(iteration); |
|
19136 clearInterval(promise.$$intervalId); |
|
19137 delete intervals[promise.$$intervalId]; |
|
19138 } |
|
19139 |
|
19140 if (!skipApply) $rootScope.$apply(); |
|
19141 |
|
19142 }, delay); |
|
19143 |
|
19144 intervals[promise.$$intervalId] = deferred; |
|
19145 |
|
19146 return promise; |
|
19147 } |
|
19148 |
|
19149 |
|
19150 /** |
|
19151 * @ngdoc method |
|
19152 * @name $interval#cancel |
|
19153 * |
|
19154 * @description |
|
19155 * Cancels a task associated with the `promise`. |
|
19156 * |
|
19157 * @param {promise} promise returned by the `$interval` function. |
|
19158 * @returns {boolean} Returns `true` if the task was successfully canceled. |
|
19159 */ |
|
19160 interval.cancel = function(promise) { |
|
19161 if (promise && promise.$$intervalId in intervals) { |
|
19162 intervals[promise.$$intervalId].reject('canceled'); |
|
19163 $window.clearInterval(promise.$$intervalId); |
|
19164 delete intervals[promise.$$intervalId]; |
|
19165 return true; |
|
19166 } |
|
19167 return false; |
|
19168 }; |
|
19169 |
|
19170 return interval; |
|
19171 }]; |
|
19172 } |
|
19173 |
|
19174 /** |
|
19175 * @ngdoc service |
|
19176 * @name $locale |
|
19177 * |
|
19178 * @description |
|
19179 * $locale service provides localization rules for various Angular components. As of right now the |
|
19180 * only public api is: |
|
19181 * |
|
19182 * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) |
|
19183 */ |
|
19184 function $LocaleProvider(){ |
|
19185 this.$get = function() { |
|
19186 return { |
|
19187 id: 'en-us', |
|
19188 |
|
19189 NUMBER_FORMATS: { |
|
19190 DECIMAL_SEP: '.', |
|
19191 GROUP_SEP: ',', |
|
19192 PATTERNS: [ |
|
19193 { // Decimal Pattern |
|
19194 minInt: 1, |
|
19195 minFrac: 0, |
|
19196 maxFrac: 3, |
|
19197 posPre: '', |
|
19198 posSuf: '', |
|
19199 negPre: '-', |
|
19200 negSuf: '', |
|
19201 gSize: 3, |
|
19202 lgSize: 3 |
|
19203 },{ //Currency Pattern |
|
19204 minInt: 1, |
|
19205 minFrac: 2, |
|
19206 maxFrac: 2, |
|
19207 posPre: '\u00A4', |
|
19208 posSuf: '', |
|
19209 negPre: '(\u00A4', |
|
19210 negSuf: ')', |
|
19211 gSize: 3, |
|
19212 lgSize: 3 |
|
19213 } |
|
19214 ], |
|
19215 CURRENCY_SYM: '$' |
|
19216 }, |
|
19217 |
|
19218 DATETIME_FORMATS: { |
|
19219 MONTH: |
|
19220 'January,February,March,April,May,June,July,August,September,October,November,December' |
|
19221 .split(','), |
|
19222 SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), |
|
19223 DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), |
|
19224 SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), |
|
19225 AMPMS: ['AM','PM'], |
|
19226 medium: 'MMM d, y h:mm:ss a', |
|
19227 short: 'M/d/yy h:mm a', |
|
19228 fullDate: 'EEEE, MMMM d, y', |
|
19229 longDate: 'MMMM d, y', |
|
19230 mediumDate: 'MMM d, y', |
|
19231 shortDate: 'M/d/yy', |
|
19232 mediumTime: 'h:mm:ss a', |
|
19233 shortTime: 'h:mm a' |
|
19234 }, |
|
19235 |
|
19236 pluralCat: function(num) { |
|
19237 if (num === 1) { |
|
19238 return 'one'; |
|
19239 } |
|
19240 return 'other'; |
|
19241 } |
|
19242 }; |
|
19243 }; |
|
19244 } |
|
19245 |
|
19246 var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, |
|
19247 DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; |
|
19248 var $locationMinErr = minErr('$location'); |
|
19249 |
|
19250 |
|
19251 /** |
|
19252 * Encode path using encodeUriSegment, ignoring forward slashes |
|
19253 * |
|
19254 * @param {string} path Path to encode |
|
19255 * @returns {string} |
|
19256 */ |
|
19257 function encodePath(path) { |
|
19258 var segments = path.split('/'), |
|
19259 i = segments.length; |
|
19260 |
|
19261 while (i--) { |
|
19262 segments[i] = encodeUriSegment(segments[i]); |
|
19263 } |
|
19264 |
|
19265 return segments.join('/'); |
|
19266 } |
|
19267 |
|
19268 function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) { |
|
19269 var parsedUrl = urlResolve(absoluteUrl, appBase); |
|
19270 |
|
19271 locationObj.$$protocol = parsedUrl.protocol; |
|
19272 locationObj.$$host = parsedUrl.hostname; |
|
19273 locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; |
|
19274 } |
|
19275 |
|
19276 |
|
19277 function parseAppUrl(relativeUrl, locationObj, appBase) { |
|
19278 var prefixed = (relativeUrl.charAt(0) !== '/'); |
|
19279 if (prefixed) { |
|
19280 relativeUrl = '/' + relativeUrl; |
|
19281 } |
|
19282 var match = urlResolve(relativeUrl, appBase); |
|
19283 locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? |
|
19284 match.pathname.substring(1) : match.pathname); |
|
19285 locationObj.$$search = parseKeyValue(match.search); |
|
19286 locationObj.$$hash = decodeURIComponent(match.hash); |
|
19287 |
|
19288 // make sure path starts with '/'; |
|
19289 if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { |
|
19290 locationObj.$$path = '/' + locationObj.$$path; |
|
19291 } |
|
19292 } |
|
19293 |
|
19294 |
|
19295 /** |
|
19296 * |
|
19297 * @param {string} begin |
|
19298 * @param {string} whole |
|
19299 * @returns {string} returns text from whole after begin or undefined if it does not begin with |
|
19300 * expected string. |
|
19301 */ |
|
19302 function beginsWith(begin, whole) { |
|
19303 if (whole.indexOf(begin) === 0) { |
|
19304 return whole.substr(begin.length); |
|
19305 } |
|
19306 } |
|
19307 |
|
19308 |
|
19309 function stripHash(url) { |
|
19310 var index = url.indexOf('#'); |
|
19311 return index == -1 ? url : url.substr(0, index); |
|
19312 } |
|
19313 |
|
19314 |
|
19315 function stripFile(url) { |
|
19316 return url.substr(0, stripHash(url).lastIndexOf('/') + 1); |
|
19317 } |
|
19318 |
|
19319 /* return the server only (scheme://host:port) */ |
|
19320 function serverBase(url) { |
|
19321 return url.substring(0, url.indexOf('/', url.indexOf('//') + 2)); |
|
19322 } |
|
19323 |
|
19324 |
|
19325 /** |
|
19326 * LocationHtml5Url represents an url |
|
19327 * This object is exposed as $location service when HTML5 mode is enabled and supported |
|
19328 * |
|
19329 * @constructor |
|
19330 * @param {string} appBase application base URL |
|
19331 * @param {string} basePrefix url path prefix |
|
19332 */ |
|
19333 function LocationHtml5Url(appBase, basePrefix) { |
|
19334 this.$$html5 = true; |
|
19335 basePrefix = basePrefix || ''; |
|
19336 var appBaseNoFile = stripFile(appBase); |
|
19337 parseAbsoluteUrl(appBase, this, appBase); |
|
19338 |
|
19339 |
|
19340 /** |
|
19341 * Parse given html5 (regular) url string into properties |
|
19342 * @param {string} newAbsoluteUrl HTML5 url |
|
19343 * @private |
|
19344 */ |
|
19345 this.$$parse = function(url) { |
|
19346 var pathUrl = beginsWith(appBaseNoFile, url); |
|
19347 if (!isString(pathUrl)) { |
|
19348 throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, |
|
19349 appBaseNoFile); |
|
19350 } |
|
19351 |
|
19352 parseAppUrl(pathUrl, this, appBase); |
|
19353 |
|
19354 if (!this.$$path) { |
|
19355 this.$$path = '/'; |
|
19356 } |
|
19357 |
|
19358 this.$$compose(); |
|
19359 }; |
|
19360 |
|
19361 /** |
|
19362 * Compose url and update `absUrl` property |
|
19363 * @private |
|
19364 */ |
|
19365 this.$$compose = function() { |
|
19366 var search = toKeyValue(this.$$search), |
|
19367 hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; |
|
19368 |
|
19369 this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; |
|
19370 this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' |
|
19371 }; |
|
19372 |
|
19373 this.$$parseLinkUrl = function(url, relHref) { |
|
19374 if (relHref && relHref[0] === '#') { |
|
19375 // special case for links to hash fragments: |
|
19376 // keep the old url and only replace the hash fragment |
|
19377 this.hash(relHref.slice(1)); |
|
19378 return true; |
|
19379 } |
|
19380 var appUrl, prevAppUrl; |
|
19381 var rewrittenUrl; |
|
19382 |
|
19383 if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { |
|
19384 prevAppUrl = appUrl; |
|
19385 if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { |
|
19386 rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); |
|
19387 } else { |
|
19388 rewrittenUrl = appBase + prevAppUrl; |
|
19389 } |
|
19390 } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { |
|
19391 rewrittenUrl = appBaseNoFile + appUrl; |
|
19392 } else if (appBaseNoFile == url + '/') { |
|
19393 rewrittenUrl = appBaseNoFile; |
|
19394 } |
|
19395 if (rewrittenUrl) { |
|
19396 this.$$parse(rewrittenUrl); |
|
19397 } |
|
19398 return !!rewrittenUrl; |
|
19399 }; |
|
19400 } |
|
19401 |
|
19402 |
|
19403 /** |
|
19404 * LocationHashbangUrl represents url |
|
19405 * This object is exposed as $location service when developer doesn't opt into html5 mode. |
|
19406 * It also serves as the base class for html5 mode fallback on legacy browsers. |
|
19407 * |
|
19408 * @constructor |
|
19409 * @param {string} appBase application base URL |
|
19410 * @param {string} hashPrefix hashbang prefix |
|
19411 */ |
|
19412 function LocationHashbangUrl(appBase, hashPrefix) { |
|
19413 var appBaseNoFile = stripFile(appBase); |
|
19414 |
|
19415 parseAbsoluteUrl(appBase, this, appBase); |
|
19416 |
|
19417 |
|
19418 /** |
|
19419 * Parse given hashbang url into properties |
|
19420 * @param {string} url Hashbang url |
|
19421 * @private |
|
19422 */ |
|
19423 this.$$parse = function(url) { |
|
19424 var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); |
|
19425 var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' |
|
19426 ? beginsWith(hashPrefix, withoutBaseUrl) |
|
19427 : (this.$$html5) |
|
19428 ? withoutBaseUrl |
|
19429 : ''; |
|
19430 |
|
19431 if (!isString(withoutHashUrl)) { |
|
19432 throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, |
|
19433 hashPrefix); |
|
19434 } |
|
19435 parseAppUrl(withoutHashUrl, this, appBase); |
|
19436 |
|
19437 this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); |
|
19438 |
|
19439 this.$$compose(); |
|
19440 |
|
19441 /* |
|
19442 * In Windows, on an anchor node on documents loaded from |
|
19443 * the filesystem, the browser will return a pathname |
|
19444 * prefixed with the drive name ('/C:/path') when a |
|
19445 * pathname without a drive is set: |
|
19446 * * a.setAttribute('href', '/foo') |
|
19447 * * a.pathname === '/C:/foo' //true |
|
19448 * |
|
19449 * Inside of Angular, we're always using pathnames that |
|
19450 * do not include drive names for routing. |
|
19451 */ |
|
19452 function removeWindowsDriveName (path, url, base) { |
|
19453 /* |
|
19454 Matches paths for file protocol on windows, |
|
19455 such as /C:/foo/bar, and captures only /foo/bar. |
|
19456 */ |
|
19457 var windowsFilePathExp = /^\/[A-Z]:(\/.*)/; |
|
19458 |
|
19459 var firstPathSegmentMatch; |
|
19460 |
|
19461 //Get the relative path from the input URL. |
|
19462 if (url.indexOf(base) === 0) { |
|
19463 url = url.replace(base, ''); |
|
19464 } |
|
19465 |
|
19466 // The input URL intentionally contains a first path segment that ends with a colon. |
|
19467 if (windowsFilePathExp.exec(url)) { |
|
19468 return path; |
|
19469 } |
|
19470 |
|
19471 firstPathSegmentMatch = windowsFilePathExp.exec(path); |
|
19472 return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path; |
|
19473 } |
|
19474 }; |
|
19475 |
|
19476 /** |
|
19477 * Compose hashbang url and update `absUrl` property |
|
19478 * @private |
|
19479 */ |
|
19480 this.$$compose = function() { |
|
19481 var search = toKeyValue(this.$$search), |
|
19482 hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; |
|
19483 |
|
19484 this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; |
|
19485 this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); |
|
19486 }; |
|
19487 |
|
19488 this.$$parseLinkUrl = function(url, relHref) { |
|
19489 if(stripHash(appBase) == stripHash(url)) { |
|
19490 this.$$parse(url); |
|
19491 return true; |
|
19492 } |
|
19493 return false; |
|
19494 }; |
|
19495 } |
|
19496 |
|
19497 |
|
19498 /** |
|
19499 * LocationHashbangUrl represents url |
|
19500 * This object is exposed as $location service when html5 history api is enabled but the browser |
|
19501 * does not support it. |
|
19502 * |
|
19503 * @constructor |
|
19504 * @param {string} appBase application base URL |
|
19505 * @param {string} hashPrefix hashbang prefix |
|
19506 */ |
|
19507 function LocationHashbangInHtml5Url(appBase, hashPrefix) { |
|
19508 this.$$html5 = true; |
|
19509 LocationHashbangUrl.apply(this, arguments); |
|
19510 |
|
19511 var appBaseNoFile = stripFile(appBase); |
|
19512 |
|
19513 this.$$parseLinkUrl = function(url, relHref) { |
|
19514 if (relHref && relHref[0] === '#') { |
|
19515 // special case for links to hash fragments: |
|
19516 // keep the old url and only replace the hash fragment |
|
19517 this.hash(relHref.slice(1)); |
|
19518 return true; |
|
19519 } |
|
19520 |
|
19521 var rewrittenUrl; |
|
19522 var appUrl; |
|
19523 |
|
19524 if ( appBase == stripHash(url) ) { |
|
19525 rewrittenUrl = url; |
|
19526 } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { |
|
19527 rewrittenUrl = appBase + hashPrefix + appUrl; |
|
19528 } else if ( appBaseNoFile === url + '/') { |
|
19529 rewrittenUrl = appBaseNoFile; |
|
19530 } |
|
19531 if (rewrittenUrl) { |
|
19532 this.$$parse(rewrittenUrl); |
|
19533 } |
|
19534 return !!rewrittenUrl; |
|
19535 }; |
|
19536 |
|
19537 this.$$compose = function() { |
|
19538 var search = toKeyValue(this.$$search), |
|
19539 hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; |
|
19540 |
|
19541 this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; |
|
19542 // include hashPrefix in $$absUrl when $$url is empty so IE8 & 9 do not reload page because of removal of '#' |
|
19543 this.$$absUrl = appBase + hashPrefix + this.$$url; |
|
19544 }; |
|
19545 |
|
19546 } |
|
19547 |
|
19548 |
|
19549 var locationPrototype = { |
|
19550 |
|
19551 /** |
|
19552 * Are we in html5 mode? |
|
19553 * @private |
|
19554 */ |
|
19555 $$html5: false, |
|
19556 |
|
19557 /** |
|
19558 * Has any change been replacing? |
|
19559 * @private |
|
19560 */ |
|
19561 $$replace: false, |
|
19562 |
|
19563 /** |
|
19564 * @ngdoc method |
|
19565 * @name $location#absUrl |
|
19566 * |
|
19567 * @description |
|
19568 * This method is getter only. |
|
19569 * |
|
19570 * Return full url representation with all segments encoded according to rules specified in |
|
19571 * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). |
|
19572 * |
|
19573 * @return {string} full url |
|
19574 */ |
|
19575 absUrl: locationGetter('$$absUrl'), |
|
19576 |
|
19577 /** |
|
19578 * @ngdoc method |
|
19579 * @name $location#url |
|
19580 * |
|
19581 * @description |
|
19582 * This method is getter / setter. |
|
19583 * |
|
19584 * Return url (e.g. `/path?a=b#hash`) when called without any parameter. |
|
19585 * |
|
19586 * Change path, search and hash, when called with parameter and return `$location`. |
|
19587 * |
|
19588 * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) |
|
19589 * @return {string} url |
|
19590 */ |
|
19591 url: function(url) { |
|
19592 if (isUndefined(url)) |
|
19593 return this.$$url; |
|
19594 |
|
19595 var match = PATH_MATCH.exec(url); |
|
19596 if (match[1]) this.path(decodeURIComponent(match[1])); |
|
19597 if (match[2] || match[1]) this.search(match[3] || ''); |
|
19598 this.hash(match[5] || ''); |
|
19599 |
|
19600 return this; |
|
19601 }, |
|
19602 |
|
19603 /** |
|
19604 * @ngdoc method |
|
19605 * @name $location#protocol |
|
19606 * |
|
19607 * @description |
|
19608 * This method is getter only. |
|
19609 * |
|
19610 * Return protocol of current url. |
|
19611 * |
|
19612 * @return {string} protocol of current url |
|
19613 */ |
|
19614 protocol: locationGetter('$$protocol'), |
|
19615 |
|
19616 /** |
|
19617 * @ngdoc method |
|
19618 * @name $location#host |
|
19619 * |
|
19620 * @description |
|
19621 * This method is getter only. |
|
19622 * |
|
19623 * Return host of current url. |
|
19624 * |
|
19625 * @return {string} host of current url. |
|
19626 */ |
|
19627 host: locationGetter('$$host'), |
|
19628 |
|
19629 /** |
|
19630 * @ngdoc method |
|
19631 * @name $location#port |
|
19632 * |
|
19633 * @description |
|
19634 * This method is getter only. |
|
19635 * |
|
19636 * Return port of current url. |
|
19637 * |
|
19638 * @return {Number} port |
|
19639 */ |
|
19640 port: locationGetter('$$port'), |
|
19641 |
|
19642 /** |
|
19643 * @ngdoc method |
|
19644 * @name $location#path |
|
19645 * |
|
19646 * @description |
|
19647 * This method is getter / setter. |
|
19648 * |
|
19649 * Return path of current url when called without any parameter. |
|
19650 * |
|
19651 * Change path when called with parameter and return `$location`. |
|
19652 * |
|
19653 * Note: Path should always begin with forward slash (/), this method will add the forward slash |
|
19654 * if it is missing. |
|
19655 * |
|
19656 * @param {(string|number)=} path New path |
|
19657 * @return {string} path |
|
19658 */ |
|
19659 path: locationGetterSetter('$$path', function(path) { |
|
19660 path = path !== null ? path.toString() : ''; |
|
19661 return path.charAt(0) == '/' ? path : '/' + path; |
|
19662 }), |
|
19663 |
|
19664 /** |
|
19665 * @ngdoc method |
|
19666 * @name $location#search |
|
19667 * |
|
19668 * @description |
|
19669 * This method is getter / setter. |
|
19670 * |
|
19671 * Return search part (as object) of current url when called without any parameter. |
|
19672 * |
|
19673 * Change search part when called with parameter and return `$location`. |
|
19674 * |
|
19675 * |
|
19676 * ```js |
|
19677 * // given url http://example.com/#/some/path?foo=bar&baz=xoxo |
|
19678 * var searchObject = $location.search(); |
|
19679 * // => {foo: 'bar', baz: 'xoxo'} |
|
19680 * |
|
19681 * |
|
19682 * // set foo to 'yipee' |
|
19683 * $location.search('foo', 'yipee'); |
|
19684 * // => $location |
|
19685 * ``` |
|
19686 * |
|
19687 * @param {string|Object.<string>|Object.<Array.<string>>} search New search params - string or |
|
19688 * hash object. |
|
19689 * |
|
19690 * When called with a single argument the method acts as a setter, setting the `search` component |
|
19691 * of `$location` to the specified value. |
|
19692 * |
|
19693 * If the argument is a hash object containing an array of values, these values will be encoded |
|
19694 * as duplicate search parameters in the url. |
|
19695 * |
|
19696 * @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue` |
|
19697 * will override only a single search property. |
|
19698 * |
|
19699 * If `paramValue` is an array, it will override the property of the `search` component of |
|
19700 * `$location` specified via the first argument. |
|
19701 * |
|
19702 * If `paramValue` is `null`, the property specified via the first argument will be deleted. |
|
19703 * |
|
19704 * If `paramValue` is `true`, the property specified via the first argument will be added with no |
|
19705 * value nor trailing equal sign. |
|
19706 * |
|
19707 * @return {Object} If called with no arguments returns the parsed `search` object. If called with |
|
19708 * one or more arguments returns `$location` object itself. |
|
19709 */ |
|
19710 search: function(search, paramValue) { |
|
19711 switch (arguments.length) { |
|
19712 case 0: |
|
19713 return this.$$search; |
|
19714 case 1: |
|
19715 if (isString(search) || isNumber(search)) { |
|
19716 search = search.toString(); |
|
19717 this.$$search = parseKeyValue(search); |
|
19718 } else if (isObject(search)) { |
|
19719 // remove object undefined or null properties |
|
19720 forEach(search, function(value, key) { |
|
19721 if (value == null) delete search[key]; |
|
19722 }); |
|
19723 |
|
19724 this.$$search = search; |
|
19725 } else { |
|
19726 throw $locationMinErr('isrcharg', |
|
19727 'The first argument of the `$location#search()` call must be a string or an object.'); |
|
19728 } |
|
19729 break; |
|
19730 default: |
|
19731 if (isUndefined(paramValue) || paramValue === null) { |
|
19732 delete this.$$search[search]; |
|
19733 } else { |
|
19734 this.$$search[search] = paramValue; |
|
19735 } |
|
19736 } |
|
19737 |
|
19738 this.$$compose(); |
|
19739 return this; |
|
19740 }, |
|
19741 |
|
19742 /** |
|
19743 * @ngdoc method |
|
19744 * @name $location#hash |
|
19745 * |
|
19746 * @description |
|
19747 * This method is getter / setter. |
|
19748 * |
|
19749 * Return hash fragment when called without any parameter. |
|
19750 * |
|
19751 * Change hash fragment when called with parameter and return `$location`. |
|
19752 * |
|
19753 * @param {(string|number)=} hash New hash fragment |
|
19754 * @return {string} hash |
|
19755 */ |
|
19756 hash: locationGetterSetter('$$hash', function(hash) { |
|
19757 return hash !== null ? hash.toString() : ''; |
|
19758 }), |
|
19759 |
|
19760 /** |
|
19761 * @ngdoc method |
|
19762 * @name $location#replace |
|
19763 * |
|
19764 * @description |
|
19765 * If called, all changes to $location during current `$digest` will be replacing current history |
|
19766 * record, instead of adding new one. |
|
19767 */ |
|
19768 replace: function() { |
|
19769 this.$$replace = true; |
|
19770 return this; |
|
19771 } |
|
19772 }; |
|
19773 |
|
19774 forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function (Location) { |
|
19775 Location.prototype = Object.create(locationPrototype); |
|
19776 |
|
19777 /** |
|
19778 * @ngdoc method |
|
19779 * @name $location#state |
|
19780 * |
|
19781 * @description |
|
19782 * This method is getter / setter. |
|
19783 * |
|
19784 * Return the history state object when called without any parameter. |
|
19785 * |
|
19786 * Change the history state object when called with one parameter and return `$location`. |
|
19787 * The state object is later passed to `pushState` or `replaceState`. |
|
19788 * |
|
19789 * NOTE: This method is supported only in HTML5 mode and only in browsers supporting |
|
19790 * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support |
|
19791 * older browsers (like IE9 or Android < 4.0), don't use this method. |
|
19792 * |
|
19793 * @param {object=} state State object for pushState or replaceState |
|
19794 * @return {object} state |
|
19795 */ |
|
19796 Location.prototype.state = function(state) { |
|
19797 if (!arguments.length) |
|
19798 return this.$$state; |
|
19799 |
|
19800 if (Location !== LocationHtml5Url || !this.$$html5) { |
|
19801 throw $locationMinErr('nostate', 'History API state support is available only ' + |
|
19802 'in HTML5 mode and only in browsers supporting HTML5 History API'); |
|
19803 } |
|
19804 // The user might modify `stateObject` after invoking `$location.state(stateObject)` |
|
19805 // but we're changing the $$state reference to $browser.state() during the $digest |
|
19806 // so the modification window is narrow. |
|
19807 this.$$state = isUndefined(state) ? null : state; |
|
19808 |
|
19809 return this; |
|
19810 }; |
|
19811 }); |
|
19812 |
|
19813 |
|
19814 function locationGetter(property) { |
|
19815 return function() { |
|
19816 return this[property]; |
|
19817 }; |
|
19818 } |
|
19819 |
|
19820 |
|
19821 function locationGetterSetter(property, preprocess) { |
|
19822 return function(value) { |
|
19823 if (isUndefined(value)) |
|
19824 return this[property]; |
|
19825 |
|
19826 this[property] = preprocess(value); |
|
19827 this.$$compose(); |
|
19828 |
|
19829 return this; |
|
19830 }; |
|
19831 } |
|
19832 |
|
19833 |
|
19834 /** |
|
19835 * @ngdoc service |
|
19836 * @name $location |
|
19837 * |
|
19838 * @requires $rootElement |
|
19839 * |
|
19840 * @description |
|
19841 * The $location service parses the URL in the browser address bar (based on the |
|
19842 * [window.location](https://developer.mozilla.org/en/window.location)) and makes the URL |
|
19843 * available to your application. Changes to the URL in the address bar are reflected into |
|
19844 * $location service and changes to $location are reflected into the browser address bar. |
|
19845 * |
|
19846 * **The $location service:** |
|
19847 * |
|
19848 * - Exposes the current URL in the browser address bar, so you can |
|
19849 * - Watch and observe the URL. |
|
19850 * - Change the URL. |
|
19851 * - Synchronizes the URL with the browser when the user |
|
19852 * - Changes the address bar. |
|
19853 * - Clicks the back or forward button (or clicks a History link). |
|
19854 * - Clicks on a link. |
|
19855 * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). |
|
19856 * |
|
19857 * For more information see {@link guide/$location Developer Guide: Using $location} |
|
19858 */ |
|
19859 |
|
19860 /** |
|
19861 * @ngdoc provider |
|
19862 * @name $locationProvider |
|
19863 * @description |
|
19864 * Use the `$locationProvider` to configure how the application deep linking paths are stored. |
|
19865 */ |
|
19866 function $LocationProvider(){ |
|
19867 var hashPrefix = '', |
|
19868 html5Mode = { |
|
19869 enabled: false, |
|
19870 requireBase: true, |
|
19871 rewriteLinks: true |
|
19872 }; |
|
19873 |
|
19874 /** |
|
19875 * @ngdoc method |
|
19876 * @name $locationProvider#hashPrefix |
|
19877 * @description |
|
19878 * @param {string=} prefix Prefix for hash part (containing path and search) |
|
19879 * @returns {*} current value if used as getter or itself (chaining) if used as setter |
|
19880 */ |
|
19881 this.hashPrefix = function(prefix) { |
|
19882 if (isDefined(prefix)) { |
|
19883 hashPrefix = prefix; |
|
19884 return this; |
|
19885 } else { |
|
19886 return hashPrefix; |
|
19887 } |
|
19888 }; |
|
19889 |
|
19890 /** |
|
19891 * @ngdoc method |
|
19892 * @name $locationProvider#html5Mode |
|
19893 * @description |
|
19894 * @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value. |
|
19895 * If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported |
|
19896 * properties: |
|
19897 * - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to |
|
19898 * change urls where supported. Will fall back to hash-prefixed paths in browsers that do not |
|
19899 * support `pushState`. |
|
19900 * - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies |
|
19901 * whether or not a <base> tag is required to be present. If `enabled` and `requireBase` are |
|
19902 * true, and a base tag is not present, an error will be thrown when `$location` is injected. |
|
19903 * See the {@link guide/$location $location guide for more information} |
|
19904 * - **rewriteLinks** - `{boolean}` - (default: `false`) When html5Mode is enabled, disables |
|
19905 * url rewriting for relative linksTurns off url rewriting for relative links. |
|
19906 * |
|
19907 * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter |
|
19908 */ |
|
19909 this.html5Mode = function(mode) { |
|
19910 if (isBoolean(mode)) { |
|
19911 html5Mode.enabled = mode; |
|
19912 return this; |
|
19913 } else if (isObject(mode)) { |
|
19914 |
|
19915 if (isBoolean(mode.enabled)) { |
|
19916 html5Mode.enabled = mode.enabled; |
|
19917 } |
|
19918 |
|
19919 if (isBoolean(mode.requireBase)) { |
|
19920 html5Mode.requireBase = mode.requireBase; |
|
19921 } |
|
19922 |
|
19923 if (isBoolean(mode.rewriteLinks)) { |
|
19924 html5Mode.rewriteLinks = mode.rewriteLinks; |
|
19925 } |
|
19926 |
|
19927 return this; |
|
19928 } else { |
|
19929 return html5Mode; |
|
19930 } |
|
19931 }; |
|
19932 |
|
19933 /** |
|
19934 * @ngdoc event |
|
19935 * @name $location#$locationChangeStart |
|
19936 * @eventType broadcast on root scope |
|
19937 * @description |
|
19938 * Broadcasted before a URL will change. |
|
19939 * |
|
19940 * This change can be prevented by calling |
|
19941 * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more |
|
19942 * details about event object. Upon successful change |
|
19943 * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired. |
|
19944 * |
|
19945 * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when |
|
19946 * the browser supports the HTML5 History API. |
|
19947 * |
|
19948 * @param {Object} angularEvent Synthetic event object. |
|
19949 * @param {string} newUrl New URL |
|
19950 * @param {string=} oldUrl URL that was before it was changed. |
|
19951 * @param {string=} newState New history state object |
|
19952 * @param {string=} oldState History state object that was before it was changed. |
|
19953 */ |
|
19954 |
|
19955 /** |
|
19956 * @ngdoc event |
|
19957 * @name $location#$locationChangeSuccess |
|
19958 * @eventType broadcast on root scope |
|
19959 * @description |
|
19960 * Broadcasted after a URL was changed. |
|
19961 * |
|
19962 * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when |
|
19963 * the browser supports the HTML5 History API. |
|
19964 * |
|
19965 * @param {Object} angularEvent Synthetic event object. |
|
19966 * @param {string} newUrl New URL |
|
19967 * @param {string=} oldUrl URL that was before it was changed. |
|
19968 * @param {string=} newState New history state object |
|
19969 * @param {string=} oldState History state object that was before it was changed. |
|
19970 */ |
|
19971 |
|
19972 this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', |
|
19973 function( $rootScope, $browser, $sniffer, $rootElement) { |
|
19974 var $location, |
|
19975 LocationMode, |
|
19976 baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' |
|
19977 initialUrl = $browser.url(), |
|
19978 appBase; |
|
19979 |
|
19980 if (html5Mode.enabled) { |
|
19981 if (!baseHref && html5Mode.requireBase) { |
|
19982 throw $locationMinErr('nobase', |
|
19983 "$location in HTML5 mode requires a <base> tag to be present!"); |
|
19984 } |
|
19985 appBase = serverBase(initialUrl) + (baseHref || '/'); |
|
19986 LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; |
|
19987 } else { |
|
19988 appBase = stripHash(initialUrl); |
|
19989 LocationMode = LocationHashbangUrl; |
|
19990 } |
|
19991 $location = new LocationMode(appBase, '#' + hashPrefix); |
|
19992 $location.$$parseLinkUrl(initialUrl, initialUrl); |
|
19993 |
|
19994 $location.$$state = $browser.state(); |
|
19995 |
|
19996 var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; |
|
19997 |
|
19998 function setBrowserUrlWithFallback(url, replace, state) { |
|
19999 var oldUrl = $location.url(); |
|
20000 var oldState = $location.$$state; |
|
20001 try { |
|
20002 $browser.url(url, replace, state); |
|
20003 |
|
20004 // Make sure $location.state() returns referentially identical (not just deeply equal) |
|
20005 // state object; this makes possible quick checking if the state changed in the digest |
|
20006 // loop. Checking deep equality would be too expensive. |
|
20007 $location.$$state = $browser.state(); |
|
20008 } catch (e) { |
|
20009 // Restore old values if pushState fails |
|
20010 $location.url(oldUrl); |
|
20011 $location.$$state = oldState; |
|
20012 |
|
20013 throw e; |
|
20014 } |
|
20015 } |
|
20016 |
|
20017 $rootElement.on('click', function(event) { |
|
20018 // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) |
|
20019 // currently we open nice url link and redirect then |
|
20020 |
|
20021 if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.which == 2) return; |
|
20022 |
|
20023 var elm = jqLite(event.target); |
|
20024 |
|
20025 // traverse the DOM up to find first A tag |
|
20026 while (nodeName_(elm[0]) !== 'a') { |
|
20027 // ignore rewriting if no A tag (reached root element, or no parent - removed from document) |
|
20028 if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; |
|
20029 } |
|
20030 |
|
20031 var absHref = elm.prop('href'); |
|
20032 // get the actual href attribute - see |
|
20033 // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx |
|
20034 var relHref = elm.attr('href') || elm.attr('xlink:href'); |
|
20035 |
|
20036 if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { |
|
20037 // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during |
|
20038 // an animation. |
|
20039 absHref = urlResolve(absHref.animVal).href; |
|
20040 } |
|
20041 |
|
20042 // Ignore when url is started with javascript: or mailto: |
|
20043 if (IGNORE_URI_REGEXP.test(absHref)) return; |
|
20044 |
|
20045 if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) { |
|
20046 if ($location.$$parseLinkUrl(absHref, relHref)) { |
|
20047 // We do a preventDefault for all urls that are part of the angular application, |
|
20048 // in html5mode and also without, so that we are able to abort navigation without |
|
20049 // getting double entries in the location history. |
|
20050 event.preventDefault(); |
|
20051 // update location manually |
|
20052 if ($location.absUrl() != $browser.url()) { |
|
20053 $rootScope.$apply(); |
|
20054 // hack to work around FF6 bug 684208 when scenario runner clicks on links |
|
20055 window.angular['ff-684208-preventDefault'] = true; |
|
20056 } |
|
20057 } |
|
20058 } |
|
20059 }); |
|
20060 |
|
20061 |
|
20062 // rewrite hashbang url <> html5 url |
|
20063 if ($location.absUrl() != initialUrl) { |
|
20064 $browser.url($location.absUrl(), true); |
|
20065 } |
|
20066 |
|
20067 var initializing = true; |
|
20068 |
|
20069 // update $location when $browser url changes |
|
20070 $browser.onUrlChange(function(newUrl, newState) { |
|
20071 $rootScope.$evalAsync(function() { |
|
20072 var oldUrl = $location.absUrl(); |
|
20073 var oldState = $location.$$state; |
|
20074 |
|
20075 $location.$$parse(newUrl); |
|
20076 $location.$$state = newState; |
|
20077 if ($rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, |
|
20078 newState, oldState).defaultPrevented) { |
|
20079 $location.$$parse(oldUrl); |
|
20080 $location.$$state = oldState; |
|
20081 setBrowserUrlWithFallback(oldUrl, false, oldState); |
|
20082 } else { |
|
20083 initializing = false; |
|
20084 afterLocationChange(oldUrl, oldState); |
|
20085 } |
|
20086 }); |
|
20087 if (!$rootScope.$$phase) $rootScope.$digest(); |
|
20088 }); |
|
20089 |
|
20090 // update browser |
|
20091 $rootScope.$watch(function $locationWatch() { |
|
20092 var oldUrl = $browser.url(); |
|
20093 var oldState = $browser.state(); |
|
20094 var currentReplace = $location.$$replace; |
|
20095 |
|
20096 if (initializing || oldUrl !== $location.absUrl() || |
|
20097 ($location.$$html5 && $sniffer.history && oldState !== $location.$$state)) { |
|
20098 initializing = false; |
|
20099 |
|
20100 $rootScope.$evalAsync(function() { |
|
20101 if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl, |
|
20102 $location.$$state, oldState).defaultPrevented) { |
|
20103 $location.$$parse(oldUrl); |
|
20104 $location.$$state = oldState; |
|
20105 } else { |
|
20106 setBrowserUrlWithFallback($location.absUrl(), currentReplace, |
|
20107 oldState === $location.$$state ? null : $location.$$state); |
|
20108 afterLocationChange(oldUrl, oldState); |
|
20109 } |
|
20110 }); |
|
20111 } |
|
20112 |
|
20113 $location.$$replace = false; |
|
20114 |
|
20115 // we don't need to return anything because $evalAsync will make the digest loop dirty when |
|
20116 // there is a change |
|
20117 }); |
|
20118 |
|
20119 return $location; |
|
20120 |
|
20121 function afterLocationChange(oldUrl, oldState) { |
|
20122 $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, |
|
20123 $location.$$state, oldState); |
|
20124 } |
|
20125 }]; |
|
20126 } |
|
20127 |
|
20128 /** |
|
20129 * @ngdoc service |
|
20130 * @name $log |
|
20131 * @requires $window |
|
20132 * |
|
20133 * @description |
|
20134 * Simple service for logging. Default implementation safely writes the message |
|
20135 * into the browser's console (if present). |
|
20136 * |
|
20137 * The main purpose of this service is to simplify debugging and troubleshooting. |
|
20138 * |
|
20139 * The default is to log `debug` messages. You can use |
|
20140 * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. |
|
20141 * |
|
20142 * @example |
|
20143 <example module="logExample"> |
|
20144 <file name="script.js"> |
|
20145 angular.module('logExample', []) |
|
20146 .controller('LogController', ['$scope', '$log', function($scope, $log) { |
|
20147 $scope.$log = $log; |
|
20148 $scope.message = 'Hello World!'; |
|
20149 }]); |
|
20150 </file> |
|
20151 <file name="index.html"> |
|
20152 <div ng-controller="LogController"> |
|
20153 <p>Reload this page with open console, enter text and hit the log button...</p> |
|
20154 Message: |
|
20155 <input type="text" ng-model="message"/> |
|
20156 <button ng-click="$log.log(message)">log</button> |
|
20157 <button ng-click="$log.warn(message)">warn</button> |
|
20158 <button ng-click="$log.info(message)">info</button> |
|
20159 <button ng-click="$log.error(message)">error</button> |
|
20160 </div> |
|
20161 </file> |
|
20162 </example> |
|
20163 */ |
|
20164 |
|
20165 /** |
|
20166 * @ngdoc provider |
|
20167 * @name $logProvider |
|
20168 * @description |
|
20169 * Use the `$logProvider` to configure how the application logs messages |
|
20170 */ |
|
20171 function $LogProvider(){ |
|
20172 var debug = true, |
|
20173 self = this; |
|
20174 |
|
20175 /** |
|
20176 * @ngdoc method |
|
20177 * @name $logProvider#debugEnabled |
|
20178 * @description |
|
20179 * @param {boolean=} flag enable or disable debug level messages |
|
20180 * @returns {*} current value if used as getter or itself (chaining) if used as setter |
|
20181 */ |
|
20182 this.debugEnabled = function(flag) { |
|
20183 if (isDefined(flag)) { |
|
20184 debug = flag; |
|
20185 return this; |
|
20186 } else { |
|
20187 return debug; |
|
20188 } |
|
20189 }; |
|
20190 |
|
20191 this.$get = ['$window', function($window){ |
|
20192 return { |
|
20193 /** |
|
20194 * @ngdoc method |
|
20195 * @name $log#log |
|
20196 * |
|
20197 * @description |
|
20198 * Write a log message |
|
20199 */ |
|
20200 log: consoleLog('log'), |
|
20201 |
|
20202 /** |
|
20203 * @ngdoc method |
|
20204 * @name $log#info |
|
20205 * |
|
20206 * @description |
|
20207 * Write an information message |
|
20208 */ |
|
20209 info: consoleLog('info'), |
|
20210 |
|
20211 /** |
|
20212 * @ngdoc method |
|
20213 * @name $log#warn |
|
20214 * |
|
20215 * @description |
|
20216 * Write a warning message |
|
20217 */ |
|
20218 warn: consoleLog('warn'), |
|
20219 |
|
20220 /** |
|
20221 * @ngdoc method |
|
20222 * @name $log#error |
|
20223 * |
|
20224 * @description |
|
20225 * Write an error message |
|
20226 */ |
|
20227 error: consoleLog('error'), |
|
20228 |
|
20229 /** |
|
20230 * @ngdoc method |
|
20231 * @name $log#debug |
|
20232 * |
|
20233 * @description |
|
20234 * Write a debug message |
|
20235 */ |
|
20236 debug: (function () { |
|
20237 var fn = consoleLog('debug'); |
|
20238 |
|
20239 return function() { |
|
20240 if (debug) { |
|
20241 fn.apply(self, arguments); |
|
20242 } |
|
20243 }; |
|
20244 }()) |
|
20245 }; |
|
20246 |
|
20247 function formatError(arg) { |
|
20248 if (arg instanceof Error) { |
|
20249 if (arg.stack) { |
|
20250 arg = (arg.message && arg.stack.indexOf(arg.message) === -1) |
|
20251 ? 'Error: ' + arg.message + '\n' + arg.stack |
|
20252 : arg.stack; |
|
20253 } else if (arg.sourceURL) { |
|
20254 arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line; |
|
20255 } |
|
20256 } |
|
20257 return arg; |
|
20258 } |
|
20259 |
|
20260 function consoleLog(type) { |
|
20261 var console = $window.console || {}, |
|
20262 logFn = console[type] || console.log || noop, |
|
20263 hasApply = false; |
|
20264 |
|
20265 // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. |
|
20266 // The reason behind this is that console.log has type "object" in IE8... |
|
20267 try { |
|
20268 hasApply = !!logFn.apply; |
|
20269 } catch (e) {} |
|
20270 |
|
20271 if (hasApply) { |
|
20272 return function() { |
|
20273 var args = []; |
|
20274 forEach(arguments, function(arg) { |
|
20275 args.push(formatError(arg)); |
|
20276 }); |
|
20277 return logFn.apply(console, args); |
|
20278 }; |
|
20279 } |
|
20280 |
|
20281 // we are IE which either doesn't have window.console => this is noop and we do nothing, |
|
20282 // or we are IE where console.log doesn't have apply so we log at least first 2 args |
|
20283 return function(arg1, arg2) { |
|
20284 logFn(arg1, arg2 == null ? '' : arg2); |
|
20285 }; |
|
20286 } |
|
20287 }]; |
|
20288 } |
|
20289 |
|
20290 var $parseMinErr = minErr('$parse'); |
|
20291 |
|
20292 // Sandboxing Angular Expressions |
|
20293 // ------------------------------ |
|
20294 // Angular expressions are generally considered safe because these expressions only have direct |
|
20295 // access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by |
|
20296 // obtaining a reference to native JS functions such as the Function constructor. |
|
20297 // |
|
20298 // As an example, consider the following Angular expression: |
|
20299 // |
|
20300 // {}.toString.constructor('alert("evil JS code")') |
|
20301 // |
|
20302 // This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits |
|
20303 // against the expression language, but not to prevent exploits that were enabled by exposing |
|
20304 // sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good |
|
20305 // practice and therefore we are not even trying to protect against interaction with an object |
|
20306 // explicitly exposed in this way. |
|
20307 // |
|
20308 // In general, it is not possible to access a Window object from an angular expression unless a |
|
20309 // window or some DOM object that has a reference to window is published onto a Scope. |
|
20310 // Similarly we prevent invocations of function known to be dangerous, as well as assignments to |
|
20311 // native objects. |
|
20312 |
|
20313 |
|
20314 function ensureSafeMemberName(name, fullExpression) { |
|
20315 if (name === "__defineGetter__" || name === "__defineSetter__" |
|
20316 || name === "__lookupGetter__" || name === "__lookupSetter__" |
|
20317 || name === "__proto__") { |
|
20318 throw $parseMinErr('isecfld', |
|
20319 'Attempting to access a disallowed field in Angular expressions! ' |
|
20320 +'Expression: {0}', fullExpression); |
|
20321 } |
|
20322 return name; |
|
20323 } |
|
20324 |
|
20325 function ensureSafeObject(obj, fullExpression) { |
|
20326 // nifty check if obj is Function that is fast and works across iframes and other contexts |
|
20327 if (obj) { |
|
20328 if (obj.constructor === obj) { |
|
20329 throw $parseMinErr('isecfn', |
|
20330 'Referencing Function in Angular expressions is disallowed! Expression: {0}', |
|
20331 fullExpression); |
|
20332 } else if (// isWindow(obj) |
|
20333 obj.window === obj) { |
|
20334 throw $parseMinErr('isecwindow', |
|
20335 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', |
|
20336 fullExpression); |
|
20337 } else if (// isElement(obj) |
|
20338 obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { |
|
20339 throw $parseMinErr('isecdom', |
|
20340 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', |
|
20341 fullExpression); |
|
20342 } else if (// block Object so that we can't get hold of dangerous Object.* methods |
|
20343 obj === Object) { |
|
20344 throw $parseMinErr('isecobj', |
|
20345 'Referencing Object in Angular expressions is disallowed! Expression: {0}', |
|
20346 fullExpression); |
|
20347 } |
|
20348 } |
|
20349 return obj; |
|
20350 } |
|
20351 |
|
20352 var CALL = Function.prototype.call; |
|
20353 var APPLY = Function.prototype.apply; |
|
20354 var BIND = Function.prototype.bind; |
|
20355 |
|
20356 function ensureSafeFunction(obj, fullExpression) { |
|
20357 if (obj) { |
|
20358 if (obj.constructor === obj) { |
|
20359 throw $parseMinErr('isecfn', |
|
20360 'Referencing Function in Angular expressions is disallowed! Expression: {0}', |
|
20361 fullExpression); |
|
20362 } else if (obj === CALL || obj === APPLY || obj === BIND) { |
|
20363 throw $parseMinErr('isecff', |
|
20364 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', |
|
20365 fullExpression); |
|
20366 } |
|
20367 } |
|
20368 } |
|
20369 |
|
20370 //Keyword constants |
|
20371 var CONSTANTS = createMap(); |
|
20372 forEach({ |
|
20373 'null': function() { return null; }, |
|
20374 'true': function() { return true; }, |
|
20375 'false': function() { return false; }, |
|
20376 'undefined': function() {} |
|
20377 }, function(constantGetter, name) { |
|
20378 constantGetter.constant = constantGetter.literal = constantGetter.sharedGetter = true; |
|
20379 CONSTANTS[name] = constantGetter; |
|
20380 }); |
|
20381 |
|
20382 //Not quite a constant, but can be lex/parsed the same |
|
20383 CONSTANTS['this'] = function(self) { return self; }; |
|
20384 CONSTANTS['this'].sharedGetter = true; |
|
20385 |
|
20386 |
|
20387 //Operators - will be wrapped by binaryFn/unaryFn/assignment/filter |
|
20388 var OPERATORS = extend(createMap(), { |
|
20389 /* jshint bitwise : false */ |
|
20390 '+':function(self, locals, a,b){ |
|
20391 a=a(self, locals); b=b(self, locals); |
|
20392 if (isDefined(a)) { |
|
20393 if (isDefined(b)) { |
|
20394 return a + b; |
|
20395 } |
|
20396 return a; |
|
20397 } |
|
20398 return isDefined(b)?b:undefined;}, |
|
20399 '-':function(self, locals, a,b){ |
|
20400 a=a(self, locals); b=b(self, locals); |
|
20401 return (isDefined(a)?a:0)-(isDefined(b)?b:0); |
|
20402 }, |
|
20403 '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);}, |
|
20404 '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);}, |
|
20405 '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);}, |
|
20406 '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);}, |
|
20407 '===':function(self, locals, a, b){return a(self, locals)===b(self, locals);}, |
|
20408 '!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);}, |
|
20409 '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);}, |
|
20410 '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);}, |
|
20411 '<':function(self, locals, a,b){return a(self, locals)<b(self, locals);}, |
|
20412 '>':function(self, locals, a,b){return a(self, locals)>b(self, locals);}, |
|
20413 '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);}, |
|
20414 '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);}, |
|
20415 '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);}, |
|
20416 '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);}, |
|
20417 '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);}, |
|
20418 '!':function(self, locals, a){return !a(self, locals);}, |
|
20419 |
|
20420 //Tokenized as operators but parsed as assignment/filters |
|
20421 '=':true, |
|
20422 '|':true |
|
20423 }); |
|
20424 /* jshint bitwise: true */ |
|
20425 var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; |
|
20426 |
|
20427 |
|
20428 ///////////////////////////////////////// |
|
20429 |
|
20430 |
|
20431 /** |
|
20432 * @constructor |
|
20433 */ |
|
20434 var Lexer = function (options) { |
|
20435 this.options = options; |
|
20436 }; |
|
20437 |
|
20438 Lexer.prototype = { |
|
20439 constructor: Lexer, |
|
20440 |
|
20441 lex: function (text) { |
|
20442 this.text = text; |
|
20443 this.index = 0; |
|
20444 this.ch = undefined; |
|
20445 this.tokens = []; |
|
20446 |
|
20447 while (this.index < this.text.length) { |
|
20448 this.ch = this.text.charAt(this.index); |
|
20449 if (this.is('"\'')) { |
|
20450 this.readString(this.ch); |
|
20451 } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) { |
|
20452 this.readNumber(); |
|
20453 } else if (this.isIdent(this.ch)) { |
|
20454 this.readIdent(); |
|
20455 } else if (this.is('(){}[].,;:?')) { |
|
20456 this.tokens.push({ |
|
20457 index: this.index, |
|
20458 text: this.ch |
|
20459 }); |
|
20460 this.index++; |
|
20461 } else if (this.isWhitespace(this.ch)) { |
|
20462 this.index++; |
|
20463 } else { |
|
20464 var ch2 = this.ch + this.peek(); |
|
20465 var ch3 = ch2 + this.peek(2); |
|
20466 var fn = OPERATORS[this.ch]; |
|
20467 var fn2 = OPERATORS[ch2]; |
|
20468 var fn3 = OPERATORS[ch3]; |
|
20469 if (fn3) { |
|
20470 this.tokens.push({index: this.index, text: ch3, fn: fn3}); |
|
20471 this.index += 3; |
|
20472 } else if (fn2) { |
|
20473 this.tokens.push({index: this.index, text: ch2, fn: fn2}); |
|
20474 this.index += 2; |
|
20475 } else if (fn) { |
|
20476 this.tokens.push({ |
|
20477 index: this.index, |
|
20478 text: this.ch, |
|
20479 fn: fn |
|
20480 }); |
|
20481 this.index += 1; |
|
20482 } else { |
|
20483 this.throwError('Unexpected next character ', this.index, this.index + 1); |
|
20484 } |
|
20485 } |
|
20486 } |
|
20487 return this.tokens; |
|
20488 }, |
|
20489 |
|
20490 is: function(chars) { |
|
20491 return chars.indexOf(this.ch) !== -1; |
|
20492 }, |
|
20493 |
|
20494 peek: function(i) { |
|
20495 var num = i || 1; |
|
20496 return (this.index + num < this.text.length) ? this.text.charAt(this.index + num) : false; |
|
20497 }, |
|
20498 |
|
20499 isNumber: function(ch) { |
|
20500 return ('0' <= ch && ch <= '9'); |
|
20501 }, |
|
20502 |
|
20503 isWhitespace: function(ch) { |
|
20504 // IE treats non-breaking space as \u00A0 |
|
20505 return (ch === ' ' || ch === '\r' || ch === '\t' || |
|
20506 ch === '\n' || ch === '\v' || ch === '\u00A0'); |
|
20507 }, |
|
20508 |
|
20509 isIdent: function(ch) { |
|
20510 return ('a' <= ch && ch <= 'z' || |
|
20511 'A' <= ch && ch <= 'Z' || |
|
20512 '_' === ch || ch === '$'); |
|
20513 }, |
|
20514 |
|
20515 isExpOperator: function(ch) { |
|
20516 return (ch === '-' || ch === '+' || this.isNumber(ch)); |
|
20517 }, |
|
20518 |
|
20519 throwError: function(error, start, end) { |
|
20520 end = end || this.index; |
|
20521 var colStr = (isDefined(start) |
|
20522 ? 's ' + start + '-' + this.index + ' [' + this.text.substring(start, end) + ']' |
|
20523 : ' ' + end); |
|
20524 throw $parseMinErr('lexerr', 'Lexer Error: {0} at column{1} in expression [{2}].', |
|
20525 error, colStr, this.text); |
|
20526 }, |
|
20527 |
|
20528 readNumber: function() { |
|
20529 var number = ''; |
|
20530 var start = this.index; |
|
20531 while (this.index < this.text.length) { |
|
20532 var ch = lowercase(this.text.charAt(this.index)); |
|
20533 if (ch == '.' || this.isNumber(ch)) { |
|
20534 number += ch; |
|
20535 } else { |
|
20536 var peekCh = this.peek(); |
|
20537 if (ch == 'e' && this.isExpOperator(peekCh)) { |
|
20538 number += ch; |
|
20539 } else if (this.isExpOperator(ch) && |
|
20540 peekCh && this.isNumber(peekCh) && |
|
20541 number.charAt(number.length - 1) == 'e') { |
|
20542 number += ch; |
|
20543 } else if (this.isExpOperator(ch) && |
|
20544 (!peekCh || !this.isNumber(peekCh)) && |
|
20545 number.charAt(number.length - 1) == 'e') { |
|
20546 this.throwError('Invalid exponent'); |
|
20547 } else { |
|
20548 break; |
|
20549 } |
|
20550 } |
|
20551 this.index++; |
|
20552 } |
|
20553 number = 1 * number; |
|
20554 this.tokens.push({ |
|
20555 index: start, |
|
20556 text: number, |
|
20557 constant: true, |
|
20558 fn: function() { return number; } |
|
20559 }); |
|
20560 }, |
|
20561 |
|
20562 readIdent: function() { |
|
20563 var expression = this.text; |
|
20564 |
|
20565 var ident = ''; |
|
20566 var start = this.index; |
|
20567 |
|
20568 var lastDot, peekIndex, methodName, ch; |
|
20569 |
|
20570 while (this.index < this.text.length) { |
|
20571 ch = this.text.charAt(this.index); |
|
20572 if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) { |
|
20573 if (ch === '.') lastDot = this.index; |
|
20574 ident += ch; |
|
20575 } else { |
|
20576 break; |
|
20577 } |
|
20578 this.index++; |
|
20579 } |
|
20580 |
|
20581 //check if the identifier ends with . and if so move back one char |
|
20582 if (lastDot && ident[ident.length - 1] === '.') { |
|
20583 this.index--; |
|
20584 ident = ident.slice(0, -1); |
|
20585 lastDot = ident.lastIndexOf('.'); |
|
20586 if (lastDot === -1) { |
|
20587 lastDot = undefined; |
|
20588 } |
|
20589 } |
|
20590 |
|
20591 //check if this is not a method invocation and if it is back out to last dot |
|
20592 if (lastDot) { |
|
20593 peekIndex = this.index; |
|
20594 while (peekIndex < this.text.length) { |
|
20595 ch = this.text.charAt(peekIndex); |
|
20596 if (ch === '(') { |
|
20597 methodName = ident.substr(lastDot - start + 1); |
|
20598 ident = ident.substr(0, lastDot - start); |
|
20599 this.index = peekIndex; |
|
20600 break; |
|
20601 } |
|
20602 if (this.isWhitespace(ch)) { |
|
20603 peekIndex++; |
|
20604 } else { |
|
20605 break; |
|
20606 } |
|
20607 } |
|
20608 } |
|
20609 |
|
20610 this.tokens.push({ |
|
20611 index: start, |
|
20612 text: ident, |
|
20613 fn: CONSTANTS[ident] || getterFn(ident, this.options, expression) |
|
20614 }); |
|
20615 |
|
20616 if (methodName) { |
|
20617 this.tokens.push({ |
|
20618 index: lastDot, |
|
20619 text: '.' |
|
20620 }); |
|
20621 this.tokens.push({ |
|
20622 index: lastDot + 1, |
|
20623 text: methodName |
|
20624 }); |
|
20625 } |
|
20626 }, |
|
20627 |
|
20628 readString: function(quote) { |
|
20629 var start = this.index; |
|
20630 this.index++; |
|
20631 var string = ''; |
|
20632 var rawString = quote; |
|
20633 var escape = false; |
|
20634 while (this.index < this.text.length) { |
|
20635 var ch = this.text.charAt(this.index); |
|
20636 rawString += ch; |
|
20637 if (escape) { |
|
20638 if (ch === 'u') { |
|
20639 var hex = this.text.substring(this.index + 1, this.index + 5); |
|
20640 if (!hex.match(/[\da-f]{4}/i)) |
|
20641 this.throwError('Invalid unicode escape [\\u' + hex + ']'); |
|
20642 this.index += 4; |
|
20643 string += String.fromCharCode(parseInt(hex, 16)); |
|
20644 } else { |
|
20645 var rep = ESCAPE[ch]; |
|
20646 string = string + (rep || ch); |
|
20647 } |
|
20648 escape = false; |
|
20649 } else if (ch === '\\') { |
|
20650 escape = true; |
|
20651 } else if (ch === quote) { |
|
20652 this.index++; |
|
20653 this.tokens.push({ |
|
20654 index: start, |
|
20655 text: rawString, |
|
20656 string: string, |
|
20657 constant: true, |
|
20658 fn: function() { return string; } |
|
20659 }); |
|
20660 return; |
|
20661 } else { |
|
20662 string += ch; |
|
20663 } |
|
20664 this.index++; |
|
20665 } |
|
20666 this.throwError('Unterminated quote', start); |
|
20667 } |
|
20668 }; |
|
20669 |
|
20670 |
|
20671 function isConstant(exp) { |
|
20672 return exp.constant; |
|
20673 } |
|
20674 |
|
20675 /** |
|
20676 * @constructor |
|
20677 */ |
|
20678 var Parser = function (lexer, $filter, options) { |
|
20679 this.lexer = lexer; |
|
20680 this.$filter = $filter; |
|
20681 this.options = options; |
|
20682 }; |
|
20683 |
|
20684 Parser.ZERO = extend(function () { |
|
20685 return 0; |
|
20686 }, { |
|
20687 sharedGetter: true, |
|
20688 constant: true |
|
20689 }); |
|
20690 |
|
20691 Parser.prototype = { |
|
20692 constructor: Parser, |
|
20693 |
|
20694 parse: function (text) { |
|
20695 this.text = text; |
|
20696 this.tokens = this.lexer.lex(text); |
|
20697 |
|
20698 var value = this.statements(); |
|
20699 |
|
20700 if (this.tokens.length !== 0) { |
|
20701 this.throwError('is an unexpected token', this.tokens[0]); |
|
20702 } |
|
20703 |
|
20704 value.literal = !!value.literal; |
|
20705 value.constant = !!value.constant; |
|
20706 |
|
20707 return value; |
|
20708 }, |
|
20709 |
|
20710 primary: function () { |
|
20711 var primary; |
|
20712 if (this.expect('(')) { |
|
20713 primary = this.filterChain(); |
|
20714 this.consume(')'); |
|
20715 } else if (this.expect('[')) { |
|
20716 primary = this.arrayDeclaration(); |
|
20717 } else if (this.expect('{')) { |
|
20718 primary = this.object(); |
|
20719 } else { |
|
20720 var token = this.expect(); |
|
20721 primary = token.fn; |
|
20722 if (!primary) { |
|
20723 this.throwError('not a primary expression', token); |
|
20724 } |
|
20725 if (token.constant) { |
|
20726 primary.constant = true; |
|
20727 primary.literal = true; |
|
20728 } |
|
20729 } |
|
20730 |
|
20731 var next, context; |
|
20732 while ((next = this.expect('(', '[', '.'))) { |
|
20733 if (next.text === '(') { |
|
20734 primary = this.functionCall(primary, context); |
|
20735 context = null; |
|
20736 } else if (next.text === '[') { |
|
20737 context = primary; |
|
20738 primary = this.objectIndex(primary); |
|
20739 } else if (next.text === '.') { |
|
20740 context = primary; |
|
20741 primary = this.fieldAccess(primary); |
|
20742 } else { |
|
20743 this.throwError('IMPOSSIBLE'); |
|
20744 } |
|
20745 } |
|
20746 return primary; |
|
20747 }, |
|
20748 |
|
20749 throwError: function(msg, token) { |
|
20750 throw $parseMinErr('syntax', |
|
20751 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', |
|
20752 token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); |
|
20753 }, |
|
20754 |
|
20755 peekToken: function() { |
|
20756 if (this.tokens.length === 0) |
|
20757 throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); |
|
20758 return this.tokens[0]; |
|
20759 }, |
|
20760 |
|
20761 peek: function(e1, e2, e3, e4) { |
|
20762 if (this.tokens.length > 0) { |
|
20763 var token = this.tokens[0]; |
|
20764 var t = token.text; |
|
20765 if (t === e1 || t === e2 || t === e3 || t === e4 || |
|
20766 (!e1 && !e2 && !e3 && !e4)) { |
|
20767 return token; |
|
20768 } |
|
20769 } |
|
20770 return false; |
|
20771 }, |
|
20772 |
|
20773 expect: function(e1, e2, e3, e4){ |
|
20774 var token = this.peek(e1, e2, e3, e4); |
|
20775 if (token) { |
|
20776 this.tokens.shift(); |
|
20777 return token; |
|
20778 } |
|
20779 return false; |
|
20780 }, |
|
20781 |
|
20782 consume: function(e1){ |
|
20783 if (!this.expect(e1)) { |
|
20784 this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); |
|
20785 } |
|
20786 }, |
|
20787 |
|
20788 unaryFn: function(fn, right) { |
|
20789 return extend(function $parseUnaryFn(self, locals) { |
|
20790 return fn(self, locals, right); |
|
20791 }, { |
|
20792 constant:right.constant, |
|
20793 inputs: [right] |
|
20794 }); |
|
20795 }, |
|
20796 |
|
20797 binaryFn: function(left, fn, right, isBranching) { |
|
20798 return extend(function $parseBinaryFn(self, locals) { |
|
20799 return fn(self, locals, left, right); |
|
20800 }, { |
|
20801 constant: left.constant && right.constant, |
|
20802 inputs: !isBranching && [left, right] |
|
20803 }); |
|
20804 }, |
|
20805 |
|
20806 statements: function() { |
|
20807 var statements = []; |
|
20808 while (true) { |
|
20809 if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) |
|
20810 statements.push(this.filterChain()); |
|
20811 if (!this.expect(';')) { |
|
20812 // optimize for the common case where there is only one statement. |
|
20813 // TODO(size): maybe we should not support multiple statements? |
|
20814 return (statements.length === 1) |
|
20815 ? statements[0] |
|
20816 : function $parseStatements(self, locals) { |
|
20817 var value; |
|
20818 for (var i = 0, ii = statements.length; i < ii; i++) { |
|
20819 value = statements[i](self, locals); |
|
20820 } |
|
20821 return value; |
|
20822 }; |
|
20823 } |
|
20824 } |
|
20825 }, |
|
20826 |
|
20827 filterChain: function() { |
|
20828 var left = this.expression(); |
|
20829 var token; |
|
20830 while ((token = this.expect('|'))) { |
|
20831 left = this.filter(left); |
|
20832 } |
|
20833 return left; |
|
20834 }, |
|
20835 |
|
20836 filter: function(inputFn) { |
|
20837 var token = this.expect(); |
|
20838 var fn = this.$filter(token.text); |
|
20839 var argsFn; |
|
20840 var args; |
|
20841 |
|
20842 if (this.peek(':')) { |
|
20843 argsFn = []; |
|
20844 args = []; // we can safely reuse the array |
|
20845 while (this.expect(':')) { |
|
20846 argsFn.push(this.expression()); |
|
20847 } |
|
20848 } |
|
20849 |
|
20850 var inputs = [inputFn].concat(argsFn || []); |
|
20851 |
|
20852 return extend(function $parseFilter(self, locals) { |
|
20853 var input = inputFn(self, locals); |
|
20854 if (args) { |
|
20855 args[0] = input; |
|
20856 |
|
20857 var i = argsFn.length; |
|
20858 while (i--) { |
|
20859 args[i + 1] = argsFn[i](self, locals); |
|
20860 } |
|
20861 |
|
20862 return fn.apply(undefined, args); |
|
20863 } |
|
20864 |
|
20865 return fn(input); |
|
20866 }, { |
|
20867 constant: !fn.$stateful && inputs.every(isConstant), |
|
20868 inputs: !fn.$stateful && inputs |
|
20869 }); |
|
20870 }, |
|
20871 |
|
20872 expression: function() { |
|
20873 return this.assignment(); |
|
20874 }, |
|
20875 |
|
20876 assignment: function() { |
|
20877 var left = this.ternary(); |
|
20878 var right; |
|
20879 var token; |
|
20880 if ((token = this.expect('='))) { |
|
20881 if (!left.assign) { |
|
20882 this.throwError('implies assignment but [' + |
|
20883 this.text.substring(0, token.index) + '] can not be assigned to', token); |
|
20884 } |
|
20885 right = this.ternary(); |
|
20886 return extend(function $parseAssignment(scope, locals) { |
|
20887 return left.assign(scope, right(scope, locals), locals); |
|
20888 }, { |
|
20889 inputs: [left, right] |
|
20890 }); |
|
20891 } |
|
20892 return left; |
|
20893 }, |
|
20894 |
|
20895 ternary: function() { |
|
20896 var left = this.logicalOR(); |
|
20897 var middle; |
|
20898 var token; |
|
20899 if ((token = this.expect('?'))) { |
|
20900 middle = this.assignment(); |
|
20901 if ((token = this.expect(':'))) { |
|
20902 var right = this.assignment(); |
|
20903 |
|
20904 return extend(function $parseTernary(self, locals){ |
|
20905 return left(self, locals) ? middle(self, locals) : right(self, locals); |
|
20906 }, { |
|
20907 constant: left.constant && middle.constant && right.constant |
|
20908 }); |
|
20909 |
|
20910 } else { |
|
20911 this.throwError('expected :', token); |
|
20912 } |
|
20913 } |
|
20914 |
|
20915 return left; |
|
20916 }, |
|
20917 |
|
20918 logicalOR: function() { |
|
20919 var left = this.logicalAND(); |
|
20920 var token; |
|
20921 while ((token = this.expect('||'))) { |
|
20922 left = this.binaryFn(left, token.fn, this.logicalAND(), true); |
|
20923 } |
|
20924 return left; |
|
20925 }, |
|
20926 |
|
20927 logicalAND: function() { |
|
20928 var left = this.equality(); |
|
20929 var token; |
|
20930 if ((token = this.expect('&&'))) { |
|
20931 left = this.binaryFn(left, token.fn, this.logicalAND(), true); |
|
20932 } |
|
20933 return left; |
|
20934 }, |
|
20935 |
|
20936 equality: function() { |
|
20937 var left = this.relational(); |
|
20938 var token; |
|
20939 if ((token = this.expect('==','!=','===','!=='))) { |
|
20940 left = this.binaryFn(left, token.fn, this.equality()); |
|
20941 } |
|
20942 return left; |
|
20943 }, |
|
20944 |
|
20945 relational: function() { |
|
20946 var left = this.additive(); |
|
20947 var token; |
|
20948 if ((token = this.expect('<', '>', '<=', '>='))) { |
|
20949 left = this.binaryFn(left, token.fn, this.relational()); |
|
20950 } |
|
20951 return left; |
|
20952 }, |
|
20953 |
|
20954 additive: function() { |
|
20955 var left = this.multiplicative(); |
|
20956 var token; |
|
20957 while ((token = this.expect('+','-'))) { |
|
20958 left = this.binaryFn(left, token.fn, this.multiplicative()); |
|
20959 } |
|
20960 return left; |
|
20961 }, |
|
20962 |
|
20963 multiplicative: function() { |
|
20964 var left = this.unary(); |
|
20965 var token; |
|
20966 while ((token = this.expect('*','/','%'))) { |
|
20967 left = this.binaryFn(left, token.fn, this.unary()); |
|
20968 } |
|
20969 return left; |
|
20970 }, |
|
20971 |
|
20972 unary: function() { |
|
20973 var token; |
|
20974 if (this.expect('+')) { |
|
20975 return this.primary(); |
|
20976 } else if ((token = this.expect('-'))) { |
|
20977 return this.binaryFn(Parser.ZERO, token.fn, this.unary()); |
|
20978 } else if ((token = this.expect('!'))) { |
|
20979 return this.unaryFn(token.fn, this.unary()); |
|
20980 } else { |
|
20981 return this.primary(); |
|
20982 } |
|
20983 }, |
|
20984 |
|
20985 fieldAccess: function(object) { |
|
20986 var expression = this.text; |
|
20987 var field = this.expect().text; |
|
20988 var getter = getterFn(field, this.options, expression); |
|
20989 |
|
20990 return extend(function $parseFieldAccess(scope, locals, self) { |
|
20991 return getter(self || object(scope, locals)); |
|
20992 }, { |
|
20993 assign: function(scope, value, locals) { |
|
20994 var o = object(scope, locals); |
|
20995 if (!o) object.assign(scope, o = {}); |
|
20996 return setter(o, field, value, expression); |
|
20997 } |
|
20998 }); |
|
20999 }, |
|
21000 |
|
21001 objectIndex: function(obj) { |
|
21002 var expression = this.text; |
|
21003 |
|
21004 var indexFn = this.expression(); |
|
21005 this.consume(']'); |
|
21006 |
|
21007 return extend(function $parseObjectIndex(self, locals) { |
|
21008 var o = obj(self, locals), |
|
21009 i = indexFn(self, locals), |
|
21010 v; |
|
21011 |
|
21012 ensureSafeMemberName(i, expression); |
|
21013 if (!o) return undefined; |
|
21014 v = ensureSafeObject(o[i], expression); |
|
21015 return v; |
|
21016 }, { |
|
21017 assign: function(self, value, locals) { |
|
21018 var key = ensureSafeMemberName(indexFn(self, locals), expression); |
|
21019 // prevent overwriting of Function.constructor which would break ensureSafeObject check |
|
21020 var o = ensureSafeObject(obj(self, locals), expression); |
|
21021 if (!o) obj.assign(self, o = {}); |
|
21022 return o[key] = value; |
|
21023 } |
|
21024 }); |
|
21025 }, |
|
21026 |
|
21027 functionCall: function(fnGetter, contextGetter) { |
|
21028 var argsFn = []; |
|
21029 if (this.peekToken().text !== ')') { |
|
21030 do { |
|
21031 argsFn.push(this.expression()); |
|
21032 } while (this.expect(',')); |
|
21033 } |
|
21034 this.consume(')'); |
|
21035 |
|
21036 var expressionText = this.text; |
|
21037 // we can safely reuse the array across invocations |
|
21038 var args = argsFn.length ? [] : null; |
|
21039 |
|
21040 return function $parseFunctionCall(scope, locals) { |
|
21041 var context = contextGetter ? contextGetter(scope, locals) : scope; |
|
21042 var fn = fnGetter(scope, locals, context) || noop; |
|
21043 |
|
21044 if (args) { |
|
21045 var i = argsFn.length; |
|
21046 while (i--) { |
|
21047 args[i] = ensureSafeObject(argsFn[i](scope, locals), expressionText); |
|
21048 } |
|
21049 } |
|
21050 |
|
21051 ensureSafeObject(context, expressionText); |
|
21052 ensureSafeFunction(fn, expressionText); |
|
21053 |
|
21054 // IE stupidity! (IE doesn't have apply for some native functions) |
|
21055 var v = fn.apply |
|
21056 ? fn.apply(context, args) |
|
21057 : fn(args[0], args[1], args[2], args[3], args[4]); |
|
21058 |
|
21059 return ensureSafeObject(v, expressionText); |
|
21060 }; |
|
21061 }, |
|
21062 |
|
21063 // This is used with json array declaration |
|
21064 arrayDeclaration: function () { |
|
21065 var elementFns = []; |
|
21066 if (this.peekToken().text !== ']') { |
|
21067 do { |
|
21068 if (this.peek(']')) { |
|
21069 // Support trailing commas per ES5.1. |
|
21070 break; |
|
21071 } |
|
21072 var elementFn = this.expression(); |
|
21073 elementFns.push(elementFn); |
|
21074 } while (this.expect(',')); |
|
21075 } |
|
21076 this.consume(']'); |
|
21077 |
|
21078 return extend(function $parseArrayLiteral(self, locals) { |
|
21079 var array = []; |
|
21080 for (var i = 0, ii = elementFns.length; i < ii; i++) { |
|
21081 array.push(elementFns[i](self, locals)); |
|
21082 } |
|
21083 return array; |
|
21084 }, { |
|
21085 literal: true, |
|
21086 constant: elementFns.every(isConstant), |
|
21087 inputs: elementFns |
|
21088 }); |
|
21089 }, |
|
21090 |
|
21091 object: function () { |
|
21092 var keys = [], valueFns = []; |
|
21093 if (this.peekToken().text !== '}') { |
|
21094 do { |
|
21095 if (this.peek('}')) { |
|
21096 // Support trailing commas per ES5.1. |
|
21097 break; |
|
21098 } |
|
21099 var token = this.expect(); |
|
21100 keys.push(token.string || token.text); |
|
21101 this.consume(':'); |
|
21102 var value = this.expression(); |
|
21103 valueFns.push(value); |
|
21104 } while (this.expect(',')); |
|
21105 } |
|
21106 this.consume('}'); |
|
21107 |
|
21108 return extend(function $parseObjectLiteral(self, locals) { |
|
21109 var object = {}; |
|
21110 for (var i = 0, ii = valueFns.length; i < ii; i++) { |
|
21111 object[keys[i]] = valueFns[i](self, locals); |
|
21112 } |
|
21113 return object; |
|
21114 }, { |
|
21115 literal: true, |
|
21116 constant: valueFns.every(isConstant), |
|
21117 inputs: valueFns |
|
21118 }); |
|
21119 } |
|
21120 }; |
|
21121 |
|
21122 |
|
21123 ////////////////////////////////////////////////// |
|
21124 // Parser helper functions |
|
21125 ////////////////////////////////////////////////// |
|
21126 |
|
21127 function setter(obj, path, setValue, fullExp) { |
|
21128 ensureSafeObject(obj, fullExp); |
|
21129 |
|
21130 var element = path.split('.'), key; |
|
21131 for (var i = 0; element.length > 1; i++) { |
|
21132 key = ensureSafeMemberName(element.shift(), fullExp); |
|
21133 var propertyObj = ensureSafeObject(obj[key], fullExp); |
|
21134 if (!propertyObj) { |
|
21135 propertyObj = {}; |
|
21136 obj[key] = propertyObj; |
|
21137 } |
|
21138 obj = propertyObj; |
|
21139 } |
|
21140 key = ensureSafeMemberName(element.shift(), fullExp); |
|
21141 ensureSafeObject(obj[key], fullExp); |
|
21142 obj[key] = setValue; |
|
21143 return setValue; |
|
21144 } |
|
21145 |
|
21146 var getterFnCache = createMap(); |
|
21147 |
|
21148 /** |
|
21149 * Implementation of the "Black Hole" variant from: |
|
21150 * - http://jsperf.com/angularjs-parse-getter/4 |
|
21151 * - http://jsperf.com/path-evaluation-simplified/7 |
|
21152 */ |
|
21153 function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) { |
|
21154 ensureSafeMemberName(key0, fullExp); |
|
21155 ensureSafeMemberName(key1, fullExp); |
|
21156 ensureSafeMemberName(key2, fullExp); |
|
21157 ensureSafeMemberName(key3, fullExp); |
|
21158 ensureSafeMemberName(key4, fullExp); |
|
21159 |
|
21160 return function cspSafeGetter(scope, locals) { |
|
21161 var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; |
|
21162 |
|
21163 if (pathVal == null) return pathVal; |
|
21164 pathVal = pathVal[key0]; |
|
21165 |
|
21166 if (!key1) return pathVal; |
|
21167 if (pathVal == null) return undefined; |
|
21168 pathVal = pathVal[key1]; |
|
21169 |
|
21170 if (!key2) return pathVal; |
|
21171 if (pathVal == null) return undefined; |
|
21172 pathVal = pathVal[key2]; |
|
21173 |
|
21174 if (!key3) return pathVal; |
|
21175 if (pathVal == null) return undefined; |
|
21176 pathVal = pathVal[key3]; |
|
21177 |
|
21178 if (!key4) return pathVal; |
|
21179 if (pathVal == null) return undefined; |
|
21180 pathVal = pathVal[key4]; |
|
21181 |
|
21182 return pathVal; |
|
21183 }; |
|
21184 } |
|
21185 |
|
21186 function getterFn(path, options, fullExp) { |
|
21187 var fn = getterFnCache[path]; |
|
21188 |
|
21189 if (fn) return fn; |
|
21190 |
|
21191 var pathKeys = path.split('.'), |
|
21192 pathKeysLength = pathKeys.length; |
|
21193 |
|
21194 // http://jsperf.com/angularjs-parse-getter/6 |
|
21195 if (options.csp) { |
|
21196 if (pathKeysLength < 6) { |
|
21197 fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp); |
|
21198 } else { |
|
21199 fn = function cspSafeGetter(scope, locals) { |
|
21200 var i = 0, val; |
|
21201 do { |
|
21202 val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], |
|
21203 pathKeys[i++], fullExp)(scope, locals); |
|
21204 |
|
21205 locals = undefined; // clear after first iteration |
|
21206 scope = val; |
|
21207 } while (i < pathKeysLength); |
|
21208 return val; |
|
21209 }; |
|
21210 } |
|
21211 } else { |
|
21212 var code = ''; |
|
21213 forEach(pathKeys, function(key, index) { |
|
21214 ensureSafeMemberName(key, fullExp); |
|
21215 code += 'if(s == null) return undefined;\n' + |
|
21216 's='+ (index |
|
21217 // we simply dereference 's' on any .dot notation |
|
21218 ? 's' |
|
21219 // but if we are first then we check locals first, and if so read it first |
|
21220 : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key + ';\n'; |
|
21221 }); |
|
21222 code += 'return s;'; |
|
21223 |
|
21224 /* jshint -W054 */ |
|
21225 var evaledFnGetter = new Function('s', 'l', code); // s=scope, l=locals |
|
21226 /* jshint +W054 */ |
|
21227 evaledFnGetter.toString = valueFn(code); |
|
21228 |
|
21229 fn = evaledFnGetter; |
|
21230 } |
|
21231 |
|
21232 fn.sharedGetter = true; |
|
21233 fn.assign = function(self, value) { |
|
21234 return setter(self, path, value, path); |
|
21235 }; |
|
21236 getterFnCache[path] = fn; |
|
21237 return fn; |
|
21238 } |
|
21239 |
|
21240 /////////////////////////////////// |
|
21241 |
|
21242 /** |
|
21243 * @ngdoc service |
|
21244 * @name $parse |
|
21245 * @kind function |
|
21246 * |
|
21247 * @description |
|
21248 * |
|
21249 * Converts Angular {@link guide/expression expression} into a function. |
|
21250 * |
|
21251 * ```js |
|
21252 * var getter = $parse('user.name'); |
|
21253 * var setter = getter.assign; |
|
21254 * var context = {user:{name:'angular'}}; |
|
21255 * var locals = {user:{name:'local'}}; |
|
21256 * |
|
21257 * expect(getter(context)).toEqual('angular'); |
|
21258 * setter(context, 'newValue'); |
|
21259 * expect(context.user.name).toEqual('newValue'); |
|
21260 * expect(getter(context, locals)).toEqual('local'); |
|
21261 * ``` |
|
21262 * |
|
21263 * |
|
21264 * @param {string} expression String expression to compile. |
|
21265 * @returns {function(context, locals)} a function which represents the compiled expression: |
|
21266 * |
|
21267 * * `context` – `{object}` – an object against which any expressions embedded in the strings |
|
21268 * are evaluated against (typically a scope object). |
|
21269 * * `locals` – `{object=}` – local variables context object, useful for overriding values in |
|
21270 * `context`. |
|
21271 * |
|
21272 * The returned function also has the following properties: |
|
21273 * * `literal` – `{boolean}` – whether the expression's top-level node is a JavaScript |
|
21274 * literal. |
|
21275 * * `constant` – `{boolean}` – whether the expression is made entirely of JavaScript |
|
21276 * constant literals. |
|
21277 * * `assign` – `{?function(context, value)}` – if the expression is assignable, this will be |
|
21278 * set to a function to change its value on the given context. |
|
21279 * |
|
21280 */ |
|
21281 |
|
21282 |
|
21283 /** |
|
21284 * @ngdoc provider |
|
21285 * @name $parseProvider |
|
21286 * |
|
21287 * @description |
|
21288 * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} |
|
21289 * service. |
|
21290 */ |
|
21291 function $ParseProvider() { |
|
21292 var cache = createMap(); |
|
21293 |
|
21294 var $parseOptions = { |
|
21295 csp: false |
|
21296 }; |
|
21297 |
|
21298 |
|
21299 this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { |
|
21300 $parseOptions.csp = $sniffer.csp; |
|
21301 |
|
21302 function wrapSharedExpression(exp) { |
|
21303 var wrapped = exp; |
|
21304 |
|
21305 if (exp.sharedGetter) { |
|
21306 wrapped = function $parseWrapper(self, locals) { |
|
21307 return exp(self, locals); |
|
21308 }; |
|
21309 wrapped.literal = exp.literal; |
|
21310 wrapped.constant = exp.constant; |
|
21311 wrapped.assign = exp.assign; |
|
21312 } |
|
21313 |
|
21314 return wrapped; |
|
21315 } |
|
21316 |
|
21317 return function $parse(exp, interceptorFn) { |
|
21318 var parsedExpression, oneTime, cacheKey; |
|
21319 |
|
21320 switch (typeof exp) { |
|
21321 case 'string': |
|
21322 cacheKey = exp = exp.trim(); |
|
21323 |
|
21324 parsedExpression = cache[cacheKey]; |
|
21325 |
|
21326 if (!parsedExpression) { |
|
21327 if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { |
|
21328 oneTime = true; |
|
21329 exp = exp.substring(2); |
|
21330 } |
|
21331 |
|
21332 var lexer = new Lexer($parseOptions); |
|
21333 var parser = new Parser(lexer, $filter, $parseOptions); |
|
21334 parsedExpression = parser.parse(exp); |
|
21335 |
|
21336 if (parsedExpression.constant) { |
|
21337 parsedExpression.$$watchDelegate = constantWatchDelegate; |
|
21338 } else if (oneTime) { |
|
21339 //oneTime is not part of the exp passed to the Parser so we may have to |
|
21340 //wrap the parsedExpression before adding a $$watchDelegate |
|
21341 parsedExpression = wrapSharedExpression(parsedExpression); |
|
21342 parsedExpression.$$watchDelegate = parsedExpression.literal ? |
|
21343 oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; |
|
21344 } else if (parsedExpression.inputs) { |
|
21345 parsedExpression.$$watchDelegate = inputsWatchDelegate; |
|
21346 } |
|
21347 |
|
21348 cache[cacheKey] = parsedExpression; |
|
21349 } |
|
21350 return addInterceptor(parsedExpression, interceptorFn); |
|
21351 |
|
21352 case 'function': |
|
21353 return addInterceptor(exp, interceptorFn); |
|
21354 |
|
21355 default: |
|
21356 return addInterceptor(noop, interceptorFn); |
|
21357 } |
|
21358 }; |
|
21359 |
|
21360 function collectExpressionInputs(inputs, list) { |
|
21361 for (var i = 0, ii = inputs.length; i < ii; i++) { |
|
21362 var input = inputs[i]; |
|
21363 if (!input.constant) { |
|
21364 if (input.inputs) { |
|
21365 collectExpressionInputs(input.inputs, list); |
|
21366 } else if (list.indexOf(input) === -1) { // TODO(perf) can we do better? |
|
21367 list.push(input); |
|
21368 } |
|
21369 } |
|
21370 } |
|
21371 |
|
21372 return list; |
|
21373 } |
|
21374 |
|
21375 function expressionInputDirtyCheck(newValue, oldValueOfValue) { |
|
21376 |
|
21377 if (newValue == null || oldValueOfValue == null) { // null/undefined |
|
21378 return newValue === oldValueOfValue; |
|
21379 } |
|
21380 |
|
21381 if (typeof newValue === 'object') { |
|
21382 |
|
21383 // attempt to convert the value to a primitive type |
|
21384 // TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can |
|
21385 // be cheaply dirty-checked |
|
21386 newValue = newValue.valueOf(); |
|
21387 |
|
21388 if (typeof newValue === 'object') { |
|
21389 // objects/arrays are not supported - deep-watching them would be too expensive |
|
21390 return false; |
|
21391 } |
|
21392 |
|
21393 // fall-through to the primitive equality check |
|
21394 } |
|
21395 |
|
21396 //Primitive or NaN |
|
21397 return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); |
|
21398 } |
|
21399 |
|
21400 function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression) { |
|
21401 var inputExpressions = parsedExpression.$$inputs || |
|
21402 (parsedExpression.$$inputs = collectExpressionInputs(parsedExpression.inputs, [])); |
|
21403 |
|
21404 var lastResult; |
|
21405 |
|
21406 if (inputExpressions.length === 1) { |
|
21407 var oldInputValue = expressionInputDirtyCheck; // init to something unique so that equals check fails |
|
21408 inputExpressions = inputExpressions[0]; |
|
21409 return scope.$watch(function expressionInputWatch(scope) { |
|
21410 var newInputValue = inputExpressions(scope); |
|
21411 if (!expressionInputDirtyCheck(newInputValue, oldInputValue)) { |
|
21412 lastResult = parsedExpression(scope); |
|
21413 oldInputValue = newInputValue && newInputValue.valueOf(); |
|
21414 } |
|
21415 return lastResult; |
|
21416 }, listener, objectEquality); |
|
21417 } |
|
21418 |
|
21419 var oldInputValueOfValues = []; |
|
21420 for (var i = 0, ii = inputExpressions.length; i < ii; i++) { |
|
21421 oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails |
|
21422 } |
|
21423 |
|
21424 return scope.$watch(function expressionInputsWatch(scope) { |
|
21425 var changed = false; |
|
21426 |
|
21427 for (var i = 0, ii = inputExpressions.length; i < ii; i++) { |
|
21428 var newInputValue = inputExpressions[i](scope); |
|
21429 if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) { |
|
21430 oldInputValueOfValues[i] = newInputValue && newInputValue.valueOf(); |
|
21431 } |
|
21432 } |
|
21433 |
|
21434 if (changed) { |
|
21435 lastResult = parsedExpression(scope); |
|
21436 } |
|
21437 |
|
21438 return lastResult; |
|
21439 }, listener, objectEquality); |
|
21440 } |
|
21441 |
|
21442 function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { |
|
21443 var unwatch, lastValue; |
|
21444 return unwatch = scope.$watch(function oneTimeWatch(scope) { |
|
21445 return parsedExpression(scope); |
|
21446 }, function oneTimeListener(value, old, scope) { |
|
21447 lastValue = value; |
|
21448 if (isFunction(listener)) { |
|
21449 listener.apply(this, arguments); |
|
21450 } |
|
21451 if (isDefined(value)) { |
|
21452 scope.$$postDigest(function () { |
|
21453 if (isDefined(lastValue)) { |
|
21454 unwatch(); |
|
21455 } |
|
21456 }); |
|
21457 } |
|
21458 }, objectEquality); |
|
21459 } |
|
21460 |
|
21461 function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { |
|
21462 var unwatch; |
|
21463 return unwatch = scope.$watch(function oneTimeWatch(scope) { |
|
21464 return parsedExpression(scope); |
|
21465 }, function oneTimeListener(value, old, scope) { |
|
21466 if (isFunction(listener)) { |
|
21467 listener.call(this, value, old, scope); |
|
21468 } |
|
21469 if (isAllDefined(value)) { |
|
21470 scope.$$postDigest(function () { |
|
21471 if(isAllDefined(value)) unwatch(); |
|
21472 }); |
|
21473 } |
|
21474 }, objectEquality); |
|
21475 |
|
21476 function isAllDefined(value) { |
|
21477 var allDefined = true; |
|
21478 forEach(value, function (val) { |
|
21479 if (!isDefined(val)) allDefined = false; |
|
21480 }); |
|
21481 return allDefined; |
|
21482 } |
|
21483 } |
|
21484 |
|
21485 function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { |
|
21486 var unwatch; |
|
21487 return unwatch = scope.$watch(function constantWatch(scope) { |
|
21488 return parsedExpression(scope); |
|
21489 }, function constantListener(value, old, scope) { |
|
21490 if (isFunction(listener)) { |
|
21491 listener.apply(this, arguments); |
|
21492 } |
|
21493 unwatch(); |
|
21494 }, objectEquality); |
|
21495 } |
|
21496 |
|
21497 function addInterceptor(parsedExpression, interceptorFn) { |
|
21498 if (!interceptorFn) return parsedExpression; |
|
21499 |
|
21500 var fn = function interceptedExpression(scope, locals) { |
|
21501 var value = parsedExpression(scope, locals); |
|
21502 var result = interceptorFn(value, scope, locals); |
|
21503 // we only return the interceptor's result if the |
|
21504 // initial value is defined (for bind-once) |
|
21505 return isDefined(value) ? result : value; |
|
21506 }; |
|
21507 |
|
21508 // Propagate $$watchDelegates other then inputsWatchDelegate |
|
21509 if (parsedExpression.$$watchDelegate && |
|
21510 parsedExpression.$$watchDelegate !== inputsWatchDelegate) { |
|
21511 fn.$$watchDelegate = parsedExpression.$$watchDelegate; |
|
21512 } else if (!interceptorFn.$stateful) { |
|
21513 // If there is an interceptor, but no watchDelegate then treat the interceptor like |
|
21514 // we treat filters - it is assumed to be a pure function unless flagged with $stateful |
|
21515 fn.$$watchDelegate = inputsWatchDelegate; |
|
21516 fn.inputs = [parsedExpression]; |
|
21517 } |
|
21518 |
|
21519 return fn; |
|
21520 } |
|
21521 }]; |
|
21522 } |
|
21523 |
|
21524 /** |
|
21525 * @ngdoc service |
|
21526 * @name $q |
|
21527 * @requires $rootScope |
|
21528 * |
|
21529 * @description |
|
21530 * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). |
|
21531 * |
|
21532 * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred |
|
21533 * implementations, and the other which resembles ES6 promises to some degree. |
|
21534 * |
|
21535 * # $q constructor |
|
21536 * |
|
21537 * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` |
|
21538 * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony, |
|
21539 * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). |
|
21540 * |
|
21541 * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are |
|
21542 * available yet. |
|
21543 * |
|
21544 * It can be used like so: |
|
21545 * |
|
21546 * ```js |
|
21547 * return $q(function(resolve, reject) { |
|
21548 * // perform some asynchronous operation, resolve or reject the promise when appropriate. |
|
21549 * setInterval(function() { |
|
21550 * if (pollStatus > 0) { |
|
21551 * resolve(polledValue); |
|
21552 * } else if (pollStatus < 0) { |
|
21553 * reject(polledValue); |
|
21554 * } else { |
|
21555 * pollStatus = pollAgain(function(value) { |
|
21556 * polledValue = value; |
|
21557 * }); |
|
21558 * } |
|
21559 * }, 10000); |
|
21560 * }). |
|
21561 * then(function(value) { |
|
21562 * // handle success |
|
21563 * }, function(reason) { |
|
21564 * // handle failure |
|
21565 * }); |
|
21566 * ``` |
|
21567 * |
|
21568 * Note: progress/notify callbacks are not currently supported via the ES6-style interface. |
|
21569 * |
|
21570 * However, the more traditional CommonJS-style usage is still available, and documented below. |
|
21571 * |
|
21572 * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an |
|
21573 * interface for interacting with an object that represents the result of an action that is |
|
21574 * performed asynchronously, and may or may not be finished at any given point in time. |
|
21575 * |
|
21576 * From the perspective of dealing with error handling, deferred and promise APIs are to |
|
21577 * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. |
|
21578 * |
|
21579 * ```js |
|
21580 * // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet` |
|
21581 * // are available in the current lexical scope (they could have been injected or passed in). |
|
21582 * |
|
21583 * function asyncGreet(name) { |
|
21584 * var deferred = $q.defer(); |
|
21585 * |
|
21586 * setTimeout(function() { |
|
21587 * deferred.notify('About to greet ' + name + '.'); |
|
21588 * |
|
21589 * if (okToGreet(name)) { |
|
21590 * deferred.resolve('Hello, ' + name + '!'); |
|
21591 * } else { |
|
21592 * deferred.reject('Greeting ' + name + ' is not allowed.'); |
|
21593 * } |
|
21594 * }, 1000); |
|
21595 * |
|
21596 * return deferred.promise; |
|
21597 * } |
|
21598 * |
|
21599 * var promise = asyncGreet('Robin Hood'); |
|
21600 * promise.then(function(greeting) { |
|
21601 * alert('Success: ' + greeting); |
|
21602 * }, function(reason) { |
|
21603 * alert('Failed: ' + reason); |
|
21604 * }, function(update) { |
|
21605 * alert('Got notification: ' + update); |
|
21606 * }); |
|
21607 * ``` |
|
21608 * |
|
21609 * At first it might not be obvious why this extra complexity is worth the trouble. The payoff |
|
21610 * comes in the way of guarantees that promise and deferred APIs make, see |
|
21611 * https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md. |
|
21612 * |
|
21613 * Additionally the promise api allows for composition that is very hard to do with the |
|
21614 * traditional callback ([CPS](http://en.wikipedia.org/wiki/Continuation-passing_style)) approach. |
|
21615 * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the |
|
21616 * section on serial or parallel joining of promises. |
|
21617 * |
|
21618 * # The Deferred API |
|
21619 * |
|
21620 * A new instance of deferred is constructed by calling `$q.defer()`. |
|
21621 * |
|
21622 * The purpose of the deferred object is to expose the associated Promise instance as well as APIs |
|
21623 * that can be used for signaling the successful or unsuccessful completion, as well as the status |
|
21624 * of the task. |
|
21625 * |
|
21626 * **Methods** |
|
21627 * |
|
21628 * - `resolve(value)` – resolves the derived promise with the `value`. If the value is a rejection |
|
21629 * constructed via `$q.reject`, the promise will be rejected instead. |
|
21630 * - `reject(reason)` – rejects the derived promise with the `reason`. This is equivalent to |
|
21631 * resolving it with a rejection constructed via `$q.reject`. |
|
21632 * - `notify(value)` - provides updates on the status of the promise's execution. This may be called |
|
21633 * multiple times before the promise is either resolved or rejected. |
|
21634 * |
|
21635 * **Properties** |
|
21636 * |
|
21637 * - promise – `{Promise}` – promise object associated with this deferred. |
|
21638 * |
|
21639 * |
|
21640 * # The Promise API |
|
21641 * |
|
21642 * A new promise instance is created when a deferred instance is created and can be retrieved by |
|
21643 * calling `deferred.promise`. |
|
21644 * |
|
21645 * The purpose of the promise object is to allow for interested parties to get access to the result |
|
21646 * of the deferred task when it completes. |
|
21647 * |
|
21648 * **Methods** |
|
21649 * |
|
21650 * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or |
|
21651 * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously |
|
21652 * as soon as the result is available. The callbacks are called with a single argument: the result |
|
21653 * or rejection reason. Additionally, the notify callback may be called zero or more times to |
|
21654 * provide a progress indication, before the promise is resolved or rejected. |
|
21655 * |
|
21656 * This method *returns a new promise* which is resolved or rejected via the return value of the |
|
21657 * `successCallback`, `errorCallback`. It also notifies via the return value of the |
|
21658 * `notifyCallback` method. The promise cannot be resolved or rejected from the notifyCallback |
|
21659 * method. |
|
21660 * |
|
21661 * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` |
|
21662 * |
|
21663 * - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise, |
|
21664 * but to do so without modifying the final value. This is useful to release resources or do some |
|
21665 * clean-up that needs to be done whether the promise was rejected or resolved. See the [full |
|
21666 * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for |
|
21667 * more information. |
|
21668 * |
|
21669 * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as |
|
21670 * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to |
|
21671 * make your code IE8 and Android 2.x compatible. |
|
21672 * |
|
21673 * # Chaining promises |
|
21674 * |
|
21675 * Because calling the `then` method of a promise returns a new derived promise, it is easily |
|
21676 * possible to create a chain of promises: |
|
21677 * |
|
21678 * ```js |
|
21679 * promiseB = promiseA.then(function(result) { |
|
21680 * return result + 1; |
|
21681 * }); |
|
21682 * |
|
21683 * // promiseB will be resolved immediately after promiseA is resolved and its value |
|
21684 * // will be the result of promiseA incremented by 1 |
|
21685 * ``` |
|
21686 * |
|
21687 * It is possible to create chains of any length and since a promise can be resolved with another |
|
21688 * promise (which will defer its resolution further), it is possible to pause/defer resolution of |
|
21689 * the promises at any point in the chain. This makes it possible to implement powerful APIs like |
|
21690 * $http's response interceptors. |
|
21691 * |
|
21692 * |
|
21693 * # Differences between Kris Kowal's Q and $q |
|
21694 * |
|
21695 * There are two main differences: |
|
21696 * |
|
21697 * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation |
|
21698 * mechanism in angular, which means faster propagation of resolution or rejection into your |
|
21699 * models and avoiding unnecessary browser repaints, which would result in flickering UI. |
|
21700 * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains |
|
21701 * all the important functionality needed for common async tasks. |
|
21702 * |
|
21703 * # Testing |
|
21704 * |
|
21705 * ```js |
|
21706 * it('should simulate promise', inject(function($q, $rootScope) { |
|
21707 * var deferred = $q.defer(); |
|
21708 * var promise = deferred.promise; |
|
21709 * var resolvedValue; |
|
21710 * |
|
21711 * promise.then(function(value) { resolvedValue = value; }); |
|
21712 * expect(resolvedValue).toBeUndefined(); |
|
21713 * |
|
21714 * // Simulate resolving of promise |
|
21715 * deferred.resolve(123); |
|
21716 * // Note that the 'then' function does not get called synchronously. |
|
21717 * // This is because we want the promise API to always be async, whether or not |
|
21718 * // it got called synchronously or asynchronously. |
|
21719 * expect(resolvedValue).toBeUndefined(); |
|
21720 * |
|
21721 * // Propagate promise resolution to 'then' functions using $apply(). |
|
21722 * $rootScope.$apply(); |
|
21723 * expect(resolvedValue).toEqual(123); |
|
21724 * })); |
|
21725 * ``` |
|
21726 * |
|
21727 * @param {function(function, function)} resolver Function which is responsible for resolving or |
|
21728 * rejecting the newly created promise. The first parameter is a function which resolves the |
|
21729 * promise, the second parameter is a function which rejects the promise. |
|
21730 * |
|
21731 * @returns {Promise} The newly created promise. |
|
21732 */ |
|
21733 function $QProvider() { |
|
21734 |
|
21735 this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { |
|
21736 return qFactory(function(callback) { |
|
21737 $rootScope.$evalAsync(callback); |
|
21738 }, $exceptionHandler); |
|
21739 }]; |
|
21740 } |
|
21741 |
|
21742 function $$QProvider() { |
|
21743 this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { |
|
21744 return qFactory(function(callback) { |
|
21745 $browser.defer(callback); |
|
21746 }, $exceptionHandler); |
|
21747 }]; |
|
21748 } |
|
21749 |
|
21750 /** |
|
21751 * Constructs a promise manager. |
|
21752 * |
|
21753 * @param {function(function)} nextTick Function for executing functions in the next turn. |
|
21754 * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for |
|
21755 * debugging purposes. |
|
21756 * @returns {object} Promise manager. |
|
21757 */ |
|
21758 function qFactory(nextTick, exceptionHandler) { |
|
21759 var $qMinErr = minErr('$q', TypeError); |
|
21760 function callOnce(self, resolveFn, rejectFn) { |
|
21761 var called = false; |
|
21762 function wrap(fn) { |
|
21763 return function(value) { |
|
21764 if (called) return; |
|
21765 called = true; |
|
21766 fn.call(self, value); |
|
21767 }; |
|
21768 } |
|
21769 |
|
21770 return [wrap(resolveFn), wrap(rejectFn)]; |
|
21771 } |
|
21772 |
|
21773 /** |
|
21774 * @ngdoc method |
|
21775 * @name ng.$q#defer |
|
21776 * @kind function |
|
21777 * |
|
21778 * @description |
|
21779 * Creates a `Deferred` object which represents a task which will finish in the future. |
|
21780 * |
|
21781 * @returns {Deferred} Returns a new instance of deferred. |
|
21782 */ |
|
21783 var defer = function() { |
|
21784 return new Deferred(); |
|
21785 }; |
|
21786 |
|
21787 function Promise() { |
|
21788 this.$$state = { status: 0 }; |
|
21789 } |
|
21790 |
|
21791 Promise.prototype = { |
|
21792 then: function(onFulfilled, onRejected, progressBack) { |
|
21793 var result = new Deferred(); |
|
21794 |
|
21795 this.$$state.pending = this.$$state.pending || []; |
|
21796 this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]); |
|
21797 if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); |
|
21798 |
|
21799 return result.promise; |
|
21800 }, |
|
21801 |
|
21802 "catch": function(callback) { |
|
21803 return this.then(null, callback); |
|
21804 }, |
|
21805 |
|
21806 "finally": function(callback, progressBack) { |
|
21807 return this.then(function(value) { |
|
21808 return handleCallback(value, true, callback); |
|
21809 }, function(error) { |
|
21810 return handleCallback(error, false, callback); |
|
21811 }, progressBack); |
|
21812 } |
|
21813 }; |
|
21814 |
|
21815 //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native |
|
21816 function simpleBind(context, fn) { |
|
21817 return function(value) { |
|
21818 fn.call(context, value); |
|
21819 }; |
|
21820 } |
|
21821 |
|
21822 function processQueue(state) { |
|
21823 var fn, promise, pending; |
|
21824 |
|
21825 pending = state.pending; |
|
21826 state.processScheduled = false; |
|
21827 state.pending = undefined; |
|
21828 for (var i = 0, ii = pending.length; i < ii; ++i) { |
|
21829 promise = pending[i][0]; |
|
21830 fn = pending[i][state.status]; |
|
21831 try { |
|
21832 if (isFunction(fn)) { |
|
21833 promise.resolve(fn(state.value)); |
|
21834 } else if (state.status === 1) { |
|
21835 promise.resolve(state.value); |
|
21836 } else { |
|
21837 promise.reject(state.value); |
|
21838 } |
|
21839 } catch(e) { |
|
21840 promise.reject(e); |
|
21841 exceptionHandler(e); |
|
21842 } |
|
21843 } |
|
21844 } |
|
21845 |
|
21846 function scheduleProcessQueue(state) { |
|
21847 if (state.processScheduled || !state.pending) return; |
|
21848 state.processScheduled = true; |
|
21849 nextTick(function() { processQueue(state); }); |
|
21850 } |
|
21851 |
|
21852 function Deferred() { |
|
21853 this.promise = new Promise(); |
|
21854 //Necessary to support unbound execution :/ |
|
21855 this.resolve = simpleBind(this, this.resolve); |
|
21856 this.reject = simpleBind(this, this.reject); |
|
21857 this.notify = simpleBind(this, this.notify); |
|
21858 } |
|
21859 |
|
21860 Deferred.prototype = { |
|
21861 resolve: function(val) { |
|
21862 if (this.promise.$$state.status) return; |
|
21863 if (val === this.promise) { |
|
21864 this.$$reject($qMinErr( |
|
21865 'qcycle', |
|
21866 "Expected promise to be resolved with value other than itself '{0}'", |
|
21867 val)); |
|
21868 } |
|
21869 else { |
|
21870 this.$$resolve(val); |
|
21871 } |
|
21872 |
|
21873 }, |
|
21874 |
|
21875 $$resolve: function(val) { |
|
21876 var then, fns; |
|
21877 |
|
21878 fns = callOnce(this, this.$$resolve, this.$$reject); |
|
21879 try { |
|
21880 if ((isObject(val) || isFunction(val))) then = val && val.then; |
|
21881 if (isFunction(then)) { |
|
21882 this.promise.$$state.status = -1; |
|
21883 then.call(val, fns[0], fns[1], this.notify); |
|
21884 } else { |
|
21885 this.promise.$$state.value = val; |
|
21886 this.promise.$$state.status = 1; |
|
21887 scheduleProcessQueue(this.promise.$$state); |
|
21888 } |
|
21889 } catch(e) { |
|
21890 fns[1](e); |
|
21891 exceptionHandler(e); |
|
21892 } |
|
21893 }, |
|
21894 |
|
21895 reject: function(reason) { |
|
21896 if (this.promise.$$state.status) return; |
|
21897 this.$$reject(reason); |
|
21898 }, |
|
21899 |
|
21900 $$reject: function(reason) { |
|
21901 this.promise.$$state.value = reason; |
|
21902 this.promise.$$state.status = 2; |
|
21903 scheduleProcessQueue(this.promise.$$state); |
|
21904 }, |
|
21905 |
|
21906 notify: function(progress) { |
|
21907 var callbacks = this.promise.$$state.pending; |
|
21908 |
|
21909 if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) { |
|
21910 nextTick(function() { |
|
21911 var callback, result; |
|
21912 for (var i = 0, ii = callbacks.length; i < ii; i++) { |
|
21913 result = callbacks[i][0]; |
|
21914 callback = callbacks[i][3]; |
|
21915 try { |
|
21916 result.notify(isFunction(callback) ? callback(progress) : progress); |
|
21917 } catch(e) { |
|
21918 exceptionHandler(e); |
|
21919 } |
|
21920 } |
|
21921 }); |
|
21922 } |
|
21923 } |
|
21924 }; |
|
21925 |
|
21926 /** |
|
21927 * @ngdoc method |
|
21928 * @name $q#reject |
|
21929 * @kind function |
|
21930 * |
|
21931 * @description |
|
21932 * Creates a promise that is resolved as rejected with the specified `reason`. This api should be |
|
21933 * used to forward rejection in a chain of promises. If you are dealing with the last promise in |
|
21934 * a promise chain, you don't need to worry about it. |
|
21935 * |
|
21936 * When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of |
|
21937 * `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via |
|
21938 * a promise error callback and you want to forward the error to the promise derived from the |
|
21939 * current promise, you have to "rethrow" the error by returning a rejection constructed via |
|
21940 * `reject`. |
|
21941 * |
|
21942 * ```js |
|
21943 * promiseB = promiseA.then(function(result) { |
|
21944 * // success: do something and resolve promiseB |
|
21945 * // with the old or a new result |
|
21946 * return result; |
|
21947 * }, function(reason) { |
|
21948 * // error: handle the error if possible and |
|
21949 * // resolve promiseB with newPromiseOrValue, |
|
21950 * // otherwise forward the rejection to promiseB |
|
21951 * if (canHandle(reason)) { |
|
21952 * // handle the error and recover |
|
21953 * return newPromiseOrValue; |
|
21954 * } |
|
21955 * return $q.reject(reason); |
|
21956 * }); |
|
21957 * ``` |
|
21958 * |
|
21959 * @param {*} reason Constant, message, exception or an object representing the rejection reason. |
|
21960 * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. |
|
21961 */ |
|
21962 var reject = function(reason) { |
|
21963 var result = new Deferred(); |
|
21964 result.reject(reason); |
|
21965 return result.promise; |
|
21966 }; |
|
21967 |
|
21968 var makePromise = function makePromise(value, resolved) { |
|
21969 var result = new Deferred(); |
|
21970 if (resolved) { |
|
21971 result.resolve(value); |
|
21972 } else { |
|
21973 result.reject(value); |
|
21974 } |
|
21975 return result.promise; |
|
21976 }; |
|
21977 |
|
21978 var handleCallback = function handleCallback(value, isResolved, callback) { |
|
21979 var callbackOutput = null; |
|
21980 try { |
|
21981 if (isFunction(callback)) callbackOutput = callback(); |
|
21982 } catch(e) { |
|
21983 return makePromise(e, false); |
|
21984 } |
|
21985 if (isPromiseLike(callbackOutput)) { |
|
21986 return callbackOutput.then(function() { |
|
21987 return makePromise(value, isResolved); |
|
21988 }, function(error) { |
|
21989 return makePromise(error, false); |
|
21990 }); |
|
21991 } else { |
|
21992 return makePromise(value, isResolved); |
|
21993 } |
|
21994 }; |
|
21995 |
|
21996 /** |
|
21997 * @ngdoc method |
|
21998 * @name $q#when |
|
21999 * @kind function |
|
22000 * |
|
22001 * @description |
|
22002 * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. |
|
22003 * This is useful when you are dealing with an object that might or might not be a promise, or if |
|
22004 * the promise comes from a source that can't be trusted. |
|
22005 * |
|
22006 * @param {*} value Value or a promise |
|
22007 * @returns {Promise} Returns a promise of the passed value or promise |
|
22008 */ |
|
22009 |
|
22010 |
|
22011 var when = function(value, callback, errback, progressBack) { |
|
22012 var result = new Deferred(); |
|
22013 result.resolve(value); |
|
22014 return result.promise.then(callback, errback, progressBack); |
|
22015 }; |
|
22016 |
|
22017 /** |
|
22018 * @ngdoc method |
|
22019 * @name $q#all |
|
22020 * @kind function |
|
22021 * |
|
22022 * @description |
|
22023 * Combines multiple promises into a single promise that is resolved when all of the input |
|
22024 * promises are resolved. |
|
22025 * |
|
22026 * @param {Array.<Promise>|Object.<Promise>} promises An array or hash of promises. |
|
22027 * @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, |
|
22028 * each value corresponding to the promise at the same index/key in the `promises` array/hash. |
|
22029 * If any of the promises is resolved with a rejection, this resulting promise will be rejected |
|
22030 * with the same rejection value. |
|
22031 */ |
|
22032 |
|
22033 function all(promises) { |
|
22034 var deferred = new Deferred(), |
|
22035 counter = 0, |
|
22036 results = isArray(promises) ? [] : {}; |
|
22037 |
|
22038 forEach(promises, function(promise, key) { |
|
22039 counter++; |
|
22040 when(promise).then(function(value) { |
|
22041 if (results.hasOwnProperty(key)) return; |
|
22042 results[key] = value; |
|
22043 if (!(--counter)) deferred.resolve(results); |
|
22044 }, function(reason) { |
|
22045 if (results.hasOwnProperty(key)) return; |
|
22046 deferred.reject(reason); |
|
22047 }); |
|
22048 }); |
|
22049 |
|
22050 if (counter === 0) { |
|
22051 deferred.resolve(results); |
|
22052 } |
|
22053 |
|
22054 return deferred.promise; |
|
22055 } |
|
22056 |
|
22057 var $Q = function Q(resolver) { |
|
22058 if (!isFunction(resolver)) { |
|
22059 throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver); |
|
22060 } |
|
22061 |
|
22062 if (!(this instanceof Q)) { |
|
22063 // More useful when $Q is the Promise itself. |
|
22064 return new Q(resolver); |
|
22065 } |
|
22066 |
|
22067 var deferred = new Deferred(); |
|
22068 |
|
22069 function resolveFn(value) { |
|
22070 deferred.resolve(value); |
|
22071 } |
|
22072 |
|
22073 function rejectFn(reason) { |
|
22074 deferred.reject(reason); |
|
22075 } |
|
22076 |
|
22077 resolver(resolveFn, rejectFn); |
|
22078 |
|
22079 return deferred.promise; |
|
22080 }; |
|
22081 |
|
22082 $Q.defer = defer; |
|
22083 $Q.reject = reject; |
|
22084 $Q.when = when; |
|
22085 $Q.all = all; |
|
22086 |
|
22087 return $Q; |
|
22088 } |
|
22089 |
|
22090 function $$RAFProvider(){ //rAF |
|
22091 this.$get = ['$window', '$timeout', function($window, $timeout) { |
|
22092 var requestAnimationFrame = $window.requestAnimationFrame || |
|
22093 $window.webkitRequestAnimationFrame || |
|
22094 $window.mozRequestAnimationFrame; |
|
22095 |
|
22096 var cancelAnimationFrame = $window.cancelAnimationFrame || |
|
22097 $window.webkitCancelAnimationFrame || |
|
22098 $window.mozCancelAnimationFrame || |
|
22099 $window.webkitCancelRequestAnimationFrame; |
|
22100 |
|
22101 var rafSupported = !!requestAnimationFrame; |
|
22102 var raf = rafSupported |
|
22103 ? function(fn) { |
|
22104 var id = requestAnimationFrame(fn); |
|
22105 return function() { |
|
22106 cancelAnimationFrame(id); |
|
22107 }; |
|
22108 } |
|
22109 : function(fn) { |
|
22110 var timer = $timeout(fn, 16.66, false); // 1000 / 60 = 16.666 |
|
22111 return function() { |
|
22112 $timeout.cancel(timer); |
|
22113 }; |
|
22114 }; |
|
22115 |
|
22116 raf.supported = rafSupported; |
|
22117 |
|
22118 return raf; |
|
22119 }]; |
|
22120 } |
|
22121 |
|
22122 /** |
|
22123 * DESIGN NOTES |
|
22124 * |
|
22125 * The design decisions behind the scope are heavily favored for speed and memory consumption. |
|
22126 * |
|
22127 * The typical use of scope is to watch the expressions, which most of the time return the same |
|
22128 * value as last time so we optimize the operation. |
|
22129 * |
|
22130 * Closures construction is expensive in terms of speed as well as memory: |
|
22131 * - No closures, instead use prototypical inheritance for API |
|
22132 * - Internal state needs to be stored on scope directly, which means that private state is |
|
22133 * exposed as $$____ properties |
|
22134 * |
|
22135 * Loop operations are optimized by using while(count--) { ... } |
|
22136 * - this means that in order to keep the same order of execution as addition we have to add |
|
22137 * items to the array at the beginning (unshift) instead of at the end (push) |
|
22138 * |
|
22139 * Child scopes are created and removed often |
|
22140 * - Using an array would be slow since inserts in middle are expensive so we use linked list |
|
22141 * |
|
22142 * There are few watches then a lot of observers. This is why you don't want the observer to be |
|
22143 * implemented in the same way as watch. Watch requires return of initialization function which |
|
22144 * are expensive to construct. |
|
22145 */ |
|
22146 |
|
22147 |
|
22148 /** |
|
22149 * @ngdoc provider |
|
22150 * @name $rootScopeProvider |
|
22151 * @description |
|
22152 * |
|
22153 * Provider for the $rootScope service. |
|
22154 */ |
|
22155 |
|
22156 /** |
|
22157 * @ngdoc method |
|
22158 * @name $rootScopeProvider#digestTtl |
|
22159 * @description |
|
22160 * |
|
22161 * Sets the number of `$digest` iterations the scope should attempt to execute before giving up and |
|
22162 * assuming that the model is unstable. |
|
22163 * |
|
22164 * The current default is 10 iterations. |
|
22165 * |
|
22166 * In complex applications it's possible that the dependencies between `$watch`s will result in |
|
22167 * several digest iterations. However if an application needs more than the default 10 digest |
|
22168 * iterations for its model to stabilize then you should investigate what is causing the model to |
|
22169 * continuously change during the digest. |
|
22170 * |
|
22171 * Increasing the TTL could have performance implications, so you should not change it without |
|
22172 * proper justification. |
|
22173 * |
|
22174 * @param {number} limit The number of digest iterations. |
|
22175 */ |
|
22176 |
|
22177 |
|
22178 /** |
|
22179 * @ngdoc service |
|
22180 * @name $rootScope |
|
22181 * @description |
|
22182 * |
|
22183 * Every application has a single root {@link ng.$rootScope.Scope scope}. |
|
22184 * All other scopes are descendant scopes of the root scope. Scopes provide separation |
|
22185 * between the model and the view, via a mechanism for watching the model for changes. |
|
22186 * They also provide an event emission/broadcast and subscription facility. See the |
|
22187 * {@link guide/scope developer guide on scopes}. |
|
22188 */ |
|
22189 function $RootScopeProvider(){ |
|
22190 var TTL = 10; |
|
22191 var $rootScopeMinErr = minErr('$rootScope'); |
|
22192 var lastDirtyWatch = null; |
|
22193 var applyAsyncId = null; |
|
22194 |
|
22195 this.digestTtl = function(value) { |
|
22196 if (arguments.length) { |
|
22197 TTL = value; |
|
22198 } |
|
22199 return TTL; |
|
22200 }; |
|
22201 |
|
22202 this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', |
|
22203 function( $injector, $exceptionHandler, $parse, $browser) { |
|
22204 |
|
22205 /** |
|
22206 * @ngdoc type |
|
22207 * @name $rootScope.Scope |
|
22208 * |
|
22209 * @description |
|
22210 * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the |
|
22211 * {@link auto.$injector $injector}. Child scopes are created using the |
|
22212 * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when |
|
22213 * compiled HTML template is executed.) |
|
22214 * |
|
22215 * Here is a simple scope snippet to show how you can interact with the scope. |
|
22216 * ```html |
|
22217 * <file src="./test/ng/rootScopeSpec.js" tag="docs1" /> |
|
22218 * ``` |
|
22219 * |
|
22220 * # Inheritance |
|
22221 * A scope can inherit from a parent scope, as in this example: |
|
22222 * ```js |
|
22223 var parent = $rootScope; |
|
22224 var child = parent.$new(); |
|
22225 |
|
22226 parent.salutation = "Hello"; |
|
22227 child.name = "World"; |
|
22228 expect(child.salutation).toEqual('Hello'); |
|
22229 |
|
22230 child.salutation = "Welcome"; |
|
22231 expect(child.salutation).toEqual('Welcome'); |
|
22232 expect(parent.salutation).toEqual('Hello'); |
|
22233 * ``` |
|
22234 * |
|
22235 * |
|
22236 * @param {Object.<string, function()>=} providers Map of service factory which need to be |
|
22237 * provided for the current scope. Defaults to {@link ng}. |
|
22238 * @param {Object.<string, *>=} instanceCache Provides pre-instantiated services which should |
|
22239 * append/override services provided by `providers`. This is handy |
|
22240 * when unit-testing and having the need to override a default |
|
22241 * service. |
|
22242 * @returns {Object} Newly created scope. |
|
22243 * |
|
22244 */ |
|
22245 function Scope() { |
|
22246 this.$id = nextUid(); |
|
22247 this.$$phase = this.$parent = this.$$watchers = |
|
22248 this.$$nextSibling = this.$$prevSibling = |
|
22249 this.$$childHead = this.$$childTail = null; |
|
22250 this.$root = this; |
|
22251 this.$$destroyed = false; |
|
22252 this.$$listeners = {}; |
|
22253 this.$$listenerCount = {}; |
|
22254 this.$$isolateBindings = null; |
|
22255 } |
|
22256 |
|
22257 /** |
|
22258 * @ngdoc property |
|
22259 * @name $rootScope.Scope#$id |
|
22260 * |
|
22261 * @description |
|
22262 * Unique scope ID (monotonically increasing) useful for debugging. |
|
22263 */ |
|
22264 |
|
22265 /** |
|
22266 * @ngdoc property |
|
22267 * @name $rootScope.Scope#$parent |
|
22268 * |
|
22269 * @description |
|
22270 * Reference to the parent scope. |
|
22271 */ |
|
22272 |
|
22273 /** |
|
22274 * @ngdoc property |
|
22275 * @name $rootScope.Scope#$root |
|
22276 * |
|
22277 * @description |
|
22278 * Reference to the root scope. |
|
22279 */ |
|
22280 |
|
22281 Scope.prototype = { |
|
22282 constructor: Scope, |
|
22283 /** |
|
22284 * @ngdoc method |
|
22285 * @name $rootScope.Scope#$new |
|
22286 * @kind function |
|
22287 * |
|
22288 * @description |
|
22289 * Creates a new child {@link ng.$rootScope.Scope scope}. |
|
22290 * |
|
22291 * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} event. |
|
22292 * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. |
|
22293 * |
|
22294 * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is |
|
22295 * desired for the scope and its child scopes to be permanently detached from the parent and |
|
22296 * thus stop participating in model change detection and listener notification by invoking. |
|
22297 * |
|
22298 * @param {boolean} isolate If true, then the scope does not prototypically inherit from the |
|
22299 * parent scope. The scope is isolated, as it can not see parent scope properties. |
|
22300 * When creating widgets, it is useful for the widget to not accidentally read parent |
|
22301 * state. |
|
22302 * |
|
22303 * @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent` |
|
22304 * of the newly created scope. Defaults to `this` scope if not provided. |
|
22305 * This is used when creating a transclude scope to correctly place it |
|
22306 * in the scope hierarchy while maintaining the correct prototypical |
|
22307 * inheritance. |
|
22308 * |
|
22309 * @returns {Object} The newly created child scope. |
|
22310 * |
|
22311 */ |
|
22312 $new: function(isolate, parent) { |
|
22313 var child; |
|
22314 |
|
22315 parent = parent || this; |
|
22316 |
|
22317 if (isolate) { |
|
22318 child = new Scope(); |
|
22319 child.$root = this.$root; |
|
22320 } else { |
|
22321 // Only create a child scope class if somebody asks for one, |
|
22322 // but cache it to allow the VM to optimize lookups. |
|
22323 if (!this.$$ChildScope) { |
|
22324 this.$$ChildScope = function ChildScope() { |
|
22325 this.$$watchers = this.$$nextSibling = |
|
22326 this.$$childHead = this.$$childTail = null; |
|
22327 this.$$listeners = {}; |
|
22328 this.$$listenerCount = {}; |
|
22329 this.$id = nextUid(); |
|
22330 this.$$ChildScope = null; |
|
22331 }; |
|
22332 this.$$ChildScope.prototype = this; |
|
22333 } |
|
22334 child = new this.$$ChildScope(); |
|
22335 } |
|
22336 child.$parent = parent; |
|
22337 child.$$prevSibling = parent.$$childTail; |
|
22338 if (parent.$$childHead) { |
|
22339 parent.$$childTail.$$nextSibling = child; |
|
22340 parent.$$childTail = child; |
|
22341 } else { |
|
22342 parent.$$childHead = parent.$$childTail = child; |
|
22343 } |
|
22344 |
|
22345 // When the new scope is not isolated or we inherit from `this`, and |
|
22346 // the parent scope is destroyed, the property `$$destroyed` is inherited |
|
22347 // prototypically. In all other cases, this property needs to be set |
|
22348 // when the parent scope is destroyed. |
|
22349 // The listener needs to be added after the parent is set |
|
22350 if (isolate || parent != this) child.$on('$destroy', destroyChild); |
|
22351 |
|
22352 return child; |
|
22353 |
|
22354 function destroyChild() { |
|
22355 child.$$destroyed = true; |
|
22356 } |
|
22357 }, |
|
22358 |
|
22359 /** |
|
22360 * @ngdoc method |
|
22361 * @name $rootScope.Scope#$watch |
|
22362 * @kind function |
|
22363 * |
|
22364 * @description |
|
22365 * Registers a `listener` callback to be executed whenever the `watchExpression` changes. |
|
22366 * |
|
22367 * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest |
|
22368 * $digest()} and should return the value that will be watched. (Since |
|
22369 * {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the |
|
22370 * `watchExpression` can execute multiple times per |
|
22371 * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) |
|
22372 * - The `listener` is called only when the value from the current `watchExpression` and the |
|
22373 * previous call to `watchExpression` are not equal (with the exception of the initial run, |
|
22374 * see below). Inequality is determined according to reference inequality, |
|
22375 * [strict comparison](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators) |
|
22376 * via the `!==` Javascript operator, unless `objectEquality == true` |
|
22377 * (see next point) |
|
22378 * - When `objectEquality == true`, inequality of the `watchExpression` is determined |
|
22379 * according to the {@link angular.equals} function. To save the value of the object for |
|
22380 * later comparison, the {@link angular.copy} function is used. This therefore means that |
|
22381 * watching complex objects will have adverse memory and performance implications. |
|
22382 * - The watch `listener` may change the model, which may trigger other `listener`s to fire. |
|
22383 * This is achieved by rerunning the watchers until no changes are detected. The rerun |
|
22384 * iteration limit is 10 to prevent an infinite loop deadlock. |
|
22385 * |
|
22386 * |
|
22387 * If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called, |
|
22388 * you can register a `watchExpression` function with no `listener`. (Since `watchExpression` |
|
22389 * can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a |
|
22390 * change is detected, be prepared for multiple calls to your listener.) |
|
22391 * |
|
22392 * After a watcher is registered with the scope, the `listener` fn is called asynchronously |
|
22393 * (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the |
|
22394 * watcher. In rare cases, this is undesirable because the listener is called when the result |
|
22395 * of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you |
|
22396 * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the |
|
22397 * listener was called due to initialization. |
|
22398 * |
|
22399 * |
|
22400 * |
|
22401 * # Example |
|
22402 * ```js |
|
22403 // let's assume that scope was dependency injected as the $rootScope |
|
22404 var scope = $rootScope; |
|
22405 scope.name = 'misko'; |
|
22406 scope.counter = 0; |
|
22407 |
|
22408 expect(scope.counter).toEqual(0); |
|
22409 scope.$watch('name', function(newValue, oldValue) { |
|
22410 scope.counter = scope.counter + 1; |
|
22411 }); |
|
22412 expect(scope.counter).toEqual(0); |
|
22413 |
|
22414 scope.$digest(); |
|
22415 // the listener is always called during the first $digest loop after it was registered |
|
22416 expect(scope.counter).toEqual(1); |
|
22417 |
|
22418 scope.$digest(); |
|
22419 // but now it will not be called unless the value changes |
|
22420 expect(scope.counter).toEqual(1); |
|
22421 |
|
22422 scope.name = 'adam'; |
|
22423 scope.$digest(); |
|
22424 expect(scope.counter).toEqual(2); |
|
22425 |
|
22426 |
|
22427 |
|
22428 // Using a function as a watchExpression |
|
22429 var food; |
|
22430 scope.foodCounter = 0; |
|
22431 expect(scope.foodCounter).toEqual(0); |
|
22432 scope.$watch( |
|
22433 // This function returns the value being watched. It is called for each turn of the $digest loop |
|
22434 function() { return food; }, |
|
22435 // This is the change listener, called when the value returned from the above function changes |
|
22436 function(newValue, oldValue) { |
|
22437 if ( newValue !== oldValue ) { |
|
22438 // Only increment the counter if the value changed |
|
22439 scope.foodCounter = scope.foodCounter + 1; |
|
22440 } |
|
22441 } |
|
22442 ); |
|
22443 // No digest has been run so the counter will be zero |
|
22444 expect(scope.foodCounter).toEqual(0); |
|
22445 |
|
22446 // Run the digest but since food has not changed count will still be zero |
|
22447 scope.$digest(); |
|
22448 expect(scope.foodCounter).toEqual(0); |
|
22449 |
|
22450 // Update food and run digest. Now the counter will increment |
|
22451 food = 'cheeseburger'; |
|
22452 scope.$digest(); |
|
22453 expect(scope.foodCounter).toEqual(1); |
|
22454 |
|
22455 * ``` |
|
22456 * |
|
22457 * |
|
22458 * |
|
22459 * @param {(function()|string)} watchExpression Expression that is evaluated on each |
|
22460 * {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers |
|
22461 * a call to the `listener`. |
|
22462 * |
|
22463 * - `string`: Evaluated as {@link guide/expression expression} |
|
22464 * - `function(scope)`: called with current `scope` as a parameter. |
|
22465 * @param {function(newVal, oldVal, scope)} listener Callback called whenever the value |
|
22466 * of `watchExpression` changes. |
|
22467 * |
|
22468 * - `newVal` contains the current value of the `watchExpression` |
|
22469 * - `oldVal` contains the previous value of the `watchExpression` |
|
22470 * - `scope` refers to the current scope |
|
22471 * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of |
|
22472 * comparing for reference equality. |
|
22473 * @returns {function()} Returns a deregistration function for this listener. |
|
22474 */ |
|
22475 $watch: function(watchExp, listener, objectEquality) { |
|
22476 var get = $parse(watchExp); |
|
22477 |
|
22478 if (get.$$watchDelegate) { |
|
22479 return get.$$watchDelegate(this, listener, objectEquality, get); |
|
22480 } |
|
22481 var scope = this, |
|
22482 array = scope.$$watchers, |
|
22483 watcher = { |
|
22484 fn: listener, |
|
22485 last: initWatchVal, |
|
22486 get: get, |
|
22487 exp: watchExp, |
|
22488 eq: !!objectEquality |
|
22489 }; |
|
22490 |
|
22491 lastDirtyWatch = null; |
|
22492 |
|
22493 if (!isFunction(listener)) { |
|
22494 watcher.fn = noop; |
|
22495 } |
|
22496 |
|
22497 if (!array) { |
|
22498 array = scope.$$watchers = []; |
|
22499 } |
|
22500 // we use unshift since we use a while loop in $digest for speed. |
|
22501 // the while loop reads in reverse order. |
|
22502 array.unshift(watcher); |
|
22503 |
|
22504 return function deregisterWatch() { |
|
22505 arrayRemove(array, watcher); |
|
22506 lastDirtyWatch = null; |
|
22507 }; |
|
22508 }, |
|
22509 |
|
22510 /** |
|
22511 * @ngdoc method |
|
22512 * @name $rootScope.Scope#$watchGroup |
|
22513 * @kind function |
|
22514 * |
|
22515 * @description |
|
22516 * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. |
|
22517 * If any one expression in the collection changes the `listener` is executed. |
|
22518 * |
|
22519 * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every |
|
22520 * call to $digest() to see if any items changes. |
|
22521 * - The `listener` is called whenever any expression in the `watchExpressions` array changes. |
|
22522 * |
|
22523 * @param {Array.<string|Function(scope)>} watchExpressions Array of expressions that will be individually |
|
22524 * watched using {@link ng.$rootScope.Scope#$watch $watch()} |
|
22525 * |
|
22526 * @param {function(newValues, oldValues, scope)} listener Callback called whenever the return value of any |
|
22527 * expression in `watchExpressions` changes |
|
22528 * The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching |
|
22529 * those of `watchExpression` |
|
22530 * and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching |
|
22531 * those of `watchExpression` |
|
22532 * The `scope` refers to the current scope. |
|
22533 * @returns {function()} Returns a de-registration function for all listeners. |
|
22534 */ |
|
22535 $watchGroup: function(watchExpressions, listener) { |
|
22536 var oldValues = new Array(watchExpressions.length); |
|
22537 var newValues = new Array(watchExpressions.length); |
|
22538 var deregisterFns = []; |
|
22539 var self = this; |
|
22540 var changeReactionScheduled = false; |
|
22541 var firstRun = true; |
|
22542 |
|
22543 if (!watchExpressions.length) { |
|
22544 // No expressions means we call the listener ASAP |
|
22545 var shouldCall = true; |
|
22546 self.$evalAsync(function () { |
|
22547 if (shouldCall) listener(newValues, newValues, self); |
|
22548 }); |
|
22549 return function deregisterWatchGroup() { |
|
22550 shouldCall = false; |
|
22551 }; |
|
22552 } |
|
22553 |
|
22554 if (watchExpressions.length === 1) { |
|
22555 // Special case size of one |
|
22556 return this.$watch(watchExpressions[0], function watchGroupAction(value, oldValue, scope) { |
|
22557 newValues[0] = value; |
|
22558 oldValues[0] = oldValue; |
|
22559 listener(newValues, (value === oldValue) ? newValues : oldValues, scope); |
|
22560 }); |
|
22561 } |
|
22562 |
|
22563 forEach(watchExpressions, function (expr, i) { |
|
22564 var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) { |
|
22565 newValues[i] = value; |
|
22566 oldValues[i] = oldValue; |
|
22567 if (!changeReactionScheduled) { |
|
22568 changeReactionScheduled = true; |
|
22569 self.$evalAsync(watchGroupAction); |
|
22570 } |
|
22571 }); |
|
22572 deregisterFns.push(unwatchFn); |
|
22573 }); |
|
22574 |
|
22575 function watchGroupAction() { |
|
22576 changeReactionScheduled = false; |
|
22577 |
|
22578 if (firstRun) { |
|
22579 firstRun = false; |
|
22580 listener(newValues, newValues, self); |
|
22581 } else { |
|
22582 listener(newValues, oldValues, self); |
|
22583 } |
|
22584 } |
|
22585 |
|
22586 return function deregisterWatchGroup() { |
|
22587 while (deregisterFns.length) { |
|
22588 deregisterFns.shift()(); |
|
22589 } |
|
22590 }; |
|
22591 }, |
|
22592 |
|
22593 |
|
22594 /** |
|
22595 * @ngdoc method |
|
22596 * @name $rootScope.Scope#$watchCollection |
|
22597 * @kind function |
|
22598 * |
|
22599 * @description |
|
22600 * Shallow watches the properties of an object and fires whenever any of the properties change |
|
22601 * (for arrays, this implies watching the array items; for object maps, this implies watching |
|
22602 * the properties). If a change is detected, the `listener` callback is fired. |
|
22603 * |
|
22604 * - The `obj` collection is observed via standard $watch operation and is examined on every |
|
22605 * call to $digest() to see if any items have been added, removed, or moved. |
|
22606 * - The `listener` is called whenever anything within the `obj` has changed. Examples include |
|
22607 * adding, removing, and moving items belonging to an object or array. |
|
22608 * |
|
22609 * |
|
22610 * # Example |
|
22611 * ```js |
|
22612 $scope.names = ['igor', 'matias', 'misko', 'james']; |
|
22613 $scope.dataCount = 4; |
|
22614 |
|
22615 $scope.$watchCollection('names', function(newNames, oldNames) { |
|
22616 $scope.dataCount = newNames.length; |
|
22617 }); |
|
22618 |
|
22619 expect($scope.dataCount).toEqual(4); |
|
22620 $scope.$digest(); |
|
22621 |
|
22622 //still at 4 ... no changes |
|
22623 expect($scope.dataCount).toEqual(4); |
|
22624 |
|
22625 $scope.names.pop(); |
|
22626 $scope.$digest(); |
|
22627 |
|
22628 //now there's been a change |
|
22629 expect($scope.dataCount).toEqual(3); |
|
22630 * ``` |
|
22631 * |
|
22632 * |
|
22633 * @param {string|function(scope)} obj Evaluated as {@link guide/expression expression}. The |
|
22634 * expression value should evaluate to an object or an array which is observed on each |
|
22635 * {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the |
|
22636 * collection will trigger a call to the `listener`. |
|
22637 * |
|
22638 * @param {function(newCollection, oldCollection, scope)} listener a callback function called |
|
22639 * when a change is detected. |
|
22640 * - The `newCollection` object is the newly modified data obtained from the `obj` expression |
|
22641 * - The `oldCollection` object is a copy of the former collection data. |
|
22642 * Due to performance considerations, the`oldCollection` value is computed only if the |
|
22643 * `listener` function declares two or more arguments. |
|
22644 * - The `scope` argument refers to the current scope. |
|
22645 * |
|
22646 * @returns {function()} Returns a de-registration function for this listener. When the |
|
22647 * de-registration function is executed, the internal watch operation is terminated. |
|
22648 */ |
|
22649 $watchCollection: function(obj, listener) { |
|
22650 $watchCollectionInterceptor.$stateful = true; |
|
22651 |
|
22652 var self = this; |
|
22653 // the current value, updated on each dirty-check run |
|
22654 var newValue; |
|
22655 // a shallow copy of the newValue from the last dirty-check run, |
|
22656 // updated to match newValue during dirty-check run |
|
22657 var oldValue; |
|
22658 // a shallow copy of the newValue from when the last change happened |
|
22659 var veryOldValue; |
|
22660 // only track veryOldValue if the listener is asking for it |
|
22661 var trackVeryOldValue = (listener.length > 1); |
|
22662 var changeDetected = 0; |
|
22663 var changeDetector = $parse(obj, $watchCollectionInterceptor); |
|
22664 var internalArray = []; |
|
22665 var internalObject = {}; |
|
22666 var initRun = true; |
|
22667 var oldLength = 0; |
|
22668 |
|
22669 function $watchCollectionInterceptor(_value) { |
|
22670 newValue = _value; |
|
22671 var newLength, key, bothNaN, newItem, oldItem; |
|
22672 |
|
22673 if (!isObject(newValue)) { // if primitive |
|
22674 if (oldValue !== newValue) { |
|
22675 oldValue = newValue; |
|
22676 changeDetected++; |
|
22677 } |
|
22678 } else if (isArrayLike(newValue)) { |
|
22679 if (oldValue !== internalArray) { |
|
22680 // we are transitioning from something which was not an array into array. |
|
22681 oldValue = internalArray; |
|
22682 oldLength = oldValue.length = 0; |
|
22683 changeDetected++; |
|
22684 } |
|
22685 |
|
22686 newLength = newValue.length; |
|
22687 |
|
22688 if (oldLength !== newLength) { |
|
22689 // if lengths do not match we need to trigger change notification |
|
22690 changeDetected++; |
|
22691 oldValue.length = oldLength = newLength; |
|
22692 } |
|
22693 // copy the items to oldValue and look for changes. |
|
22694 for (var i = 0; i < newLength; i++) { |
|
22695 oldItem = oldValue[i]; |
|
22696 newItem = newValue[i]; |
|
22697 |
|
22698 bothNaN = (oldItem !== oldItem) && (newItem !== newItem); |
|
22699 if (!bothNaN && (oldItem !== newItem)) { |
|
22700 changeDetected++; |
|
22701 oldValue[i] = newItem; |
|
22702 } |
|
22703 } |
|
22704 } else { |
|
22705 if (oldValue !== internalObject) { |
|
22706 // we are transitioning from something which was not an object into object. |
|
22707 oldValue = internalObject = {}; |
|
22708 oldLength = 0; |
|
22709 changeDetected++; |
|
22710 } |
|
22711 // copy the items to oldValue and look for changes. |
|
22712 newLength = 0; |
|
22713 for (key in newValue) { |
|
22714 if (newValue.hasOwnProperty(key)) { |
|
22715 newLength++; |
|
22716 newItem = newValue[key]; |
|
22717 oldItem = oldValue[key]; |
|
22718 |
|
22719 if (key in oldValue) { |
|
22720 bothNaN = (oldItem !== oldItem) && (newItem !== newItem); |
|
22721 if (!bothNaN && (oldItem !== newItem)) { |
|
22722 changeDetected++; |
|
22723 oldValue[key] = newItem; |
|
22724 } |
|
22725 } else { |
|
22726 oldLength++; |
|
22727 oldValue[key] = newItem; |
|
22728 changeDetected++; |
|
22729 } |
|
22730 } |
|
22731 } |
|
22732 if (oldLength > newLength) { |
|
22733 // we used to have more keys, need to find them and destroy them. |
|
22734 changeDetected++; |
|
22735 for(key in oldValue) { |
|
22736 if (!newValue.hasOwnProperty(key)) { |
|
22737 oldLength--; |
|
22738 delete oldValue[key]; |
|
22739 } |
|
22740 } |
|
22741 } |
|
22742 } |
|
22743 return changeDetected; |
|
22744 } |
|
22745 |
|
22746 function $watchCollectionAction() { |
|
22747 if (initRun) { |
|
22748 initRun = false; |
|
22749 listener(newValue, newValue, self); |
|
22750 } else { |
|
22751 listener(newValue, veryOldValue, self); |
|
22752 } |
|
22753 |
|
22754 // make a copy for the next time a collection is changed |
|
22755 if (trackVeryOldValue) { |
|
22756 if (!isObject(newValue)) { |
|
22757 //primitive |
|
22758 veryOldValue = newValue; |
|
22759 } else if (isArrayLike(newValue)) { |
|
22760 veryOldValue = new Array(newValue.length); |
|
22761 for (var i = 0; i < newValue.length; i++) { |
|
22762 veryOldValue[i] = newValue[i]; |
|
22763 } |
|
22764 } else { // if object |
|
22765 veryOldValue = {}; |
|
22766 for (var key in newValue) { |
|
22767 if (hasOwnProperty.call(newValue, key)) { |
|
22768 veryOldValue[key] = newValue[key]; |
|
22769 } |
|
22770 } |
|
22771 } |
|
22772 } |
|
22773 } |
|
22774 |
|
22775 return this.$watch(changeDetector, $watchCollectionAction); |
|
22776 }, |
|
22777 |
|
22778 /** |
|
22779 * @ngdoc method |
|
22780 * @name $rootScope.Scope#$digest |
|
22781 * @kind function |
|
22782 * |
|
22783 * @description |
|
22784 * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and |
|
22785 * its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change |
|
22786 * the model, the `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} |
|
22787 * until no more listeners are firing. This means that it is possible to get into an infinite |
|
22788 * loop. This function will throw `'Maximum iteration limit exceeded.'` if the number of |
|
22789 * iterations exceeds 10. |
|
22790 * |
|
22791 * Usually, you don't call `$digest()` directly in |
|
22792 * {@link ng.directive:ngController controllers} or in |
|
22793 * {@link ng.$compileProvider#directive directives}. |
|
22794 * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within |
|
22795 * a {@link ng.$compileProvider#directive directive}), which will force a `$digest()`. |
|
22796 * |
|
22797 * If you want to be notified whenever `$digest()` is called, |
|
22798 * you can register a `watchExpression` function with |
|
22799 * {@link ng.$rootScope.Scope#$watch $watch()} with no `listener`. |
|
22800 * |
|
22801 * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. |
|
22802 * |
|
22803 * # Example |
|
22804 * ```js |
|
22805 var scope = ...; |
|
22806 scope.name = 'misko'; |
|
22807 scope.counter = 0; |
|
22808 |
|
22809 expect(scope.counter).toEqual(0); |
|
22810 scope.$watch('name', function(newValue, oldValue) { |
|
22811 scope.counter = scope.counter + 1; |
|
22812 }); |
|
22813 expect(scope.counter).toEqual(0); |
|
22814 |
|
22815 scope.$digest(); |
|
22816 // the listener is always called during the first $digest loop after it was registered |
|
22817 expect(scope.counter).toEqual(1); |
|
22818 |
|
22819 scope.$digest(); |
|
22820 // but now it will not be called unless the value changes |
|
22821 expect(scope.counter).toEqual(1); |
|
22822 |
|
22823 scope.name = 'adam'; |
|
22824 scope.$digest(); |
|
22825 expect(scope.counter).toEqual(2); |
|
22826 * ``` |
|
22827 * |
|
22828 */ |
|
22829 $digest: function() { |
|
22830 var watch, value, last, |
|
22831 watchers, |
|
22832 length, |
|
22833 dirty, ttl = TTL, |
|
22834 next, current, target = this, |
|
22835 watchLog = [], |
|
22836 logIdx, logMsg, asyncTask; |
|
22837 |
|
22838 beginPhase('$digest'); |
|
22839 // Check for changes to browser url that happened in sync before the call to $digest |
|
22840 $browser.$$checkUrlChange(); |
|
22841 |
|
22842 if (this === $rootScope && applyAsyncId !== null) { |
|
22843 // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then |
|
22844 // cancel the scheduled $apply and flush the queue of expressions to be evaluated. |
|
22845 $browser.defer.cancel(applyAsyncId); |
|
22846 flushApplyAsync(); |
|
22847 } |
|
22848 |
|
22849 lastDirtyWatch = null; |
|
22850 |
|
22851 do { // "while dirty" loop |
|
22852 dirty = false; |
|
22853 current = target; |
|
22854 |
|
22855 while(asyncQueue.length) { |
|
22856 try { |
|
22857 asyncTask = asyncQueue.shift(); |
|
22858 asyncTask.scope.$eval(asyncTask.expression); |
|
22859 } catch (e) { |
|
22860 $exceptionHandler(e); |
|
22861 } |
|
22862 lastDirtyWatch = null; |
|
22863 } |
|
22864 |
|
22865 traverseScopesLoop: |
|
22866 do { // "traverse the scopes" loop |
|
22867 if ((watchers = current.$$watchers)) { |
|
22868 // process our watches |
|
22869 length = watchers.length; |
|
22870 while (length--) { |
|
22871 try { |
|
22872 watch = watchers[length]; |
|
22873 // Most common watches are on primitives, in which case we can short |
|
22874 // circuit it with === operator, only when === fails do we use .equals |
|
22875 if (watch) { |
|
22876 if ((value = watch.get(current)) !== (last = watch.last) && |
|
22877 !(watch.eq |
|
22878 ? equals(value, last) |
|
22879 : (typeof value === 'number' && typeof last === 'number' |
|
22880 && isNaN(value) && isNaN(last)))) { |
|
22881 dirty = true; |
|
22882 lastDirtyWatch = watch; |
|
22883 watch.last = watch.eq ? copy(value, null) : value; |
|
22884 watch.fn(value, ((last === initWatchVal) ? value : last), current); |
|
22885 if (ttl < 5) { |
|
22886 logIdx = 4 - ttl; |
|
22887 if (!watchLog[logIdx]) watchLog[logIdx] = []; |
|
22888 logMsg = (isFunction(watch.exp)) |
|
22889 ? 'fn: ' + (watch.exp.name || watch.exp.toString()) |
|
22890 : watch.exp; |
|
22891 logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); |
|
22892 watchLog[logIdx].push(logMsg); |
|
22893 } |
|
22894 } else if (watch === lastDirtyWatch) { |
|
22895 // If the most recently dirty watcher is now clean, short circuit since the remaining watchers |
|
22896 // have already been tested. |
|
22897 dirty = false; |
|
22898 break traverseScopesLoop; |
|
22899 } |
|
22900 } |
|
22901 } catch (e) { |
|
22902 $exceptionHandler(e); |
|
22903 } |
|
22904 } |
|
22905 } |
|
22906 |
|
22907 // Insanity Warning: scope depth-first traversal |
|
22908 // yes, this code is a bit crazy, but it works and we have tests to prove it! |
|
22909 // this piece should be kept in sync with the traversal in $broadcast |
|
22910 if (!(next = (current.$$childHead || |
|
22911 (current !== target && current.$$nextSibling)))) { |
|
22912 while(current !== target && !(next = current.$$nextSibling)) { |
|
22913 current = current.$parent; |
|
22914 } |
|
22915 } |
|
22916 } while ((current = next)); |
|
22917 |
|
22918 // `break traverseScopesLoop;` takes us to here |
|
22919 |
|
22920 if((dirty || asyncQueue.length) && !(ttl--)) { |
|
22921 clearPhase(); |
|
22922 throw $rootScopeMinErr('infdig', |
|
22923 '{0} $digest() iterations reached. Aborting!\n' + |
|
22924 'Watchers fired in the last 5 iterations: {1}', |
|
22925 TTL, toJson(watchLog)); |
|
22926 } |
|
22927 |
|
22928 } while (dirty || asyncQueue.length); |
|
22929 |
|
22930 clearPhase(); |
|
22931 |
|
22932 while(postDigestQueue.length) { |
|
22933 try { |
|
22934 postDigestQueue.shift()(); |
|
22935 } catch (e) { |
|
22936 $exceptionHandler(e); |
|
22937 } |
|
22938 } |
|
22939 }, |
|
22940 |
|
22941 |
|
22942 /** |
|
22943 * @ngdoc event |
|
22944 * @name $rootScope.Scope#$destroy |
|
22945 * @eventType broadcast on scope being destroyed |
|
22946 * |
|
22947 * @description |
|
22948 * Broadcasted when a scope and its children are being destroyed. |
|
22949 * |
|
22950 * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to |
|
22951 * clean up DOM bindings before an element is removed from the DOM. |
|
22952 */ |
|
22953 |
|
22954 /** |
|
22955 * @ngdoc method |
|
22956 * @name $rootScope.Scope#$destroy |
|
22957 * @kind function |
|
22958 * |
|
22959 * @description |
|
22960 * Removes the current scope (and all of its children) from the parent scope. Removal implies |
|
22961 * that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer |
|
22962 * propagate to the current scope and its children. Removal also implies that the current |
|
22963 * scope is eligible for garbage collection. |
|
22964 * |
|
22965 * The `$destroy()` is usually used by directives such as |
|
22966 * {@link ng.directive:ngRepeat ngRepeat} for managing the |
|
22967 * unrolling of the loop. |
|
22968 * |
|
22969 * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. |
|
22970 * Application code can register a `$destroy` event handler that will give it a chance to |
|
22971 * perform any necessary cleanup. |
|
22972 * |
|
22973 * Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to |
|
22974 * clean up DOM bindings before an element is removed from the DOM. |
|
22975 */ |
|
22976 $destroy: function() { |
|
22977 // we can't destroy the root scope or a scope that has been already destroyed |
|
22978 if (this.$$destroyed) return; |
|
22979 var parent = this.$parent; |
|
22980 |
|
22981 this.$broadcast('$destroy'); |
|
22982 this.$$destroyed = true; |
|
22983 if (this === $rootScope) return; |
|
22984 |
|
22985 for (var eventName in this.$$listenerCount) { |
|
22986 decrementListenerCount(this, this.$$listenerCount[eventName], eventName); |
|
22987 } |
|
22988 |
|
22989 // sever all the references to parent scopes (after this cleanup, the current scope should |
|
22990 // not be retained by any of our references and should be eligible for garbage collection) |
|
22991 if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; |
|
22992 if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; |
|
22993 if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; |
|
22994 if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; |
|
22995 |
|
22996 // Disable listeners, watchers and apply/digest methods |
|
22997 this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop; |
|
22998 this.$on = this.$watch = this.$watchGroup = function() { return noop; }; |
|
22999 this.$$listeners = {}; |
|
23000 |
|
23001 // All of the code below is bogus code that works around V8's memory leak via optimized code |
|
23002 // and inline caches. |
|
23003 // |
|
23004 // see: |
|
23005 // - https://code.google.com/p/v8/issues/detail?id=2073#c26 |
|
23006 // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 |
|
23007 // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 |
|
23008 |
|
23009 this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = |
|
23010 this.$$childTail = this.$root = this.$$watchers = null; |
|
23011 }, |
|
23012 |
|
23013 /** |
|
23014 * @ngdoc method |
|
23015 * @name $rootScope.Scope#$eval |
|
23016 * @kind function |
|
23017 * |
|
23018 * @description |
|
23019 * Executes the `expression` on the current scope and returns the result. Any exceptions in |
|
23020 * the expression are propagated (uncaught). This is useful when evaluating Angular |
|
23021 * expressions. |
|
23022 * |
|
23023 * # Example |
|
23024 * ```js |
|
23025 var scope = ng.$rootScope.Scope(); |
|
23026 scope.a = 1; |
|
23027 scope.b = 2; |
|
23028 |
|
23029 expect(scope.$eval('a+b')).toEqual(3); |
|
23030 expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3); |
|
23031 * ``` |
|
23032 * |
|
23033 * @param {(string|function())=} expression An angular expression to be executed. |
|
23034 * |
|
23035 * - `string`: execute using the rules as defined in {@link guide/expression expression}. |
|
23036 * - `function(scope)`: execute the function with the current `scope` parameter. |
|
23037 * |
|
23038 * @param {(object)=} locals Local variables object, useful for overriding values in scope. |
|
23039 * @returns {*} The result of evaluating the expression. |
|
23040 */ |
|
23041 $eval: function(expr, locals) { |
|
23042 return $parse(expr)(this, locals); |
|
23043 }, |
|
23044 |
|
23045 /** |
|
23046 * @ngdoc method |
|
23047 * @name $rootScope.Scope#$evalAsync |
|
23048 * @kind function |
|
23049 * |
|
23050 * @description |
|
23051 * Executes the expression on the current scope at a later point in time. |
|
23052 * |
|
23053 * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only |
|
23054 * that: |
|
23055 * |
|
23056 * - it will execute after the function that scheduled the evaluation (preferably before DOM |
|
23057 * rendering). |
|
23058 * - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after |
|
23059 * `expression` execution. |
|
23060 * |
|
23061 * Any exceptions from the execution of the expression are forwarded to the |
|
23062 * {@link ng.$exceptionHandler $exceptionHandler} service. |
|
23063 * |
|
23064 * __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle |
|
23065 * will be scheduled. However, it is encouraged to always call code that changes the model |
|
23066 * from within an `$apply` call. That includes code evaluated via `$evalAsync`. |
|
23067 * |
|
23068 * @param {(string|function())=} expression An angular expression to be executed. |
|
23069 * |
|
23070 * - `string`: execute using the rules as defined in {@link guide/expression expression}. |
|
23071 * - `function(scope)`: execute the function with the current `scope` parameter. |
|
23072 * |
|
23073 */ |
|
23074 $evalAsync: function(expr) { |
|
23075 // if we are outside of an $digest loop and this is the first time we are scheduling async |
|
23076 // task also schedule async auto-flush |
|
23077 if (!$rootScope.$$phase && !asyncQueue.length) { |
|
23078 $browser.defer(function() { |
|
23079 if (asyncQueue.length) { |
|
23080 $rootScope.$digest(); |
|
23081 } |
|
23082 }); |
|
23083 } |
|
23084 |
|
23085 asyncQueue.push({scope: this, expression: expr}); |
|
23086 }, |
|
23087 |
|
23088 $$postDigest : function(fn) { |
|
23089 postDigestQueue.push(fn); |
|
23090 }, |
|
23091 |
|
23092 /** |
|
23093 * @ngdoc method |
|
23094 * @name $rootScope.Scope#$apply |
|
23095 * @kind function |
|
23096 * |
|
23097 * @description |
|
23098 * `$apply()` is used to execute an expression in angular from outside of the angular |
|
23099 * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). |
|
23100 * Because we are calling into the angular framework we need to perform proper scope life |
|
23101 * cycle of {@link ng.$exceptionHandler exception handling}, |
|
23102 * {@link ng.$rootScope.Scope#$digest executing watches}. |
|
23103 * |
|
23104 * ## Life cycle |
|
23105 * |
|
23106 * # Pseudo-Code of `$apply()` |
|
23107 * ```js |
|
23108 function $apply(expr) { |
|
23109 try { |
|
23110 return $eval(expr); |
|
23111 } catch (e) { |
|
23112 $exceptionHandler(e); |
|
23113 } finally { |
|
23114 $root.$digest(); |
|
23115 } |
|
23116 } |
|
23117 * ``` |
|
23118 * |
|
23119 * |
|
23120 * Scope's `$apply()` method transitions through the following stages: |
|
23121 * |
|
23122 * 1. The {@link guide/expression expression} is executed using the |
|
23123 * {@link ng.$rootScope.Scope#$eval $eval()} method. |
|
23124 * 2. Any exceptions from the execution of the expression are forwarded to the |
|
23125 * {@link ng.$exceptionHandler $exceptionHandler} service. |
|
23126 * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the |
|
23127 * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. |
|
23128 * |
|
23129 * |
|
23130 * @param {(string|function())=} exp An angular expression to be executed. |
|
23131 * |
|
23132 * - `string`: execute using the rules as defined in {@link guide/expression expression}. |
|
23133 * - `function(scope)`: execute the function with current `scope` parameter. |
|
23134 * |
|
23135 * @returns {*} The result of evaluating the expression. |
|
23136 */ |
|
23137 $apply: function(expr) { |
|
23138 try { |
|
23139 beginPhase('$apply'); |
|
23140 return this.$eval(expr); |
|
23141 } catch (e) { |
|
23142 $exceptionHandler(e); |
|
23143 } finally { |
|
23144 clearPhase(); |
|
23145 try { |
|
23146 $rootScope.$digest(); |
|
23147 } catch (e) { |
|
23148 $exceptionHandler(e); |
|
23149 throw e; |
|
23150 } |
|
23151 } |
|
23152 }, |
|
23153 |
|
23154 /** |
|
23155 * @ngdoc method |
|
23156 * @name $rootScope.Scope#$applyAsync |
|
23157 * @kind function |
|
23158 * |
|
23159 * @description |
|
23160 * Schedule the invokation of $apply to occur at a later time. The actual time difference |
|
23161 * varies across browsers, but is typically around ~10 milliseconds. |
|
23162 * |
|
23163 * This can be used to queue up multiple expressions which need to be evaluated in the same |
|
23164 * digest. |
|
23165 * |
|
23166 * @param {(string|function())=} exp An angular expression to be executed. |
|
23167 * |
|
23168 * - `string`: execute using the rules as defined in {@link guide/expression expression}. |
|
23169 * - `function(scope)`: execute the function with current `scope` parameter. |
|
23170 */ |
|
23171 $applyAsync: function(expr) { |
|
23172 var scope = this; |
|
23173 expr && applyAsyncQueue.push($applyAsyncExpression); |
|
23174 scheduleApplyAsync(); |
|
23175 |
|
23176 function $applyAsyncExpression() { |
|
23177 scope.$eval(expr); |
|
23178 } |
|
23179 }, |
|
23180 |
|
23181 /** |
|
23182 * @ngdoc method |
|
23183 * @name $rootScope.Scope#$on |
|
23184 * @kind function |
|
23185 * |
|
23186 * @description |
|
23187 * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for |
|
23188 * discussion of event life cycle. |
|
23189 * |
|
23190 * The event listener function format is: `function(event, args...)`. The `event` object |
|
23191 * passed into the listener has the following attributes: |
|
23192 * |
|
23193 * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or |
|
23194 * `$broadcast`-ed. |
|
23195 * - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the |
|
23196 * event propagates through the scope hierarchy, this property is set to null. |
|
23197 * - `name` - `{string}`: name of the event. |
|
23198 * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel |
|
23199 * further event propagation (available only for events that were `$emit`-ed). |
|
23200 * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag |
|
23201 * to true. |
|
23202 * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. |
|
23203 * |
|
23204 * @param {string} name Event name to listen on. |
|
23205 * @param {function(event, ...args)} listener Function to call when the event is emitted. |
|
23206 * @returns {function()} Returns a deregistration function for this listener. |
|
23207 */ |
|
23208 $on: function(name, listener) { |
|
23209 var namedListeners = this.$$listeners[name]; |
|
23210 if (!namedListeners) { |
|
23211 this.$$listeners[name] = namedListeners = []; |
|
23212 } |
|
23213 namedListeners.push(listener); |
|
23214 |
|
23215 var current = this; |
|
23216 do { |
|
23217 if (!current.$$listenerCount[name]) { |
|
23218 current.$$listenerCount[name] = 0; |
|
23219 } |
|
23220 current.$$listenerCount[name]++; |
|
23221 } while ((current = current.$parent)); |
|
23222 |
|
23223 var self = this; |
|
23224 return function() { |
|
23225 namedListeners[namedListeners.indexOf(listener)] = null; |
|
23226 decrementListenerCount(self, 1, name); |
|
23227 }; |
|
23228 }, |
|
23229 |
|
23230 |
|
23231 /** |
|
23232 * @ngdoc method |
|
23233 * @name $rootScope.Scope#$emit |
|
23234 * @kind function |
|
23235 * |
|
23236 * @description |
|
23237 * Dispatches an event `name` upwards through the scope hierarchy notifying the |
|
23238 * registered {@link ng.$rootScope.Scope#$on} listeners. |
|
23239 * |
|
23240 * The event life cycle starts at the scope on which `$emit` was called. All |
|
23241 * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get |
|
23242 * notified. Afterwards, the event traverses upwards toward the root scope and calls all |
|
23243 * registered listeners along the way. The event will stop propagating if one of the listeners |
|
23244 * cancels it. |
|
23245 * |
|
23246 * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed |
|
23247 * onto the {@link ng.$exceptionHandler $exceptionHandler} service. |
|
23248 * |
|
23249 * @param {string} name Event name to emit. |
|
23250 * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. |
|
23251 * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}). |
|
23252 */ |
|
23253 $emit: function(name, args) { |
|
23254 var empty = [], |
|
23255 namedListeners, |
|
23256 scope = this, |
|
23257 stopPropagation = false, |
|
23258 event = { |
|
23259 name: name, |
|
23260 targetScope: scope, |
|
23261 stopPropagation: function() {stopPropagation = true;}, |
|
23262 preventDefault: function() { |
|
23263 event.defaultPrevented = true; |
|
23264 }, |
|
23265 defaultPrevented: false |
|
23266 }, |
|
23267 listenerArgs = concat([event], arguments, 1), |
|
23268 i, length; |
|
23269 |
|
23270 do { |
|
23271 namedListeners = scope.$$listeners[name] || empty; |
|
23272 event.currentScope = scope; |
|
23273 for (i=0, length=namedListeners.length; i<length; i++) { |
|
23274 |
|
23275 // if listeners were deregistered, defragment the array |
|
23276 if (!namedListeners[i]) { |
|
23277 namedListeners.splice(i, 1); |
|
23278 i--; |
|
23279 length--; |
|
23280 continue; |
|
23281 } |
|
23282 try { |
|
23283 //allow all listeners attached to the current scope to run |
|
23284 namedListeners[i].apply(null, listenerArgs); |
|
23285 } catch (e) { |
|
23286 $exceptionHandler(e); |
|
23287 } |
|
23288 } |
|
23289 //if any listener on the current scope stops propagation, prevent bubbling |
|
23290 if (stopPropagation) { |
|
23291 event.currentScope = null; |
|
23292 return event; |
|
23293 } |
|
23294 //traverse upwards |
|
23295 scope = scope.$parent; |
|
23296 } while (scope); |
|
23297 |
|
23298 event.currentScope = null; |
|
23299 |
|
23300 return event; |
|
23301 }, |
|
23302 |
|
23303 |
|
23304 /** |
|
23305 * @ngdoc method |
|
23306 * @name $rootScope.Scope#$broadcast |
|
23307 * @kind function |
|
23308 * |
|
23309 * @description |
|
23310 * Dispatches an event `name` downwards to all child scopes (and their children) notifying the |
|
23311 * registered {@link ng.$rootScope.Scope#$on} listeners. |
|
23312 * |
|
23313 * The event life cycle starts at the scope on which `$broadcast` was called. All |
|
23314 * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get |
|
23315 * notified. Afterwards, the event propagates to all direct and indirect scopes of the current |
|
23316 * scope and calls all registered listeners along the way. The event cannot be canceled. |
|
23317 * |
|
23318 * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed |
|
23319 * onto the {@link ng.$exceptionHandler $exceptionHandler} service. |
|
23320 * |
|
23321 * @param {string} name Event name to broadcast. |
|
23322 * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. |
|
23323 * @return {Object} Event object, see {@link ng.$rootScope.Scope#$on} |
|
23324 */ |
|
23325 $broadcast: function(name, args) { |
|
23326 var target = this, |
|
23327 current = target, |
|
23328 next = target, |
|
23329 event = { |
|
23330 name: name, |
|
23331 targetScope: target, |
|
23332 preventDefault: function() { |
|
23333 event.defaultPrevented = true; |
|
23334 }, |
|
23335 defaultPrevented: false |
|
23336 }; |
|
23337 |
|
23338 if (!target.$$listenerCount[name]) return event; |
|
23339 |
|
23340 var listenerArgs = concat([event], arguments, 1), |
|
23341 listeners, i, length; |
|
23342 |
|
23343 //down while you can, then up and next sibling or up and next sibling until back at root |
|
23344 while ((current = next)) { |
|
23345 event.currentScope = current; |
|
23346 listeners = current.$$listeners[name] || []; |
|
23347 for (i=0, length = listeners.length; i<length; i++) { |
|
23348 // if listeners were deregistered, defragment the array |
|
23349 if (!listeners[i]) { |
|
23350 listeners.splice(i, 1); |
|
23351 i--; |
|
23352 length--; |
|
23353 continue; |
|
23354 } |
|
23355 |
|
23356 try { |
|
23357 listeners[i].apply(null, listenerArgs); |
|
23358 } catch(e) { |
|
23359 $exceptionHandler(e); |
|
23360 } |
|
23361 } |
|
23362 |
|
23363 // Insanity Warning: scope depth-first traversal |
|
23364 // yes, this code is a bit crazy, but it works and we have tests to prove it! |
|
23365 // this piece should be kept in sync with the traversal in $digest |
|
23366 // (though it differs due to having the extra check for $$listenerCount) |
|
23367 if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || |
|
23368 (current !== target && current.$$nextSibling)))) { |
|
23369 while(current !== target && !(next = current.$$nextSibling)) { |
|
23370 current = current.$parent; |
|
23371 } |
|
23372 } |
|
23373 } |
|
23374 |
|
23375 event.currentScope = null; |
|
23376 return event; |
|
23377 } |
|
23378 }; |
|
23379 |
|
23380 var $rootScope = new Scope(); |
|
23381 |
|
23382 //The internal queues. Expose them on the $rootScope for debugging/testing purposes. |
|
23383 var asyncQueue = $rootScope.$$asyncQueue = []; |
|
23384 var postDigestQueue = $rootScope.$$postDigestQueue = []; |
|
23385 var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; |
|
23386 |
|
23387 return $rootScope; |
|
23388 |
|
23389 |
|
23390 function beginPhase(phase) { |
|
23391 if ($rootScope.$$phase) { |
|
23392 throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase); |
|
23393 } |
|
23394 |
|
23395 $rootScope.$$phase = phase; |
|
23396 } |
|
23397 |
|
23398 function clearPhase() { |
|
23399 $rootScope.$$phase = null; |
|
23400 } |
|
23401 |
|
23402 |
|
23403 function decrementListenerCount(current, count, name) { |
|
23404 do { |
|
23405 current.$$listenerCount[name] -= count; |
|
23406 |
|
23407 if (current.$$listenerCount[name] === 0) { |
|
23408 delete current.$$listenerCount[name]; |
|
23409 } |
|
23410 } while ((current = current.$parent)); |
|
23411 } |
|
23412 |
|
23413 /** |
|
23414 * function used as an initial value for watchers. |
|
23415 * because it's unique we can easily tell it apart from other values |
|
23416 */ |
|
23417 function initWatchVal() {} |
|
23418 |
|
23419 function flushApplyAsync() { |
|
23420 while (applyAsyncQueue.length) { |
|
23421 try { |
|
23422 applyAsyncQueue.shift()(); |
|
23423 } catch(e) { |
|
23424 $exceptionHandler(e); |
|
23425 } |
|
23426 } |
|
23427 applyAsyncId = null; |
|
23428 } |
|
23429 |
|
23430 function scheduleApplyAsync() { |
|
23431 if (applyAsyncId === null) { |
|
23432 applyAsyncId = $browser.defer(function() { |
|
23433 $rootScope.$apply(flushApplyAsync); |
|
23434 }); |
|
23435 } |
|
23436 } |
|
23437 }]; |
|
23438 } |
|
23439 |
|
23440 /** |
|
23441 * @description |
|
23442 * Private service to sanitize uris for links and images. Used by $compile and $sanitize. |
|
23443 */ |
|
23444 function $$SanitizeUriProvider() { |
|
23445 var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, |
|
23446 imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/; |
|
23447 |
|
23448 /** |
|
23449 * @description |
|
23450 * Retrieves or overrides the default regular expression that is used for whitelisting of safe |
|
23451 * urls during a[href] sanitization. |
|
23452 * |
|
23453 * The sanitization is a security measure aimed at prevent XSS attacks via html links. |
|
23454 * |
|
23455 * Any url about to be assigned to a[href] via data-binding is first normalized and turned into |
|
23456 * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` |
|
23457 * regular expression. If a match is found, the original url is written into the dom. Otherwise, |
|
23458 * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. |
|
23459 * |
|
23460 * @param {RegExp=} regexp New regexp to whitelist urls with. |
|
23461 * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for |
|
23462 * chaining otherwise. |
|
23463 */ |
|
23464 this.aHrefSanitizationWhitelist = function(regexp) { |
|
23465 if (isDefined(regexp)) { |
|
23466 aHrefSanitizationWhitelist = regexp; |
|
23467 return this; |
|
23468 } |
|
23469 return aHrefSanitizationWhitelist; |
|
23470 }; |
|
23471 |
|
23472 |
|
23473 /** |
|
23474 * @description |
|
23475 * Retrieves or overrides the default regular expression that is used for whitelisting of safe |
|
23476 * urls during img[src] sanitization. |
|
23477 * |
|
23478 * The sanitization is a security measure aimed at prevent XSS attacks via html links. |
|
23479 * |
|
23480 * Any url about to be assigned to img[src] via data-binding is first normalized and turned into |
|
23481 * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` |
|
23482 * regular expression. If a match is found, the original url is written into the dom. Otherwise, |
|
23483 * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. |
|
23484 * |
|
23485 * @param {RegExp=} regexp New regexp to whitelist urls with. |
|
23486 * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for |
|
23487 * chaining otherwise. |
|
23488 */ |
|
23489 this.imgSrcSanitizationWhitelist = function(regexp) { |
|
23490 if (isDefined(regexp)) { |
|
23491 imgSrcSanitizationWhitelist = regexp; |
|
23492 return this; |
|
23493 } |
|
23494 return imgSrcSanitizationWhitelist; |
|
23495 }; |
|
23496 |
|
23497 this.$get = function() { |
|
23498 return function sanitizeUri(uri, isImage) { |
|
23499 var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; |
|
23500 var normalizedVal; |
|
23501 normalizedVal = urlResolve(uri).href; |
|
23502 if (normalizedVal !== '' && !normalizedVal.match(regex)) { |
|
23503 return 'unsafe:'+normalizedVal; |
|
23504 } |
|
23505 return uri; |
|
23506 }; |
|
23507 }; |
|
23508 } |
|
23509 |
|
23510 var $sceMinErr = minErr('$sce'); |
|
23511 |
|
23512 var SCE_CONTEXTS = { |
|
23513 HTML: 'html', |
|
23514 CSS: 'css', |
|
23515 URL: 'url', |
|
23516 // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a |
|
23517 // url. (e.g. ng-include, script src, templateUrl) |
|
23518 RESOURCE_URL: 'resourceUrl', |
|
23519 JS: 'js' |
|
23520 }; |
|
23521 |
|
23522 // Helper functions follow. |
|
23523 |
|
23524 // Copied from: |
|
23525 // http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962 |
|
23526 // Prereq: s is a string. |
|
23527 function escapeForRegexp(s) { |
|
23528 return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1'). |
|
23529 replace(/\x08/g, '\\x08'); |
|
23530 } |
|
23531 |
|
23532 |
|
23533 function adjustMatcher(matcher) { |
|
23534 if (matcher === 'self') { |
|
23535 return matcher; |
|
23536 } else if (isString(matcher)) { |
|
23537 // Strings match exactly except for 2 wildcards - '*' and '**'. |
|
23538 // '*' matches any character except those from the set ':/.?&'. |
|
23539 // '**' matches any character (like .* in a RegExp). |
|
23540 // More than 2 *'s raises an error as it's ill defined. |
|
23541 if (matcher.indexOf('***') > -1) { |
|
23542 throw $sceMinErr('iwcard', |
|
23543 'Illegal sequence *** in string matcher. String: {0}', matcher); |
|
23544 } |
|
23545 matcher = escapeForRegexp(matcher). |
|
23546 replace('\\*\\*', '.*'). |
|
23547 replace('\\*', '[^:/.?&;]*'); |
|
23548 return new RegExp('^' + matcher + '$'); |
|
23549 } else if (isRegExp(matcher)) { |
|
23550 // The only other type of matcher allowed is a Regexp. |
|
23551 // Match entire URL / disallow partial matches. |
|
23552 // Flags are reset (i.e. no global, ignoreCase or multiline) |
|
23553 return new RegExp('^' + matcher.source + '$'); |
|
23554 } else { |
|
23555 throw $sceMinErr('imatcher', |
|
23556 'Matchers may only be "self", string patterns or RegExp objects'); |
|
23557 } |
|
23558 } |
|
23559 |
|
23560 |
|
23561 function adjustMatchers(matchers) { |
|
23562 var adjustedMatchers = []; |
|
23563 if (isDefined(matchers)) { |
|
23564 forEach(matchers, function(matcher) { |
|
23565 adjustedMatchers.push(adjustMatcher(matcher)); |
|
23566 }); |
|
23567 } |
|
23568 return adjustedMatchers; |
|
23569 } |
|
23570 |
|
23571 |
|
23572 /** |
|
23573 * @ngdoc service |
|
23574 * @name $sceDelegate |
|
23575 * @kind function |
|
23576 * |
|
23577 * @description |
|
23578 * |
|
23579 * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict |
|
23580 * Contextual Escaping (SCE)} services to AngularJS. |
|
23581 * |
|
23582 * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of |
|
23583 * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is |
|
23584 * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to |
|
23585 * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things |
|
23586 * work because `$sce` delegates to `$sceDelegate` for these operations. |
|
23587 * |
|
23588 * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service. |
|
23589 * |
|
23590 * The default instance of `$sceDelegate` should work out of the box with little pain. While you |
|
23591 * can override it completely to change the behavior of `$sce`, the common case would |
|
23592 * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting |
|
23593 * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as |
|
23594 * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist |
|
23595 * $sceDelegateProvider.resourceUrlWhitelist} and {@link |
|
23596 * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} |
|
23597 */ |
|
23598 |
|
23599 /** |
|
23600 * @ngdoc provider |
|
23601 * @name $sceDelegateProvider |
|
23602 * @description |
|
23603 * |
|
23604 * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate |
|
23605 * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure |
|
23606 * that the URLs used for sourcing Angular templates are safe. Refer {@link |
|
23607 * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and |
|
23608 * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} |
|
23609 * |
|
23610 * For the general details about this service in Angular, read the main page for {@link ng.$sce |
|
23611 * Strict Contextual Escaping (SCE)}. |
|
23612 * |
|
23613 * **Example**: Consider the following case. <a name="example"></a> |
|
23614 * |
|
23615 * - your app is hosted at url `http://myapp.example.com/` |
|
23616 * - but some of your templates are hosted on other domains you control such as |
|
23617 * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. |
|
23618 * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. |
|
23619 * |
|
23620 * Here is what a secure configuration for this scenario might look like: |
|
23621 * |
|
23622 * ``` |
|
23623 * angular.module('myApp', []).config(function($sceDelegateProvider) { |
|
23624 * $sceDelegateProvider.resourceUrlWhitelist([ |
|
23625 * // Allow same origin resource loads. |
|
23626 * 'self', |
|
23627 * // Allow loading from our assets domain. Notice the difference between * and **. |
|
23628 * 'http://srv*.assets.example.com/**' |
|
23629 * ]); |
|
23630 * |
|
23631 * // The blacklist overrides the whitelist so the open redirect here is blocked. |
|
23632 * $sceDelegateProvider.resourceUrlBlacklist([ |
|
23633 * 'http://myapp.example.com/clickThru**' |
|
23634 * ]); |
|
23635 * }); |
|
23636 * ``` |
|
23637 */ |
|
23638 |
|
23639 function $SceDelegateProvider() { |
|
23640 this.SCE_CONTEXTS = SCE_CONTEXTS; |
|
23641 |
|
23642 // Resource URLs can also be trusted by policy. |
|
23643 var resourceUrlWhitelist = ['self'], |
|
23644 resourceUrlBlacklist = []; |
|
23645 |
|
23646 /** |
|
23647 * @ngdoc method |
|
23648 * @name $sceDelegateProvider#resourceUrlWhitelist |
|
23649 * @kind function |
|
23650 * |
|
23651 * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value |
|
23652 * provided. This must be an array or null. A snapshot of this array is used so further |
|
23653 * changes to the array are ignored. |
|
23654 * |
|
23655 * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items |
|
23656 * allowed in this array. |
|
23657 * |
|
23658 * Note: **an empty whitelist array will block all URLs**! |
|
23659 * |
|
23660 * @return {Array} the currently set whitelist array. |
|
23661 * |
|
23662 * The **default value** when no whitelist has been explicitly set is `['self']` allowing only |
|
23663 * same origin resource requests. |
|
23664 * |
|
23665 * @description |
|
23666 * Sets/Gets the whitelist of trusted resource URLs. |
|
23667 */ |
|
23668 this.resourceUrlWhitelist = function (value) { |
|
23669 if (arguments.length) { |
|
23670 resourceUrlWhitelist = adjustMatchers(value); |
|
23671 } |
|
23672 return resourceUrlWhitelist; |
|
23673 }; |
|
23674 |
|
23675 /** |
|
23676 * @ngdoc method |
|
23677 * @name $sceDelegateProvider#resourceUrlBlacklist |
|
23678 * @kind function |
|
23679 * |
|
23680 * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value |
|
23681 * provided. This must be an array or null. A snapshot of this array is used so further |
|
23682 * changes to the array are ignored. |
|
23683 * |
|
23684 * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items |
|
23685 * allowed in this array. |
|
23686 * |
|
23687 * The typical usage for the blacklist is to **block |
|
23688 * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as |
|
23689 * these would otherwise be trusted but actually return content from the redirected domain. |
|
23690 * |
|
23691 * Finally, **the blacklist overrides the whitelist** and has the final say. |
|
23692 * |
|
23693 * @return {Array} the currently set blacklist array. |
|
23694 * |
|
23695 * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there |
|
23696 * is no blacklist.) |
|
23697 * |
|
23698 * @description |
|
23699 * Sets/Gets the blacklist of trusted resource URLs. |
|
23700 */ |
|
23701 |
|
23702 this.resourceUrlBlacklist = function (value) { |
|
23703 if (arguments.length) { |
|
23704 resourceUrlBlacklist = adjustMatchers(value); |
|
23705 } |
|
23706 return resourceUrlBlacklist; |
|
23707 }; |
|
23708 |
|
23709 this.$get = ['$injector', function($injector) { |
|
23710 |
|
23711 var htmlSanitizer = function htmlSanitizer(html) { |
|
23712 throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); |
|
23713 }; |
|
23714 |
|
23715 if ($injector.has('$sanitize')) { |
|
23716 htmlSanitizer = $injector.get('$sanitize'); |
|
23717 } |
|
23718 |
|
23719 |
|
23720 function matchUrl(matcher, parsedUrl) { |
|
23721 if (matcher === 'self') { |
|
23722 return urlIsSameOrigin(parsedUrl); |
|
23723 } else { |
|
23724 // definitely a regex. See adjustMatchers() |
|
23725 return !!matcher.exec(parsedUrl.href); |
|
23726 } |
|
23727 } |
|
23728 |
|
23729 function isResourceUrlAllowedByPolicy(url) { |
|
23730 var parsedUrl = urlResolve(url.toString()); |
|
23731 var i, n, allowed = false; |
|
23732 // Ensure that at least one item from the whitelist allows this url. |
|
23733 for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { |
|
23734 if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { |
|
23735 allowed = true; |
|
23736 break; |
|
23737 } |
|
23738 } |
|
23739 if (allowed) { |
|
23740 // Ensure that no item from the blacklist blocked this url. |
|
23741 for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { |
|
23742 if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { |
|
23743 allowed = false; |
|
23744 break; |
|
23745 } |
|
23746 } |
|
23747 } |
|
23748 return allowed; |
|
23749 } |
|
23750 |
|
23751 function generateHolderType(Base) { |
|
23752 var holderType = function TrustedValueHolderType(trustedValue) { |
|
23753 this.$$unwrapTrustedValue = function() { |
|
23754 return trustedValue; |
|
23755 }; |
|
23756 }; |
|
23757 if (Base) { |
|
23758 holderType.prototype = new Base(); |
|
23759 } |
|
23760 holderType.prototype.valueOf = function sceValueOf() { |
|
23761 return this.$$unwrapTrustedValue(); |
|
23762 }; |
|
23763 holderType.prototype.toString = function sceToString() { |
|
23764 return this.$$unwrapTrustedValue().toString(); |
|
23765 }; |
|
23766 return holderType; |
|
23767 } |
|
23768 |
|
23769 var trustedValueHolderBase = generateHolderType(), |
|
23770 byType = {}; |
|
23771 |
|
23772 byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); |
|
23773 byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); |
|
23774 byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); |
|
23775 byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); |
|
23776 byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); |
|
23777 |
|
23778 /** |
|
23779 * @ngdoc method |
|
23780 * @name $sceDelegate#trustAs |
|
23781 * |
|
23782 * @description |
|
23783 * Returns an object that is trusted by angular for use in specified strict |
|
23784 * contextual escaping contexts (such as ng-bind-html, ng-include, any src |
|
23785 * attribute interpolation, any dom event binding attribute interpolation |
|
23786 * such as for onclick, etc.) that uses the provided value. |
|
23787 * See {@link ng.$sce $sce} for enabling strict contextual escaping. |
|
23788 * |
|
23789 * @param {string} type The kind of context in which this value is safe for use. e.g. url, |
|
23790 * resourceUrl, html, js and css. |
|
23791 * @param {*} value The value that that should be considered trusted/safe. |
|
23792 * @returns {*} A value that can be used to stand in for the provided `value` in places |
|
23793 * where Angular expects a $sce.trustAs() return value. |
|
23794 */ |
|
23795 function trustAs(type, trustedValue) { |
|
23796 var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); |
|
23797 if (!Constructor) { |
|
23798 throw $sceMinErr('icontext', |
|
23799 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', |
|
23800 type, trustedValue); |
|
23801 } |
|
23802 if (trustedValue === null || trustedValue === undefined || trustedValue === '') { |
|
23803 return trustedValue; |
|
23804 } |
|
23805 // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting |
|
23806 // mutable objects, we ensure here that the value passed in is actually a string. |
|
23807 if (typeof trustedValue !== 'string') { |
|
23808 throw $sceMinErr('itype', |
|
23809 'Attempted to trust a non-string value in a content requiring a string: Context: {0}', |
|
23810 type); |
|
23811 } |
|
23812 return new Constructor(trustedValue); |
|
23813 } |
|
23814 |
|
23815 /** |
|
23816 * @ngdoc method |
|
23817 * @name $sceDelegate#valueOf |
|
23818 * |
|
23819 * @description |
|
23820 * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#trustAs |
|
23821 * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link |
|
23822 * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. |
|
23823 * |
|
23824 * If the passed parameter is not a value that had been returned by {@link |
|
23825 * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is. |
|
23826 * |
|
23827 * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} |
|
23828 * call or anything else. |
|
23829 * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs |
|
23830 * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns |
|
23831 * `value` unchanged. |
|
23832 */ |
|
23833 function valueOf(maybeTrusted) { |
|
23834 if (maybeTrusted instanceof trustedValueHolderBase) { |
|
23835 return maybeTrusted.$$unwrapTrustedValue(); |
|
23836 } else { |
|
23837 return maybeTrusted; |
|
23838 } |
|
23839 } |
|
23840 |
|
23841 /** |
|
23842 * @ngdoc method |
|
23843 * @name $sceDelegate#getTrusted |
|
23844 * |
|
23845 * @description |
|
23846 * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and |
|
23847 * returns the originally supplied value if the queried context type is a supertype of the |
|
23848 * created type. If this condition isn't satisfied, throws an exception. |
|
23849 * |
|
23850 * @param {string} type The kind of context in which this value is to be used. |
|
23851 * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs |
|
23852 * `$sceDelegate.trustAs`} call. |
|
23853 * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs |
|
23854 * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. |
|
23855 */ |
|
23856 function getTrusted(type, maybeTrusted) { |
|
23857 if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') { |
|
23858 return maybeTrusted; |
|
23859 } |
|
23860 var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); |
|
23861 if (constructor && maybeTrusted instanceof constructor) { |
|
23862 return maybeTrusted.$$unwrapTrustedValue(); |
|
23863 } |
|
23864 // If we get here, then we may only take one of two actions. |
|
23865 // 1. sanitize the value for the requested type, or |
|
23866 // 2. throw an exception. |
|
23867 if (type === SCE_CONTEXTS.RESOURCE_URL) { |
|
23868 if (isResourceUrlAllowedByPolicy(maybeTrusted)) { |
|
23869 return maybeTrusted; |
|
23870 } else { |
|
23871 throw $sceMinErr('insecurl', |
|
23872 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', |
|
23873 maybeTrusted.toString()); |
|
23874 } |
|
23875 } else if (type === SCE_CONTEXTS.HTML) { |
|
23876 return htmlSanitizer(maybeTrusted); |
|
23877 } |
|
23878 throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); |
|
23879 } |
|
23880 |
|
23881 return { trustAs: trustAs, |
|
23882 getTrusted: getTrusted, |
|
23883 valueOf: valueOf }; |
|
23884 }]; |
|
23885 } |
|
23886 |
|
23887 |
|
23888 /** |
|
23889 * @ngdoc provider |
|
23890 * @name $sceProvider |
|
23891 * @description |
|
23892 * |
|
23893 * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. |
|
23894 * - enable/disable Strict Contextual Escaping (SCE) in a module |
|
23895 * - override the default implementation with a custom delegate |
|
23896 * |
|
23897 * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. |
|
23898 */ |
|
23899 |
|
23900 /* jshint maxlen: false*/ |
|
23901 |
|
23902 /** |
|
23903 * @ngdoc service |
|
23904 * @name $sce |
|
23905 * @kind function |
|
23906 * |
|
23907 * @description |
|
23908 * |
|
23909 * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. |
|
23910 * |
|
23911 * # Strict Contextual Escaping |
|
23912 * |
|
23913 * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain |
|
23914 * contexts to result in a value that is marked as safe to use for that context. One example of |
|
23915 * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer |
|
23916 * to these contexts as privileged or SCE contexts. |
|
23917 * |
|
23918 * As of version 1.2, Angular ships with SCE enabled by default. |
|
23919 * |
|
23920 * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows |
|
23921 * one to execute arbitrary javascript by the use of the expression() syntax. Refer |
|
23922 * <http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx> to learn more about them. |
|
23923 * You can ensure your document is in standards mode and not quirks mode by adding `<!doctype html>` |
|
23924 * to the top of your HTML document. |
|
23925 * |
|
23926 * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for |
|
23927 * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. |
|
23928 * |
|
23929 * Here's an example of a binding in a privileged context: |
|
23930 * |
|
23931 * ``` |
|
23932 * <input ng-model="userHtml"> |
|
23933 * <div ng-bind-html="userHtml"></div> |
|
23934 * ``` |
|
23935 * |
|
23936 * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE |
|
23937 * disabled, this application allows the user to render arbitrary HTML into the DIV. |
|
23938 * In a more realistic example, one may be rendering user comments, blog articles, etc. via |
|
23939 * bindings. (HTML is just one example of a context where rendering user controlled input creates |
|
23940 * security vulnerabilities.) |
|
23941 * |
|
23942 * For the case of HTML, you might use a library, either on the client side, or on the server side, |
|
23943 * to sanitize unsafe HTML before binding to the value and rendering it in the document. |
|
23944 * |
|
23945 * How would you ensure that every place that used these types of bindings was bound to a value that |
|
23946 * was sanitized by your library (or returned as safe for rendering by your server?) How can you |
|
23947 * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some |
|
23948 * properties/fields and forgot to update the binding to the sanitized value? |
|
23949 * |
|
23950 * To be secure by default, you want to ensure that any such bindings are disallowed unless you can |
|
23951 * determine that something explicitly says it's safe to use a value for binding in that |
|
23952 * context. You can then audit your code (a simple grep would do) to ensure that this is only done |
|
23953 * for those values that you can easily tell are safe - because they were received from your server, |
|
23954 * sanitized by your library, etc. You can organize your codebase to help with this - perhaps |
|
23955 * allowing only the files in a specific directory to do this. Ensuring that the internal API |
|
23956 * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. |
|
23957 * |
|
23958 * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} |
|
23959 * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to |
|
23960 * obtain values that will be accepted by SCE / privileged contexts. |
|
23961 * |
|
23962 * |
|
23963 * ## How does it work? |
|
23964 * |
|
23965 * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted |
|
23966 * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link |
|
23967 * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the |
|
23968 * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. |
|
23969 * |
|
23970 * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link |
|
23971 * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly |
|
23972 * simplified): |
|
23973 * |
|
23974 * ``` |
|
23975 * var ngBindHtmlDirective = ['$sce', function($sce) { |
|
23976 * return function(scope, element, attr) { |
|
23977 * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { |
|
23978 * element.html(value || ''); |
|
23979 * }); |
|
23980 * }; |
|
23981 * }]; |
|
23982 * ``` |
|
23983 * |
|
23984 * ## Impact on loading templates |
|
23985 * |
|
23986 * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as |
|
23987 * `templateUrl`'s specified by {@link guide/directive directives}. |
|
23988 * |
|
23989 * By default, Angular only loads templates from the same domain and protocol as the application |
|
23990 * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl |
|
23991 * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or |
|
23992 * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist |
|
23993 * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. |
|
23994 * |
|
23995 * *Please note*: |
|
23996 * The browser's |
|
23997 * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) |
|
23998 * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) |
|
23999 * policy apply in addition to this and may further restrict whether the template is successfully |
|
24000 * loaded. This means that without the right CORS policy, loading templates from a different domain |
|
24001 * won't work on all browsers. Also, loading templates from `file://` URL does not work on some |
|
24002 * browsers. |
|
24003 * |
|
24004 * ## This feels like too much overhead |
|
24005 * |
|
24006 * It's important to remember that SCE only applies to interpolation expressions. |
|
24007 * |
|
24008 * If your expressions are constant literals, they're automatically trusted and you don't need to |
|
24009 * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g. |
|
24010 * `<div ng-bind-html="'<b>implicitly trusted</b>'"></div>`) just works. |
|
24011 * |
|
24012 * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them |
|
24013 * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. |
|
24014 * |
|
24015 * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load |
|
24016 * templates in `ng-include` from your application's domain without having to even know about SCE. |
|
24017 * It blocks loading templates from other domains or loading templates over http from an https |
|
24018 * served document. You can change these by setting your own custom {@link |
|
24019 * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link |
|
24020 * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. |
|
24021 * |
|
24022 * This significantly reduces the overhead. It is far easier to pay the small overhead and have an |
|
24023 * application that's secure and can be audited to verify that with much more ease than bolting |
|
24024 * security onto an application later. |
|
24025 * |
|
24026 * <a name="contexts"></a> |
|
24027 * ## What trusted context types are supported? |
|
24028 * |
|
24029 * | Context | Notes | |
|
24030 * |---------------------|----------------| |
|
24031 * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | |
|
24032 * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | |
|
24033 * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`<a href=` and `<img src=` sanitize their urls and don't constitute an SCE context. | |
|
24034 * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | |
|
24035 * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | |
|
24036 * |
|
24037 * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist} <a name="resourceUrlPatternItem"></a> |
|
24038 * |
|
24039 * Each element in these arrays must be one of the following: |
|
24040 * |
|
24041 * - **'self'** |
|
24042 * - The special **string**, `'self'`, can be used to match against all URLs of the **same |
|
24043 * domain** as the application document using the **same protocol**. |
|
24044 * - **String** (except the special value `'self'`) |
|
24045 * - The string is matched against the full *normalized / absolute URL* of the resource |
|
24046 * being tested (substring matches are not good enough.) |
|
24047 * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters |
|
24048 * match themselves. |
|
24049 * - `*`: matches zero or more occurrences of any character other than one of the following 6 |
|
24050 * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use |
|
24051 * in a whitelist. |
|
24052 * - `**`: matches zero or more occurrences of *any* character. As such, it's not |
|
24053 * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. |
|
24054 * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might |
|
24055 * not have been the intention.) Its usage at the very end of the path is ok. (e.g. |
|
24056 * http://foo.example.com/templates/**). |
|
24057 * - **RegExp** (*see caveat below*) |
|
24058 * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax |
|
24059 * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to |
|
24060 * accidentally introduce a bug when one updates a complex expression (imho, all regexes should |
|
24061 * have good test coverage.). For instance, the use of `.` in the regex is correct only in a |
|
24062 * small number of cases. A `.` character in the regex used when matching the scheme or a |
|
24063 * subdomain could be matched against a `:` or literal `.` that was likely not intended. It |
|
24064 * is highly recommended to use the string patterns and only fall back to regular expressions |
|
24065 * if they as a last resort. |
|
24066 * - The regular expression must be an instance of RegExp (i.e. not a string.) It is |
|
24067 * matched against the **entire** *normalized / absolute URL* of the resource being tested |
|
24068 * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags |
|
24069 * present on the RegExp (such as multiline, global, ignoreCase) are ignored. |
|
24070 * - If you are generating your JavaScript from some other templating engine (not |
|
24071 * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)), |
|
24072 * remember to escape your regular expression (and be aware that you might need more than |
|
24073 * one level of escaping depending on your templating engine and the way you interpolated |
|
24074 * the value.) Do make use of your platform's escaping mechanism as it might be good |
|
24075 * enough before coding your own. e.g. Ruby has |
|
24076 * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape) |
|
24077 * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape). |
|
24078 * Javascript lacks a similar built in function for escaping. Take a look at Google |
|
24079 * Closure library's [goog.string.regExpEscape(s)]( |
|
24080 * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962). |
|
24081 * |
|
24082 * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. |
|
24083 * |
|
24084 * ## Show me an example using SCE. |
|
24085 * |
|
24086 * <example module="mySceApp" deps="angular-sanitize.js"> |
|
24087 * <file name="index.html"> |
|
24088 * <div ng-controller="AppController as myCtrl"> |
|
24089 * <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br> |
|
24090 * <b>User comments</b><br> |
|
24091 * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when |
|
24092 * $sanitize is available. If $sanitize isn't available, this results in an error instead of an |
|
24093 * exploit. |
|
24094 * <div class="well"> |
|
24095 * <div ng-repeat="userComment in myCtrl.userComments"> |
|
24096 * <b>{{userComment.name}}</b>: |
|
24097 * <span ng-bind-html="userComment.htmlComment" class="htmlComment"></span> |
|
24098 * <br> |
|
24099 * </div> |
|
24100 * </div> |
|
24101 * </div> |
|
24102 * </file> |
|
24103 * |
|
24104 * <file name="script.js"> |
|
24105 * angular.module('mySceApp', ['ngSanitize']) |
|
24106 * .controller('AppController', ['$http', '$templateCache', '$sce', |
|
24107 * function($http, $templateCache, $sce) { |
|
24108 * var self = this; |
|
24109 * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { |
|
24110 * self.userComments = userComments; |
|
24111 * }); |
|
24112 * self.explicitlyTrustedHtml = $sce.trustAsHtml( |
|
24113 * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + |
|
24114 * 'sanitization."">Hover over this text.</span>'); |
|
24115 * }]); |
|
24116 * </file> |
|
24117 * |
|
24118 * <file name="test_data.json"> |
|
24119 * [ |
|
24120 * { "name": "Alice", |
|
24121 * "htmlComment": |
|
24122 * "<span onmouseover='this.textContent=\"PWN3D!\"'>Is <i>anyone</i> reading this?</span>" |
|
24123 * }, |
|
24124 * { "name": "Bob", |
|
24125 * "htmlComment": "<i>Yes!</i> Am I the only other one?" |
|
24126 * } |
|
24127 * ] |
|
24128 * </file> |
|
24129 * |
|
24130 * <file name="protractor.js" type="protractor"> |
|
24131 * describe('SCE doc demo', function() { |
|
24132 * it('should sanitize untrusted values', function() { |
|
24133 * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) |
|
24134 * .toBe('<span>Is <i>anyone</i> reading this?</span>'); |
|
24135 * }); |
|
24136 * |
|
24137 * it('should NOT sanitize explicitly trusted values', function() { |
|
24138 * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( |
|
24139 * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + |
|
24140 * 'sanitization."">Hover over this text.</span>'); |
|
24141 * }); |
|
24142 * }); |
|
24143 * </file> |
|
24144 * </example> |
|
24145 * |
|
24146 * |
|
24147 * |
|
24148 * ## Can I disable SCE completely? |
|
24149 * |
|
24150 * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits |
|
24151 * for little coding overhead. It will be much harder to take an SCE disabled application and |
|
24152 * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE |
|
24153 * for cases where you have a lot of existing code that was written before SCE was introduced and |
|
24154 * you're migrating them a module at a time. |
|
24155 * |
|
24156 * That said, here's how you can completely disable SCE: |
|
24157 * |
|
24158 * ``` |
|
24159 * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { |
|
24160 * // Completely disable SCE. For demonstration purposes only! |
|
24161 * // Do not use in new projects. |
|
24162 * $sceProvider.enabled(false); |
|
24163 * }); |
|
24164 * ``` |
|
24165 * |
|
24166 */ |
|
24167 /* jshint maxlen: 100 */ |
|
24168 |
|
24169 function $SceProvider() { |
|
24170 var enabled = true; |
|
24171 |
|
24172 /** |
|
24173 * @ngdoc method |
|
24174 * @name $sceProvider#enabled |
|
24175 * @kind function |
|
24176 * |
|
24177 * @param {boolean=} value If provided, then enables/disables SCE. |
|
24178 * @return {boolean} true if SCE is enabled, false otherwise. |
|
24179 * |
|
24180 * @description |
|
24181 * Enables/disables SCE and returns the current value. |
|
24182 */ |
|
24183 this.enabled = function (value) { |
|
24184 if (arguments.length) { |
|
24185 enabled = !!value; |
|
24186 } |
|
24187 return enabled; |
|
24188 }; |
|
24189 |
|
24190 |
|
24191 /* Design notes on the default implementation for SCE. |
|
24192 * |
|
24193 * The API contract for the SCE delegate |
|
24194 * ------------------------------------- |
|
24195 * The SCE delegate object must provide the following 3 methods: |
|
24196 * |
|
24197 * - trustAs(contextEnum, value) |
|
24198 * This method is used to tell the SCE service that the provided value is OK to use in the |
|
24199 * contexts specified by contextEnum. It must return an object that will be accepted by |
|
24200 * getTrusted() for a compatible contextEnum and return this value. |
|
24201 * |
|
24202 * - valueOf(value) |
|
24203 * For values that were not produced by trustAs(), return them as is. For values that were |
|
24204 * produced by trustAs(), return the corresponding input value to trustAs. Basically, if |
|
24205 * trustAs is wrapping the given values into some type, this operation unwraps it when given |
|
24206 * such a value. |
|
24207 * |
|
24208 * - getTrusted(contextEnum, value) |
|
24209 * This function should return the a value that is safe to use in the context specified by |
|
24210 * contextEnum or throw and exception otherwise. |
|
24211 * |
|
24212 * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be |
|
24213 * opaque or wrapped in some holder object. That happens to be an implementation detail. For |
|
24214 * instance, an implementation could maintain a registry of all trusted objects by context. In |
|
24215 * such a case, trustAs() would return the same object that was passed in. getTrusted() would |
|
24216 * return the same object passed in if it was found in the registry under a compatible context or |
|
24217 * throw an exception otherwise. An implementation might only wrap values some of the time based |
|
24218 * on some criteria. getTrusted() might return a value and not throw an exception for special |
|
24219 * constants or objects even if not wrapped. All such implementations fulfill this contract. |
|
24220 * |
|
24221 * |
|
24222 * A note on the inheritance model for SCE contexts |
|
24223 * ------------------------------------------------ |
|
24224 * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This |
|
24225 * is purely an implementation details. |
|
24226 * |
|
24227 * The contract is simply this: |
|
24228 * |
|
24229 * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) |
|
24230 * will also succeed. |
|
24231 * |
|
24232 * Inheritance happens to capture this in a natural way. In some future, we |
|
24233 * may not use inheritance anymore. That is OK because no code outside of |
|
24234 * sce.js and sceSpecs.js would need to be aware of this detail. |
|
24235 */ |
|
24236 |
|
24237 this.$get = ['$parse', '$sniffer', '$sceDelegate', function( |
|
24238 $parse, $sniffer, $sceDelegate) { |
|
24239 // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows |
|
24240 // the "expression(javascript expression)" syntax which is insecure. |
|
24241 if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) { |
|
24242 throw $sceMinErr('iequirks', |
|
24243 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + |
|
24244 'mode. You can fix this by adding the text <!doctype html> to the top of your HTML ' + |
|
24245 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); |
|
24246 } |
|
24247 |
|
24248 var sce = shallowCopy(SCE_CONTEXTS); |
|
24249 |
|
24250 /** |
|
24251 * @ngdoc method |
|
24252 * @name $sce#isEnabled |
|
24253 * @kind function |
|
24254 * |
|
24255 * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you |
|
24256 * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. |
|
24257 * |
|
24258 * @description |
|
24259 * Returns a boolean indicating if SCE is enabled. |
|
24260 */ |
|
24261 sce.isEnabled = function () { |
|
24262 return enabled; |
|
24263 }; |
|
24264 sce.trustAs = $sceDelegate.trustAs; |
|
24265 sce.getTrusted = $sceDelegate.getTrusted; |
|
24266 sce.valueOf = $sceDelegate.valueOf; |
|
24267 |
|
24268 if (!enabled) { |
|
24269 sce.trustAs = sce.getTrusted = function(type, value) { return value; }; |
|
24270 sce.valueOf = identity; |
|
24271 } |
|
24272 |
|
24273 /** |
|
24274 * @ngdoc method |
|
24275 * @name $sce#parseAs |
|
24276 * |
|
24277 * @description |
|
24278 * Converts Angular {@link guide/expression expression} into a function. This is like {@link |
|
24279 * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it |
|
24280 * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, |
|
24281 * *result*)} |
|
24282 * |
|
24283 * @param {string} type The kind of SCE context in which this result will be used. |
|
24284 * @param {string} expression String expression to compile. |
|
24285 * @returns {function(context, locals)} a function which represents the compiled expression: |
|
24286 * |
|
24287 * * `context` – `{object}` – an object against which any expressions embedded in the strings |
|
24288 * are evaluated against (typically a scope object). |
|
24289 * * `locals` – `{object=}` – local variables context object, useful for overriding values in |
|
24290 * `context`. |
|
24291 */ |
|
24292 sce.parseAs = function sceParseAs(type, expr) { |
|
24293 var parsed = $parse(expr); |
|
24294 if (parsed.literal && parsed.constant) { |
|
24295 return parsed; |
|
24296 } else { |
|
24297 return $parse(expr, function (value) { |
|
24298 return sce.getTrusted(type, value); |
|
24299 }); |
|
24300 } |
|
24301 }; |
|
24302 |
|
24303 /** |
|
24304 * @ngdoc method |
|
24305 * @name $sce#trustAs |
|
24306 * |
|
24307 * @description |
|
24308 * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, |
|
24309 * returns an object that is trusted by angular for use in specified strict contextual |
|
24310 * escaping contexts (such as ng-bind-html, ng-include, any src attribute |
|
24311 * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) |
|
24312 * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual |
|
24313 * escaping. |
|
24314 * |
|
24315 * @param {string} type The kind of context in which this value is safe for use. e.g. url, |
|
24316 * resource_url, html, js and css. |
|
24317 * @param {*} value The value that that should be considered trusted/safe. |
|
24318 * @returns {*} A value that can be used to stand in for the provided `value` in places |
|
24319 * where Angular expects a $sce.trustAs() return value. |
|
24320 */ |
|
24321 |
|
24322 /** |
|
24323 * @ngdoc method |
|
24324 * @name $sce#trustAsHtml |
|
24325 * |
|
24326 * @description |
|
24327 * Shorthand method. `$sce.trustAsHtml(value)` → |
|
24328 * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} |
|
24329 * |
|
24330 * @param {*} value The value to trustAs. |
|
24331 * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml |
|
24332 * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives |
|
24333 * only accept expressions that are either literal constants or are the |
|
24334 * return value of {@link ng.$sce#trustAs $sce.trustAs}.) |
|
24335 */ |
|
24336 |
|
24337 /** |
|
24338 * @ngdoc method |
|
24339 * @name $sce#trustAsUrl |
|
24340 * |
|
24341 * @description |
|
24342 * Shorthand method. `$sce.trustAsUrl(value)` → |
|
24343 * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} |
|
24344 * |
|
24345 * @param {*} value The value to trustAs. |
|
24346 * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl |
|
24347 * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives |
|
24348 * only accept expressions that are either literal constants or are the |
|
24349 * return value of {@link ng.$sce#trustAs $sce.trustAs}.) |
|
24350 */ |
|
24351 |
|
24352 /** |
|
24353 * @ngdoc method |
|
24354 * @name $sce#trustAsResourceUrl |
|
24355 * |
|
24356 * @description |
|
24357 * Shorthand method. `$sce.trustAsResourceUrl(value)` → |
|
24358 * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} |
|
24359 * |
|
24360 * @param {*} value The value to trustAs. |
|
24361 * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl |
|
24362 * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives |
|
24363 * only accept expressions that are either literal constants or are the return |
|
24364 * value of {@link ng.$sce#trustAs $sce.trustAs}.) |
|
24365 */ |
|
24366 |
|
24367 /** |
|
24368 * @ngdoc method |
|
24369 * @name $sce#trustAsJs |
|
24370 * |
|
24371 * @description |
|
24372 * Shorthand method. `$sce.trustAsJs(value)` → |
|
24373 * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} |
|
24374 * |
|
24375 * @param {*} value The value to trustAs. |
|
24376 * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs |
|
24377 * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives |
|
24378 * only accept expressions that are either literal constants or are the |
|
24379 * return value of {@link ng.$sce#trustAs $sce.trustAs}.) |
|
24380 */ |
|
24381 |
|
24382 /** |
|
24383 * @ngdoc method |
|
24384 * @name $sce#getTrusted |
|
24385 * |
|
24386 * @description |
|
24387 * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, |
|
24388 * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the |
|
24389 * originally supplied value if the queried context type is a supertype of the created type. |
|
24390 * If this condition isn't satisfied, throws an exception. |
|
24391 * |
|
24392 * @param {string} type The kind of context in which this value is to be used. |
|
24393 * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} |
|
24394 * call. |
|
24395 * @returns {*} The value the was originally provided to |
|
24396 * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. |
|
24397 * Otherwise, throws an exception. |
|
24398 */ |
|
24399 |
|
24400 /** |
|
24401 * @ngdoc method |
|
24402 * @name $sce#getTrustedHtml |
|
24403 * |
|
24404 * @description |
|
24405 * Shorthand method. `$sce.getTrustedHtml(value)` → |
|
24406 * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} |
|
24407 * |
|
24408 * @param {*} value The value to pass to `$sce.getTrusted`. |
|
24409 * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` |
|
24410 */ |
|
24411 |
|
24412 /** |
|
24413 * @ngdoc method |
|
24414 * @name $sce#getTrustedCss |
|
24415 * |
|
24416 * @description |
|
24417 * Shorthand method. `$sce.getTrustedCss(value)` → |
|
24418 * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} |
|
24419 * |
|
24420 * @param {*} value The value to pass to `$sce.getTrusted`. |
|
24421 * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` |
|
24422 */ |
|
24423 |
|
24424 /** |
|
24425 * @ngdoc method |
|
24426 * @name $sce#getTrustedUrl |
|
24427 * |
|
24428 * @description |
|
24429 * Shorthand method. `$sce.getTrustedUrl(value)` → |
|
24430 * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} |
|
24431 * |
|
24432 * @param {*} value The value to pass to `$sce.getTrusted`. |
|
24433 * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` |
|
24434 */ |
|
24435 |
|
24436 /** |
|
24437 * @ngdoc method |
|
24438 * @name $sce#getTrustedResourceUrl |
|
24439 * |
|
24440 * @description |
|
24441 * Shorthand method. `$sce.getTrustedResourceUrl(value)` → |
|
24442 * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} |
|
24443 * |
|
24444 * @param {*} value The value to pass to `$sceDelegate.getTrusted`. |
|
24445 * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` |
|
24446 */ |
|
24447 |
|
24448 /** |
|
24449 * @ngdoc method |
|
24450 * @name $sce#getTrustedJs |
|
24451 * |
|
24452 * @description |
|
24453 * Shorthand method. `$sce.getTrustedJs(value)` → |
|
24454 * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} |
|
24455 * |
|
24456 * @param {*} value The value to pass to `$sce.getTrusted`. |
|
24457 * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` |
|
24458 */ |
|
24459 |
|
24460 /** |
|
24461 * @ngdoc method |
|
24462 * @name $sce#parseAsHtml |
|
24463 * |
|
24464 * @description |
|
24465 * Shorthand method. `$sce.parseAsHtml(expression string)` → |
|
24466 * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`} |
|
24467 * |
|
24468 * @param {string} expression String expression to compile. |
|
24469 * @returns {function(context, locals)} a function which represents the compiled expression: |
|
24470 * |
|
24471 * * `context` – `{object}` – an object against which any expressions embedded in the strings |
|
24472 * are evaluated against (typically a scope object). |
|
24473 * * `locals` – `{object=}` – local variables context object, useful for overriding values in |
|
24474 * `context`. |
|
24475 */ |
|
24476 |
|
24477 /** |
|
24478 * @ngdoc method |
|
24479 * @name $sce#parseAsCss |
|
24480 * |
|
24481 * @description |
|
24482 * Shorthand method. `$sce.parseAsCss(value)` → |
|
24483 * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`} |
|
24484 * |
|
24485 * @param {string} expression String expression to compile. |
|
24486 * @returns {function(context, locals)} a function which represents the compiled expression: |
|
24487 * |
|
24488 * * `context` – `{object}` – an object against which any expressions embedded in the strings |
|
24489 * are evaluated against (typically a scope object). |
|
24490 * * `locals` – `{object=}` – local variables context object, useful for overriding values in |
|
24491 * `context`. |
|
24492 */ |
|
24493 |
|
24494 /** |
|
24495 * @ngdoc method |
|
24496 * @name $sce#parseAsUrl |
|
24497 * |
|
24498 * @description |
|
24499 * Shorthand method. `$sce.parseAsUrl(value)` → |
|
24500 * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`} |
|
24501 * |
|
24502 * @param {string} expression String expression to compile. |
|
24503 * @returns {function(context, locals)} a function which represents the compiled expression: |
|
24504 * |
|
24505 * * `context` – `{object}` – an object against which any expressions embedded in the strings |
|
24506 * are evaluated against (typically a scope object). |
|
24507 * * `locals` – `{object=}` – local variables context object, useful for overriding values in |
|
24508 * `context`. |
|
24509 */ |
|
24510 |
|
24511 /** |
|
24512 * @ngdoc method |
|
24513 * @name $sce#parseAsResourceUrl |
|
24514 * |
|
24515 * @description |
|
24516 * Shorthand method. `$sce.parseAsResourceUrl(value)` → |
|
24517 * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`} |
|
24518 * |
|
24519 * @param {string} expression String expression to compile. |
|
24520 * @returns {function(context, locals)} a function which represents the compiled expression: |
|
24521 * |
|
24522 * * `context` – `{object}` – an object against which any expressions embedded in the strings |
|
24523 * are evaluated against (typically a scope object). |
|
24524 * * `locals` – `{object=}` – local variables context object, useful for overriding values in |
|
24525 * `context`. |
|
24526 */ |
|
24527 |
|
24528 /** |
|
24529 * @ngdoc method |
|
24530 * @name $sce#parseAsJs |
|
24531 * |
|
24532 * @description |
|
24533 * Shorthand method. `$sce.parseAsJs(value)` → |
|
24534 * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`} |
|
24535 * |
|
24536 * @param {string} expression String expression to compile. |
|
24537 * @returns {function(context, locals)} a function which represents the compiled expression: |
|
24538 * |
|
24539 * * `context` – `{object}` – an object against which any expressions embedded in the strings |
|
24540 * are evaluated against (typically a scope object). |
|
24541 * * `locals` – `{object=}` – local variables context object, useful for overriding values in |
|
24542 * `context`. |
|
24543 */ |
|
24544 |
|
24545 // Shorthand delegations. |
|
24546 var parse = sce.parseAs, |
|
24547 getTrusted = sce.getTrusted, |
|
24548 trustAs = sce.trustAs; |
|
24549 |
|
24550 forEach(SCE_CONTEXTS, function (enumValue, name) { |
|
24551 var lName = lowercase(name); |
|
24552 sce[camelCase("parse_as_" + lName)] = function (expr) { |
|
24553 return parse(enumValue, expr); |
|
24554 }; |
|
24555 sce[camelCase("get_trusted_" + lName)] = function (value) { |
|
24556 return getTrusted(enumValue, value); |
|
24557 }; |
|
24558 sce[camelCase("trust_as_" + lName)] = function (value) { |
|
24559 return trustAs(enumValue, value); |
|
24560 }; |
|
24561 }); |
|
24562 |
|
24563 return sce; |
|
24564 }]; |
|
24565 } |
|
24566 |
|
24567 /** |
|
24568 * !!! This is an undocumented "private" service !!! |
|
24569 * |
|
24570 * @name $sniffer |
|
24571 * @requires $window |
|
24572 * @requires $document |
|
24573 * |
|
24574 * @property {boolean} history Does the browser support html5 history api ? |
|
24575 * @property {boolean} transitions Does the browser support CSS transition events ? |
|
24576 * @property {boolean} animations Does the browser support CSS animation events ? |
|
24577 * |
|
24578 * @description |
|
24579 * This is very simple implementation of testing browser's features. |
|
24580 */ |
|
24581 function $SnifferProvider() { |
|
24582 this.$get = ['$window', '$document', function($window, $document) { |
|
24583 var eventSupport = {}, |
|
24584 android = |
|
24585 int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), |
|
24586 boxee = /Boxee/i.test(($window.navigator || {}).userAgent), |
|
24587 document = $document[0] || {}, |
|
24588 documentMode = document.documentMode, |
|
24589 vendorPrefix, |
|
24590 vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, |
|
24591 bodyStyle = document.body && document.body.style, |
|
24592 transitions = false, |
|
24593 animations = false, |
|
24594 match; |
|
24595 |
|
24596 if (bodyStyle) { |
|
24597 for(var prop in bodyStyle) { |
|
24598 if(match = vendorRegex.exec(prop)) { |
|
24599 vendorPrefix = match[0]; |
|
24600 vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); |
|
24601 break; |
|
24602 } |
|
24603 } |
|
24604 |
|
24605 if(!vendorPrefix) { |
|
24606 vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; |
|
24607 } |
|
24608 |
|
24609 transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); |
|
24610 animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); |
|
24611 |
|
24612 if (android && (!transitions||!animations)) { |
|
24613 transitions = isString(document.body.style.webkitTransition); |
|
24614 animations = isString(document.body.style.webkitAnimation); |
|
24615 } |
|
24616 } |
|
24617 |
|
24618 |
|
24619 return { |
|
24620 // Android has history.pushState, but it does not update location correctly |
|
24621 // so let's not use the history API at all. |
|
24622 // http://code.google.com/p/android/issues/detail?id=17471 |
|
24623 // https://github.com/angular/angular.js/issues/904 |
|
24624 |
|
24625 // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has |
|
24626 // so let's not use the history API also |
|
24627 // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined |
|
24628 // jshint -W018 |
|
24629 history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), |
|
24630 // jshint +W018 |
|
24631 hasEvent: function(event) { |
|
24632 // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have |
|
24633 // it. In particular the event is not fired when backspace or delete key are pressed or |
|
24634 // when cut operation is performed. |
|
24635 if (event == 'input' && msie == 9) return false; |
|
24636 |
|
24637 if (isUndefined(eventSupport[event])) { |
|
24638 var divElm = document.createElement('div'); |
|
24639 eventSupport[event] = 'on' + event in divElm; |
|
24640 } |
|
24641 |
|
24642 return eventSupport[event]; |
|
24643 }, |
|
24644 csp: csp(), |
|
24645 vendorPrefix: vendorPrefix, |
|
24646 transitions : transitions, |
|
24647 animations : animations, |
|
24648 android: android, |
|
24649 msie : msie, |
|
24650 msieDocumentMode: documentMode |
|
24651 }; |
|
24652 }]; |
|
24653 } |
|
24654 |
|
24655 var $compileMinErr = minErr('$compile'); |
|
24656 |
|
24657 /** |
|
24658 * @ngdoc service |
|
24659 * @name $templateRequest |
|
24660 * |
|
24661 * @description |
|
24662 * The `$templateRequest` service downloads the provided template using `$http` and, upon success, |
|
24663 * stores the contents inside of `$templateCache`. If the HTTP request fails or the response data |
|
24664 * of the HTTP request is empty then a `$compile` error will be thrown (the exception can be thwarted |
|
24665 * by setting the 2nd parameter of the function to true). |
|
24666 * |
|
24667 * @param {string} tpl The HTTP request template URL |
|
24668 * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty |
|
24669 * |
|
24670 * @return {Promise} the HTTP Promise for the given. |
|
24671 * |
|
24672 * @property {number} totalPendingRequests total amount of pending template requests being downloaded. |
|
24673 */ |
|
24674 function $TemplateRequestProvider() { |
|
24675 this.$get = ['$templateCache', '$http', '$q', function($templateCache, $http, $q) { |
|
24676 function handleRequestFn(tpl, ignoreRequestError) { |
|
24677 var self = handleRequestFn; |
|
24678 self.totalPendingRequests++; |
|
24679 |
|
24680 return $http.get(tpl, { cache : $templateCache }) |
|
24681 .then(function(response) { |
|
24682 var html = response.data; |
|
24683 if(!html || html.length === 0) { |
|
24684 return handleError(); |
|
24685 } |
|
24686 |
|
24687 self.totalPendingRequests--; |
|
24688 $templateCache.put(tpl, html); |
|
24689 return html; |
|
24690 }, handleError); |
|
24691 |
|
24692 function handleError() { |
|
24693 self.totalPendingRequests--; |
|
24694 if (!ignoreRequestError) { |
|
24695 throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl); |
|
24696 } |
|
24697 return $q.reject(); |
|
24698 } |
|
24699 } |
|
24700 |
|
24701 handleRequestFn.totalPendingRequests = 0; |
|
24702 |
|
24703 return handleRequestFn; |
|
24704 }]; |
|
24705 } |
|
24706 |
|
24707 function $$TestabilityProvider() { |
|
24708 this.$get = ['$rootScope', '$browser', '$location', |
|
24709 function($rootScope, $browser, $location) { |
|
24710 |
|
24711 /** |
|
24712 * @name $testability |
|
24713 * |
|
24714 * @description |
|
24715 * The private $$testability service provides a collection of methods for use when debugging |
|
24716 * or by automated test and debugging tools. |
|
24717 */ |
|
24718 var testability = {}; |
|
24719 |
|
24720 /** |
|
24721 * @name $$testability#findBindings |
|
24722 * |
|
24723 * @description |
|
24724 * Returns an array of elements that are bound (via ng-bind or {{}}) |
|
24725 * to expressions matching the input. |
|
24726 * |
|
24727 * @param {Element} element The element root to search from. |
|
24728 * @param {string} expression The binding expression to match. |
|
24729 * @param {boolean} opt_exactMatch If true, only returns exact matches |
|
24730 * for the expression. Filters and whitespace are ignored. |
|
24731 */ |
|
24732 testability.findBindings = function(element, expression, opt_exactMatch) { |
|
24733 var bindings = element.getElementsByClassName('ng-binding'); |
|
24734 var matches = []; |
|
24735 forEach(bindings, function(binding) { |
|
24736 var dataBinding = angular.element(binding).data('$binding'); |
|
24737 if (dataBinding) { |
|
24738 forEach(dataBinding, function(bindingName) { |
|
24739 if (opt_exactMatch) { |
|
24740 var matcher = new RegExp('(^|\\s)' + expression + '(\\s|\\||$)'); |
|
24741 if (matcher.test(bindingName)) { |
|
24742 matches.push(binding); |
|
24743 } |
|
24744 } else { |
|
24745 if (bindingName.indexOf(expression) != -1) { |
|
24746 matches.push(binding); |
|
24747 } |
|
24748 } |
|
24749 }); |
|
24750 } |
|
24751 }); |
|
24752 return matches; |
|
24753 }; |
|
24754 |
|
24755 /** |
|
24756 * @name $$testability#findModels |
|
24757 * |
|
24758 * @description |
|
24759 * Returns an array of elements that are two-way found via ng-model to |
|
24760 * expressions matching the input. |
|
24761 * |
|
24762 * @param {Element} element The element root to search from. |
|
24763 * @param {string} expression The model expression to match. |
|
24764 * @param {boolean} opt_exactMatch If true, only returns exact matches |
|
24765 * for the expression. |
|
24766 */ |
|
24767 testability.findModels = function(element, expression, opt_exactMatch) { |
|
24768 var prefixes = ['ng-', 'data-ng-', 'ng\\:']; |
|
24769 for (var p = 0; p < prefixes.length; ++p) { |
|
24770 var attributeEquals = opt_exactMatch ? '=' : '*='; |
|
24771 var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]'; |
|
24772 var elements = element.querySelectorAll(selector); |
|
24773 if (elements.length) { |
|
24774 return elements; |
|
24775 } |
|
24776 } |
|
24777 }; |
|
24778 |
|
24779 /** |
|
24780 * @name $$testability#getLocation |
|
24781 * |
|
24782 * @description |
|
24783 * Shortcut for getting the location in a browser agnostic way. Returns |
|
24784 * the path, search, and hash. (e.g. /path?a=b#hash) |
|
24785 */ |
|
24786 testability.getLocation = function() { |
|
24787 return $location.url(); |
|
24788 }; |
|
24789 |
|
24790 /** |
|
24791 * @name $$testability#setLocation |
|
24792 * |
|
24793 * @description |
|
24794 * Shortcut for navigating to a location without doing a full page reload. |
|
24795 * |
|
24796 * @param {string} url The location url (path, search and hash, |
|
24797 * e.g. /path?a=b#hash) to go to. |
|
24798 */ |
|
24799 testability.setLocation = function(url) { |
|
24800 if (url !== $location.url()) { |
|
24801 $location.url(url); |
|
24802 $rootScope.$digest(); |
|
24803 } |
|
24804 }; |
|
24805 |
|
24806 /** |
|
24807 * @name $$testability#whenStable |
|
24808 * |
|
24809 * @description |
|
24810 * Calls the callback when $timeout and $http requests are completed. |
|
24811 * |
|
24812 * @param {function} callback |
|
24813 */ |
|
24814 testability.whenStable = function(callback) { |
|
24815 $browser.notifyWhenNoOutstandingRequests(callback); |
|
24816 }; |
|
24817 |
|
24818 return testability; |
|
24819 }]; |
|
24820 } |
|
24821 |
|
24822 function $TimeoutProvider() { |
|
24823 this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', |
|
24824 function($rootScope, $browser, $q, $$q, $exceptionHandler) { |
|
24825 var deferreds = {}; |
|
24826 |
|
24827 |
|
24828 /** |
|
24829 * @ngdoc service |
|
24830 * @name $timeout |
|
24831 * |
|
24832 * @description |
|
24833 * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch |
|
24834 * block and delegates any exceptions to |
|
24835 * {@link ng.$exceptionHandler $exceptionHandler} service. |
|
24836 * |
|
24837 * The return value of registering a timeout function is a promise, which will be resolved when |
|
24838 * the timeout is reached and the timeout function is executed. |
|
24839 * |
|
24840 * To cancel a timeout request, call `$timeout.cancel(promise)`. |
|
24841 * |
|
24842 * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to |
|
24843 * synchronously flush the queue of deferred functions. |
|
24844 * |
|
24845 * @param {function()} fn A function, whose execution should be delayed. |
|
24846 * @param {number=} [delay=0] Delay in milliseconds. |
|
24847 * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise |
|
24848 * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. |
|
24849 * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this |
|
24850 * promise will be resolved with is the return value of the `fn` function. |
|
24851 * |
|
24852 */ |
|
24853 function timeout(fn, delay, invokeApply) { |
|
24854 var skipApply = (isDefined(invokeApply) && !invokeApply), |
|
24855 deferred = (skipApply ? $$q : $q).defer(), |
|
24856 promise = deferred.promise, |
|
24857 timeoutId; |
|
24858 |
|
24859 timeoutId = $browser.defer(function() { |
|
24860 try { |
|
24861 deferred.resolve(fn()); |
|
24862 } catch(e) { |
|
24863 deferred.reject(e); |
|
24864 $exceptionHandler(e); |
|
24865 } |
|
24866 finally { |
|
24867 delete deferreds[promise.$$timeoutId]; |
|
24868 } |
|
24869 |
|
24870 if (!skipApply) $rootScope.$apply(); |
|
24871 }, delay); |
|
24872 |
|
24873 promise.$$timeoutId = timeoutId; |
|
24874 deferreds[timeoutId] = deferred; |
|
24875 |
|
24876 return promise; |
|
24877 } |
|
24878 |
|
24879 |
|
24880 /** |
|
24881 * @ngdoc method |
|
24882 * @name $timeout#cancel |
|
24883 * |
|
24884 * @description |
|
24885 * Cancels a task associated with the `promise`. As a result of this, the promise will be |
|
24886 * resolved with a rejection. |
|
24887 * |
|
24888 * @param {Promise=} promise Promise returned by the `$timeout` function. |
|
24889 * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully |
|
24890 * canceled. |
|
24891 */ |
|
24892 timeout.cancel = function(promise) { |
|
24893 if (promise && promise.$$timeoutId in deferreds) { |
|
24894 deferreds[promise.$$timeoutId].reject('canceled'); |
|
24895 delete deferreds[promise.$$timeoutId]; |
|
24896 return $browser.defer.cancel(promise.$$timeoutId); |
|
24897 } |
|
24898 return false; |
|
24899 }; |
|
24900 |
|
24901 return timeout; |
|
24902 }]; |
|
24903 } |
|
24904 |
|
24905 // NOTE: The usage of window and document instead of $window and $document here is |
|
24906 // deliberate. This service depends on the specific behavior of anchor nodes created by the |
|
24907 // browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and |
|
24908 // cause us to break tests. In addition, when the browser resolves a URL for XHR, it |
|
24909 // doesn't know about mocked locations and resolves URLs to the real document - which is |
|
24910 // exactly the behavior needed here. There is little value is mocking these out for this |
|
24911 // service. |
|
24912 var urlParsingNode = document.createElement("a"); |
|
24913 var originUrl = urlResolve(window.location.href, true); |
|
24914 |
|
24915 |
|
24916 /** |
|
24917 * |
|
24918 * Implementation Notes for non-IE browsers |
|
24919 * ---------------------------------------- |
|
24920 * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, |
|
24921 * results both in the normalizing and parsing of the URL. Normalizing means that a relative |
|
24922 * URL will be resolved into an absolute URL in the context of the application document. |
|
24923 * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related |
|
24924 * properties are all populated to reflect the normalized URL. This approach has wide |
|
24925 * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See |
|
24926 * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html |
|
24927 * |
|
24928 * Implementation Notes for IE |
|
24929 * --------------------------- |
|
24930 * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other |
|
24931 * browsers. However, the parsed components will not be set if the URL assigned did not specify |
|
24932 * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We |
|
24933 * work around that by performing the parsing in a 2nd step by taking a previously normalized |
|
24934 * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the |
|
24935 * properties such as protocol, hostname, port, etc. |
|
24936 * |
|
24937 * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one |
|
24938 * uses the inner HTML approach to assign the URL as part of an HTML snippet - |
|
24939 * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. |
|
24940 * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. |
|
24941 * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that |
|
24942 * method and IE < 8 is unsupported. |
|
24943 * |
|
24944 * References: |
|
24945 * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement |
|
24946 * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html |
|
24947 * http://url.spec.whatwg.org/#urlutils |
|
24948 * https://github.com/angular/angular.js/pull/2902 |
|
24949 * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ |
|
24950 * |
|
24951 * @kind function |
|
24952 * @param {string} url The URL to be parsed. |
|
24953 * @description Normalizes and parses a URL. |
|
24954 * @returns {object} Returns the normalized URL as a dictionary. |
|
24955 * |
|
24956 * | member name | Description | |
|
24957 * |---------------|----------------| |
|
24958 * | href | A normalized version of the provided URL if it was not an absolute URL | |
|
24959 * | protocol | The protocol including the trailing colon | |
|
24960 * | host | The host and port (if the port is non-default) of the normalizedUrl | |
|
24961 * | search | The search params, minus the question mark | |
|
24962 * | hash | The hash string, minus the hash symbol |
|
24963 * | hostname | The hostname |
|
24964 * | port | The port, without ":" |
|
24965 * | pathname | The pathname, beginning with "/" |
|
24966 * |
|
24967 */ |
|
24968 function urlResolve(url, base) { |
|
24969 var href = url; |
|
24970 |
|
24971 if (msie) { |
|
24972 // Normalize before parse. Refer Implementation Notes on why this is |
|
24973 // done in two steps on IE. |
|
24974 urlParsingNode.setAttribute("href", href); |
|
24975 href = urlParsingNode.href; |
|
24976 } |
|
24977 |
|
24978 urlParsingNode.setAttribute('href', href); |
|
24979 |
|
24980 // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils |
|
24981 return { |
|
24982 href: urlParsingNode.href, |
|
24983 protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', |
|
24984 host: urlParsingNode.host, |
|
24985 search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', |
|
24986 hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', |
|
24987 hostname: urlParsingNode.hostname, |
|
24988 port: urlParsingNode.port, |
|
24989 pathname: (urlParsingNode.pathname.charAt(0) === '/') |
|
24990 ? urlParsingNode.pathname |
|
24991 : '/' + urlParsingNode.pathname |
|
24992 }; |
|
24993 } |
|
24994 |
|
24995 /** |
|
24996 * Parse a request URL and determine whether this is a same-origin request as the application document. |
|
24997 * |
|
24998 * @param {string|object} requestUrl The url of the request as a string that will be resolved |
|
24999 * or a parsed URL object. |
|
25000 * @returns {boolean} Whether the request is for the same origin as the application document. |
|
25001 */ |
|
25002 function urlIsSameOrigin(requestUrl) { |
|
25003 var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; |
|
25004 return (parsed.protocol === originUrl.protocol && |
|
25005 parsed.host === originUrl.host); |
|
25006 } |
|
25007 |
|
25008 /** |
|
25009 * @ngdoc service |
|
25010 * @name $window |
|
25011 * |
|
25012 * @description |
|
25013 * A reference to the browser's `window` object. While `window` |
|
25014 * is globally available in JavaScript, it causes testability problems, because |
|
25015 * it is a global variable. In angular we always refer to it through the |
|
25016 * `$window` service, so it may be overridden, removed or mocked for testing. |
|
25017 * |
|
25018 * Expressions, like the one defined for the `ngClick` directive in the example |
|
25019 * below, are evaluated with respect to the current scope. Therefore, there is |
|
25020 * no risk of inadvertently coding in a dependency on a global value in such an |
|
25021 * expression. |
|
25022 * |
|
25023 * @example |
|
25024 <example module="windowExample"> |
|
25025 <file name="index.html"> |
|
25026 <script> |
|
25027 angular.module('windowExample', []) |
|
25028 .controller('ExampleController', ['$scope', '$window', function ($scope, $window) { |
|
25029 $scope.greeting = 'Hello, World!'; |
|
25030 $scope.doGreeting = function(greeting) { |
|
25031 $window.alert(greeting); |
|
25032 }; |
|
25033 }]); |
|
25034 </script> |
|
25035 <div ng-controller="ExampleController"> |
|
25036 <input type="text" ng-model="greeting" /> |
|
25037 <button ng-click="doGreeting(greeting)">ALERT</button> |
|
25038 </div> |
|
25039 </file> |
|
25040 <file name="protractor.js" type="protractor"> |
|
25041 it('should display the greeting in the input box', function() { |
|
25042 element(by.model('greeting')).sendKeys('Hello, E2E Tests'); |
|
25043 // If we click the button it will block the test runner |
|
25044 // element(':button').click(); |
|
25045 }); |
|
25046 </file> |
|
25047 </example> |
|
25048 */ |
|
25049 function $WindowProvider(){ |
|
25050 this.$get = valueFn(window); |
|
25051 } |
|
25052 |
|
25053 /* global currencyFilter: true, |
|
25054 dateFilter: true, |
|
25055 filterFilter: true, |
|
25056 jsonFilter: true, |
|
25057 limitToFilter: true, |
|
25058 lowercaseFilter: true, |
|
25059 numberFilter: true, |
|
25060 orderByFilter: true, |
|
25061 uppercaseFilter: true, |
|
25062 */ |
|
25063 |
|
25064 /** |
|
25065 * @ngdoc provider |
|
25066 * @name $filterProvider |
|
25067 * @description |
|
25068 * |
|
25069 * Filters are just functions which transform input to an output. However filters need to be |
|
25070 * Dependency Injected. To achieve this a filter definition consists of a factory function which is |
|
25071 * annotated with dependencies and is responsible for creating a filter function. |
|
25072 * |
|
25073 * ```js |
|
25074 * // Filter registration |
|
25075 * function MyModule($provide, $filterProvider) { |
|
25076 * // create a service to demonstrate injection (not always needed) |
|
25077 * $provide.value('greet', function(name){ |
|
25078 * return 'Hello ' + name + '!'; |
|
25079 * }); |
|
25080 * |
|
25081 * // register a filter factory which uses the |
|
25082 * // greet service to demonstrate DI. |
|
25083 * $filterProvider.register('greet', function(greet){ |
|
25084 * // return the filter function which uses the greet service |
|
25085 * // to generate salutation |
|
25086 * return function(text) { |
|
25087 * // filters need to be forgiving so check input validity |
|
25088 * return text && greet(text) || text; |
|
25089 * }; |
|
25090 * }); |
|
25091 * } |
|
25092 * ``` |
|
25093 * |
|
25094 * The filter function is registered with the `$injector` under the filter name suffix with |
|
25095 * `Filter`. |
|
25096 * |
|
25097 * ```js |
|
25098 * it('should be the same instance', inject( |
|
25099 * function($filterProvider) { |
|
25100 * $filterProvider.register('reverse', function(){ |
|
25101 * return ...; |
|
25102 * }); |
|
25103 * }, |
|
25104 * function($filter, reverseFilter) { |
|
25105 * expect($filter('reverse')).toBe(reverseFilter); |
|
25106 * }); |
|
25107 * ``` |
|
25108 * |
|
25109 * |
|
25110 * For more information about how angular filters work, and how to create your own filters, see |
|
25111 * {@link guide/filter Filters} in the Angular Developer Guide. |
|
25112 */ |
|
25113 |
|
25114 /** |
|
25115 * @ngdoc service |
|
25116 * @name $filter |
|
25117 * @kind function |
|
25118 * @description |
|
25119 * Filters are used for formatting data displayed to the user. |
|
25120 * |
|
25121 * The general syntax in templates is as follows: |
|
25122 * |
|
25123 * {{ expression [| filter_name[:parameter_value] ... ] }} |
|
25124 * |
|
25125 * @param {String} name Name of the filter function to retrieve |
|
25126 * @return {Function} the filter function |
|
25127 * @example |
|
25128 <example name="$filter" module="filterExample"> |
|
25129 <file name="index.html"> |
|
25130 <div ng-controller="MainCtrl"> |
|
25131 <h3>{{ originalText }}</h3> |
|
25132 <h3>{{ filteredText }}</h3> |
|
25133 </div> |
|
25134 </file> |
|
25135 |
|
25136 <file name="script.js"> |
|
25137 angular.module('filterExample', []) |
|
25138 .controller('MainCtrl', function($scope, $filter) { |
|
25139 $scope.originalText = 'hello'; |
|
25140 $scope.filteredText = $filter('uppercase')($scope.originalText); |
|
25141 }); |
|
25142 </file> |
|
25143 </example> |
|
25144 */ |
|
25145 $FilterProvider.$inject = ['$provide']; |
|
25146 function $FilterProvider($provide) { |
|
25147 var suffix = 'Filter'; |
|
25148 |
|
25149 /** |
|
25150 * @ngdoc method |
|
25151 * @name $filterProvider#register |
|
25152 * @param {string|Object} name Name of the filter function, or an object map of filters where |
|
25153 * the keys are the filter names and the values are the filter factories. |
|
25154 * @returns {Object} Registered filter instance, or if a map of filters was provided then a map |
|
25155 * of the registered filter instances. |
|
25156 */ |
|
25157 function register(name, factory) { |
|
25158 if(isObject(name)) { |
|
25159 var filters = {}; |
|
25160 forEach(name, function(filter, key) { |
|
25161 filters[key] = register(key, filter); |
|
25162 }); |
|
25163 return filters; |
|
25164 } else { |
|
25165 return $provide.factory(name + suffix, factory); |
|
25166 } |
|
25167 } |
|
25168 this.register = register; |
|
25169 |
|
25170 this.$get = ['$injector', function($injector) { |
|
25171 return function(name) { |
|
25172 return $injector.get(name + suffix); |
|
25173 }; |
|
25174 }]; |
|
25175 |
|
25176 //////////////////////////////////////// |
|
25177 |
|
25178 /* global |
|
25179 currencyFilter: false, |
|
25180 dateFilter: false, |
|
25181 filterFilter: false, |
|
25182 jsonFilter: false, |
|
25183 limitToFilter: false, |
|
25184 lowercaseFilter: false, |
|
25185 numberFilter: false, |
|
25186 orderByFilter: false, |
|
25187 uppercaseFilter: false, |
|
25188 */ |
|
25189 |
|
25190 register('currency', currencyFilter); |
|
25191 register('date', dateFilter); |
|
25192 register('filter', filterFilter); |
|
25193 register('json', jsonFilter); |
|
25194 register('limitTo', limitToFilter); |
|
25195 register('lowercase', lowercaseFilter); |
|
25196 register('number', numberFilter); |
|
25197 register('orderBy', orderByFilter); |
|
25198 register('uppercase', uppercaseFilter); |
|
25199 } |
|
25200 |
|
25201 /** |
|
25202 * @ngdoc filter |
|
25203 * @name filter |
|
25204 * @kind function |
|
25205 * |
|
25206 * @description |
|
25207 * Selects a subset of items from `array` and returns it as a new array. |
|
25208 * |
|
25209 * @param {Array} array The source array. |
|
25210 * @param {string|Object|function()} expression The predicate to be used for selecting items from |
|
25211 * `array`. |
|
25212 * |
|
25213 * Can be one of: |
|
25214 * |
|
25215 * - `string`: The string is evaluated as an expression and the resulting value is used for substring match against |
|
25216 * the contents of the `array`. All strings or objects with string properties in `array` that contain this string |
|
25217 * will be returned. The predicate can be negated by prefixing the string with `!`. |
|
25218 * |
|
25219 * - `Object`: A pattern object can be used to filter specific properties on objects contained |
|
25220 * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items |
|
25221 * which have property `name` containing "M" and property `phone` containing "1". A special |
|
25222 * property name `$` can be used (as in `{$:"text"}`) to accept a match against any |
|
25223 * property of the object. That's equivalent to the simple substring match with a `string` |
|
25224 * as described above. The predicate can be negated by prefixing the string with `!`. |
|
25225 * For Example `{name: "!M"}` predicate will return an array of items which have property `name` |
|
25226 * not containing "M". |
|
25227 * |
|
25228 * - `function(value, index)`: A predicate function can be used to write arbitrary filters. The |
|
25229 * function is called for each element of `array`. The final result is an array of those |
|
25230 * elements that the predicate returned true for. |
|
25231 * |
|
25232 * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in |
|
25233 * determining if the expected value (from the filter expression) and actual value (from |
|
25234 * the object in the array) should be considered a match. |
|
25235 * |
|
25236 * Can be one of: |
|
25237 * |
|
25238 * - `function(actual, expected)`: |
|
25239 * The function will be given the object value and the predicate value to compare and |
|
25240 * should return true if the item should be included in filtered result. |
|
25241 * |
|
25242 * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. |
|
25243 * this is essentially strict comparison of expected and actual. |
|
25244 * |
|
25245 * - `false|undefined`: A short hand for a function which will look for a substring match in case |
|
25246 * insensitive way. |
|
25247 * |
|
25248 * @example |
|
25249 <example> |
|
25250 <file name="index.html"> |
|
25251 <div ng-init="friends = [{name:'John', phone:'555-1276'}, |
|
25252 {name:'Mary', phone:'800-BIG-MARY'}, |
|
25253 {name:'Mike', phone:'555-4321'}, |
|
25254 {name:'Adam', phone:'555-5678'}, |
|
25255 {name:'Julie', phone:'555-8765'}, |
|
25256 {name:'Juliette', phone:'555-5678'}]"></div> |
|
25257 |
|
25258 Search: <input ng-model="searchText"> |
|
25259 <table id="searchTextResults"> |
|
25260 <tr><th>Name</th><th>Phone</th></tr> |
|
25261 <tr ng-repeat="friend in friends | filter:searchText"> |
|
25262 <td>{{friend.name}}</td> |
|
25263 <td>{{friend.phone}}</td> |
|
25264 </tr> |
|
25265 </table> |
|
25266 <hr> |
|
25267 Any: <input ng-model="search.$"> <br> |
|
25268 Name only <input ng-model="search.name"><br> |
|
25269 Phone only <input ng-model="search.phone"><br> |
|
25270 Equality <input type="checkbox" ng-model="strict"><br> |
|
25271 <table id="searchObjResults"> |
|
25272 <tr><th>Name</th><th>Phone</th></tr> |
|
25273 <tr ng-repeat="friendObj in friends | filter:search:strict"> |
|
25274 <td>{{friendObj.name}}</td> |
|
25275 <td>{{friendObj.phone}}</td> |
|
25276 </tr> |
|
25277 </table> |
|
25278 </file> |
|
25279 <file name="protractor.js" type="protractor"> |
|
25280 var expectFriendNames = function(expectedNames, key) { |
|
25281 element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) { |
|
25282 arr.forEach(function(wd, i) { |
|
25283 expect(wd.getText()).toMatch(expectedNames[i]); |
|
25284 }); |
|
25285 }); |
|
25286 }; |
|
25287 |
|
25288 it('should search across all fields when filtering with a string', function() { |
|
25289 var searchText = element(by.model('searchText')); |
|
25290 searchText.clear(); |
|
25291 searchText.sendKeys('m'); |
|
25292 expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend'); |
|
25293 |
|
25294 searchText.clear(); |
|
25295 searchText.sendKeys('76'); |
|
25296 expectFriendNames(['John', 'Julie'], 'friend'); |
|
25297 }); |
|
25298 |
|
25299 it('should search in specific fields when filtering with a predicate object', function() { |
|
25300 var searchAny = element(by.model('search.$')); |
|
25301 searchAny.clear(); |
|
25302 searchAny.sendKeys('i'); |
|
25303 expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj'); |
|
25304 }); |
|
25305 it('should use a equal comparison when comparator is true', function() { |
|
25306 var searchName = element(by.model('search.name')); |
|
25307 var strict = element(by.model('strict')); |
|
25308 searchName.clear(); |
|
25309 searchName.sendKeys('Julie'); |
|
25310 strict.click(); |
|
25311 expectFriendNames(['Julie'], 'friendObj'); |
|
25312 }); |
|
25313 </file> |
|
25314 </example> |
|
25315 */ |
|
25316 function filterFilter() { |
|
25317 return function(array, expression, comparator) { |
|
25318 if (!isArray(array)) return array; |
|
25319 |
|
25320 var comparatorType = typeof(comparator), |
|
25321 predicates = []; |
|
25322 |
|
25323 predicates.check = function(value, index) { |
|
25324 for (var j = 0; j < predicates.length; j++) { |
|
25325 if(!predicates[j](value, index)) { |
|
25326 return false; |
|
25327 } |
|
25328 } |
|
25329 return true; |
|
25330 }; |
|
25331 |
|
25332 if (comparatorType !== 'function') { |
|
25333 if (comparatorType === 'boolean' && comparator) { |
|
25334 comparator = function(obj, text) { |
|
25335 return angular.equals(obj, text); |
|
25336 }; |
|
25337 } else { |
|
25338 comparator = function(obj, text) { |
|
25339 if (obj && text && typeof obj === 'object' && typeof text === 'object') { |
|
25340 for (var objKey in obj) { |
|
25341 if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) && |
|
25342 comparator(obj[objKey], text[objKey])) { |
|
25343 return true; |
|
25344 } |
|
25345 } |
|
25346 return false; |
|
25347 } |
|
25348 text = (''+text).toLowerCase(); |
|
25349 return (''+obj).toLowerCase().indexOf(text) > -1; |
|
25350 }; |
|
25351 } |
|
25352 } |
|
25353 |
|
25354 var search = function(obj, text){ |
|
25355 if (typeof text == 'string' && text.charAt(0) === '!') { |
|
25356 return !search(obj, text.substr(1)); |
|
25357 } |
|
25358 switch (typeof obj) { |
|
25359 case "boolean": |
|
25360 case "number": |
|
25361 case "string": |
|
25362 return comparator(obj, text); |
|
25363 case "object": |
|
25364 switch (typeof text) { |
|
25365 case "object": |
|
25366 return comparator(obj, text); |
|
25367 default: |
|
25368 for ( var objKey in obj) { |
|
25369 if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { |
|
25370 return true; |
|
25371 } |
|
25372 } |
|
25373 break; |
|
25374 } |
|
25375 return false; |
|
25376 case "array": |
|
25377 for ( var i = 0; i < obj.length; i++) { |
|
25378 if (search(obj[i], text)) { |
|
25379 return true; |
|
25380 } |
|
25381 } |
|
25382 return false; |
|
25383 default: |
|
25384 return false; |
|
25385 } |
|
25386 }; |
|
25387 switch (typeof expression) { |
|
25388 case "boolean": |
|
25389 case "number": |
|
25390 case "string": |
|
25391 // Set up expression object and fall through |
|
25392 expression = {$:expression}; |
|
25393 // jshint -W086 |
|
25394 case "object": |
|
25395 // jshint +W086 |
|
25396 for (var key in expression) { |
|
25397 (function(path) { |
|
25398 if (typeof expression[path] === 'undefined') return; |
|
25399 predicates.push(function(value) { |
|
25400 return search(path == '$' ? value : (value && value[path]), expression[path]); |
|
25401 }); |
|
25402 })(key); |
|
25403 } |
|
25404 break; |
|
25405 case 'function': |
|
25406 predicates.push(expression); |
|
25407 break; |
|
25408 default: |
|
25409 return array; |
|
25410 } |
|
25411 var filtered = []; |
|
25412 for ( var j = 0; j < array.length; j++) { |
|
25413 var value = array[j]; |
|
25414 if (predicates.check(value, j)) { |
|
25415 filtered.push(value); |
|
25416 } |
|
25417 } |
|
25418 return filtered; |
|
25419 }; |
|
25420 } |
|
25421 |
|
25422 /** |
|
25423 * @ngdoc filter |
|
25424 * @name currency |
|
25425 * @kind function |
|
25426 * |
|
25427 * @description |
|
25428 * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default |
|
25429 * symbol for current locale is used. |
|
25430 * |
|
25431 * @param {number} amount Input to filter. |
|
25432 * @param {string=} symbol Currency symbol or identifier to be displayed. |
|
25433 * @returns {string} Formatted number. |
|
25434 * |
|
25435 * |
|
25436 * @example |
|
25437 <example module="currencyExample"> |
|
25438 <file name="index.html"> |
|
25439 <script> |
|
25440 angular.module('currencyExample', []) |
|
25441 .controller('ExampleController', ['$scope', function($scope) { |
|
25442 $scope.amount = 1234.56; |
|
25443 }]); |
|
25444 </script> |
|
25445 <div ng-controller="ExampleController"> |
|
25446 <input type="number" ng-model="amount"> <br> |
|
25447 default currency symbol ($): <span id="currency-default">{{amount | currency}}</span><br> |
|
25448 custom currency identifier (USD$): <span>{{amount | currency:"USD$"}}</span> |
|
25449 </div> |
|
25450 </file> |
|
25451 <file name="protractor.js" type="protractor"> |
|
25452 it('should init with 1234.56', function() { |
|
25453 expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); |
|
25454 expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('USD$1,234.56'); |
|
25455 }); |
|
25456 it('should update', function() { |
|
25457 if (browser.params.browser == 'safari') { |
|
25458 // Safari does not understand the minus key. See |
|
25459 // https://github.com/angular/protractor/issues/481 |
|
25460 return; |
|
25461 } |
|
25462 element(by.model('amount')).clear(); |
|
25463 element(by.model('amount')).sendKeys('-1234'); |
|
25464 expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)'); |
|
25465 expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('(USD$1,234.00)'); |
|
25466 }); |
|
25467 </file> |
|
25468 </example> |
|
25469 */ |
|
25470 currencyFilter.$inject = ['$locale']; |
|
25471 function currencyFilter($locale) { |
|
25472 var formats = $locale.NUMBER_FORMATS; |
|
25473 return function(amount, currencySymbol){ |
|
25474 if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; |
|
25475 |
|
25476 // if null or undefined pass it through |
|
25477 return (amount == null) |
|
25478 ? amount |
|
25479 : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). |
|
25480 replace(/\u00A4/g, currencySymbol); |
|
25481 }; |
|
25482 } |
|
25483 |
|
25484 /** |
|
25485 * @ngdoc filter |
|
25486 * @name number |
|
25487 * @kind function |
|
25488 * |
|
25489 * @description |
|
25490 * Formats a number as text. |
|
25491 * |
|
25492 * If the input is not a number an empty string is returned. |
|
25493 * |
|
25494 * @param {number|string} number Number to format. |
|
25495 * @param {(number|string)=} fractionSize Number of decimal places to round the number to. |
|
25496 * If this is not provided then the fraction size is computed from the current locale's number |
|
25497 * formatting pattern. In the case of the default locale, it will be 3. |
|
25498 * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. |
|
25499 * |
|
25500 * @example |
|
25501 <example module="numberFilterExample"> |
|
25502 <file name="index.html"> |
|
25503 <script> |
|
25504 angular.module('numberFilterExample', []) |
|
25505 .controller('ExampleController', ['$scope', function($scope) { |
|
25506 $scope.val = 1234.56789; |
|
25507 }]); |
|
25508 </script> |
|
25509 <div ng-controller="ExampleController"> |
|
25510 Enter number: <input ng-model='val'><br> |
|
25511 Default formatting: <span id='number-default'>{{val | number}}</span><br> |
|
25512 No fractions: <span>{{val | number:0}}</span><br> |
|
25513 Negative number: <span>{{-val | number:4}}</span> |
|
25514 </div> |
|
25515 </file> |
|
25516 <file name="protractor.js" type="protractor"> |
|
25517 it('should format numbers', function() { |
|
25518 expect(element(by.id('number-default')).getText()).toBe('1,234.568'); |
|
25519 expect(element(by.binding('val | number:0')).getText()).toBe('1,235'); |
|
25520 expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679'); |
|
25521 }); |
|
25522 |
|
25523 it('should update', function() { |
|
25524 element(by.model('val')).clear(); |
|
25525 element(by.model('val')).sendKeys('3374.333'); |
|
25526 expect(element(by.id('number-default')).getText()).toBe('3,374.333'); |
|
25527 expect(element(by.binding('val | number:0')).getText()).toBe('3,374'); |
|
25528 expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330'); |
|
25529 }); |
|
25530 </file> |
|
25531 </example> |
|
25532 */ |
|
25533 |
|
25534 |
|
25535 numberFilter.$inject = ['$locale']; |
|
25536 function numberFilter($locale) { |
|
25537 var formats = $locale.NUMBER_FORMATS; |
|
25538 return function(number, fractionSize) { |
|
25539 |
|
25540 // if null or undefined pass it through |
|
25541 return (number == null) |
|
25542 ? number |
|
25543 : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, |
|
25544 fractionSize); |
|
25545 }; |
|
25546 } |
|
25547 |
|
25548 var DECIMAL_SEP = '.'; |
|
25549 function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { |
|
25550 if (!isFinite(number) || isObject(number)) return ''; |
|
25551 |
|
25552 var isNegative = number < 0; |
|
25553 number = Math.abs(number); |
|
25554 var numStr = number + '', |
|
25555 formatedText = '', |
|
25556 parts = []; |
|
25557 |
|
25558 var hasExponent = false; |
|
25559 if (numStr.indexOf('e') !== -1) { |
|
25560 var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); |
|
25561 if (match && match[2] == '-' && match[3] > fractionSize + 1) { |
|
25562 numStr = '0'; |
|
25563 number = 0; |
|
25564 } else { |
|
25565 formatedText = numStr; |
|
25566 hasExponent = true; |
|
25567 } |
|
25568 } |
|
25569 |
|
25570 if (!hasExponent) { |
|
25571 var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; |
|
25572 |
|
25573 // determine fractionSize if it is not specified |
|
25574 if (isUndefined(fractionSize)) { |
|
25575 fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); |
|
25576 } |
|
25577 |
|
25578 // safely round numbers in JS without hitting imprecisions of floating-point arithmetics |
|
25579 // inspired by: |
|
25580 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round |
|
25581 number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize); |
|
25582 |
|
25583 if (number === 0) { |
|
25584 isNegative = false; |
|
25585 } |
|
25586 |
|
25587 var fraction = ('' + number).split(DECIMAL_SEP); |
|
25588 var whole = fraction[0]; |
|
25589 fraction = fraction[1] || ''; |
|
25590 |
|
25591 var i, pos = 0, |
|
25592 lgroup = pattern.lgSize, |
|
25593 group = pattern.gSize; |
|
25594 |
|
25595 if (whole.length >= (lgroup + group)) { |
|
25596 pos = whole.length - lgroup; |
|
25597 for (i = 0; i < pos; i++) { |
|
25598 if ((pos - i)%group === 0 && i !== 0) { |
|
25599 formatedText += groupSep; |
|
25600 } |
|
25601 formatedText += whole.charAt(i); |
|
25602 } |
|
25603 } |
|
25604 |
|
25605 for (i = pos; i < whole.length; i++) { |
|
25606 if ((whole.length - i)%lgroup === 0 && i !== 0) { |
|
25607 formatedText += groupSep; |
|
25608 } |
|
25609 formatedText += whole.charAt(i); |
|
25610 } |
|
25611 |
|
25612 // format fraction part. |
|
25613 while(fraction.length < fractionSize) { |
|
25614 fraction += '0'; |
|
25615 } |
|
25616 |
|
25617 if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); |
|
25618 } else { |
|
25619 |
|
25620 if (fractionSize > 0 && number > -1 && number < 1) { |
|
25621 formatedText = number.toFixed(fractionSize); |
|
25622 } |
|
25623 } |
|
25624 |
|
25625 parts.push(isNegative ? pattern.negPre : pattern.posPre); |
|
25626 parts.push(formatedText); |
|
25627 parts.push(isNegative ? pattern.negSuf : pattern.posSuf); |
|
25628 return parts.join(''); |
|
25629 } |
|
25630 |
|
25631 function padNumber(num, digits, trim) { |
|
25632 var neg = ''; |
|
25633 if (num < 0) { |
|
25634 neg = '-'; |
|
25635 num = -num; |
|
25636 } |
|
25637 num = '' + num; |
|
25638 while(num.length < digits) num = '0' + num; |
|
25639 if (trim) |
|
25640 num = num.substr(num.length - digits); |
|
25641 return neg + num; |
|
25642 } |
|
25643 |
|
25644 |
|
25645 function dateGetter(name, size, offset, trim) { |
|
25646 offset = offset || 0; |
|
25647 return function(date) { |
|
25648 var value = date['get' + name](); |
|
25649 if (offset > 0 || value > -offset) |
|
25650 value += offset; |
|
25651 if (value === 0 && offset == -12 ) value = 12; |
|
25652 return padNumber(value, size, trim); |
|
25653 }; |
|
25654 } |
|
25655 |
|
25656 function dateStrGetter(name, shortForm) { |
|
25657 return function(date, formats) { |
|
25658 var value = date['get' + name](); |
|
25659 var get = uppercase(shortForm ? ('SHORT' + name) : name); |
|
25660 |
|
25661 return formats[get][value]; |
|
25662 }; |
|
25663 } |
|
25664 |
|
25665 function timeZoneGetter(date) { |
|
25666 var zone = -1 * date.getTimezoneOffset(); |
|
25667 var paddedZone = (zone >= 0) ? "+" : ""; |
|
25668 |
|
25669 paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + |
|
25670 padNumber(Math.abs(zone % 60), 2); |
|
25671 |
|
25672 return paddedZone; |
|
25673 } |
|
25674 |
|
25675 function getFirstThursdayOfYear(year) { |
|
25676 // 0 = index of January |
|
25677 var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay(); |
|
25678 // 4 = index of Thursday (+1 to account for 1st = 5) |
|
25679 // 11 = index of *next* Thursday (+1 account for 1st = 12) |
|
25680 return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst); |
|
25681 } |
|
25682 |
|
25683 function getThursdayThisWeek(datetime) { |
|
25684 return new Date(datetime.getFullYear(), datetime.getMonth(), |
|
25685 // 4 = index of Thursday |
|
25686 datetime.getDate() + (4 - datetime.getDay())); |
|
25687 } |
|
25688 |
|
25689 function weekGetter(size) { |
|
25690 return function(date) { |
|
25691 var firstThurs = getFirstThursdayOfYear(date.getFullYear()), |
|
25692 thisThurs = getThursdayThisWeek(date); |
|
25693 |
|
25694 var diff = +thisThurs - +firstThurs, |
|
25695 result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week |
|
25696 |
|
25697 return padNumber(result, size); |
|
25698 }; |
|
25699 } |
|
25700 |
|
25701 function ampmGetter(date, formats) { |
|
25702 return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; |
|
25703 } |
|
25704 |
|
25705 var DATE_FORMATS = { |
|
25706 yyyy: dateGetter('FullYear', 4), |
|
25707 yy: dateGetter('FullYear', 2, 0, true), |
|
25708 y: dateGetter('FullYear', 1), |
|
25709 MMMM: dateStrGetter('Month'), |
|
25710 MMM: dateStrGetter('Month', true), |
|
25711 MM: dateGetter('Month', 2, 1), |
|
25712 M: dateGetter('Month', 1, 1), |
|
25713 dd: dateGetter('Date', 2), |
|
25714 d: dateGetter('Date', 1), |
|
25715 HH: dateGetter('Hours', 2), |
|
25716 H: dateGetter('Hours', 1), |
|
25717 hh: dateGetter('Hours', 2, -12), |
|
25718 h: dateGetter('Hours', 1, -12), |
|
25719 mm: dateGetter('Minutes', 2), |
|
25720 m: dateGetter('Minutes', 1), |
|
25721 ss: dateGetter('Seconds', 2), |
|
25722 s: dateGetter('Seconds', 1), |
|
25723 // while ISO 8601 requires fractions to be prefixed with `.` or `,` |
|
25724 // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions |
|
25725 sss: dateGetter('Milliseconds', 3), |
|
25726 EEEE: dateStrGetter('Day'), |
|
25727 EEE: dateStrGetter('Day', true), |
|
25728 a: ampmGetter, |
|
25729 Z: timeZoneGetter, |
|
25730 ww: weekGetter(2), |
|
25731 w: weekGetter(1) |
|
25732 }; |
|
25733 |
|
25734 var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEw']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|w+))(.*)/, |
|
25735 NUMBER_STRING = /^\-?\d+$/; |
|
25736 |
|
25737 /** |
|
25738 * @ngdoc filter |
|
25739 * @name date |
|
25740 * @kind function |
|
25741 * |
|
25742 * @description |
|
25743 * Formats `date` to a string based on the requested `format`. |
|
25744 * |
|
25745 * `format` string can be composed of the following elements: |
|
25746 * |
|
25747 * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) |
|
25748 * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) |
|
25749 * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) |
|
25750 * * `'MMMM'`: Month in year (January-December) |
|
25751 * * `'MMM'`: Month in year (Jan-Dec) |
|
25752 * * `'MM'`: Month in year, padded (01-12) |
|
25753 * * `'M'`: Month in year (1-12) |
|
25754 * * `'dd'`: Day in month, padded (01-31) |
|
25755 * * `'d'`: Day in month (1-31) |
|
25756 * * `'EEEE'`: Day in Week,(Sunday-Saturday) |
|
25757 * * `'EEE'`: Day in Week, (Sun-Sat) |
|
25758 * * `'HH'`: Hour in day, padded (00-23) |
|
25759 * * `'H'`: Hour in day (0-23) |
|
25760 * * `'hh'`: Hour in AM/PM, padded (01-12) |
|
25761 * * `'h'`: Hour in AM/PM, (1-12) |
|
25762 * * `'mm'`: Minute in hour, padded (00-59) |
|
25763 * * `'m'`: Minute in hour (0-59) |
|
25764 * * `'ss'`: Second in minute, padded (00-59) |
|
25765 * * `'s'`: Second in minute (0-59) |
|
25766 * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) |
|
25767 * * `'a'`: AM/PM marker |
|
25768 * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) |
|
25769 * * `'ww'`: ISO-8601 week of year (00-53) |
|
25770 * * `'w'`: ISO-8601 week of year (0-53) |
|
25771 * |
|
25772 * `format` string can also be one of the following predefined |
|
25773 * {@link guide/i18n localizable formats}: |
|
25774 * |
|
25775 * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale |
|
25776 * (e.g. Sep 3, 2010 12:05:08 PM) |
|
25777 * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM) |
|
25778 * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale |
|
25779 * (e.g. Friday, September 3, 2010) |
|
25780 * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) |
|
25781 * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) |
|
25782 * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) |
|
25783 * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM) |
|
25784 * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM) |
|
25785 * |
|
25786 * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g. |
|
25787 * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence |
|
25788 * (e.g. `"h 'o''clock'"`). |
|
25789 * |
|
25790 * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or |
|
25791 * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its |
|
25792 * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is |
|
25793 * specified in the string input, the time is considered to be in the local timezone. |
|
25794 * @param {string=} format Formatting rules (see Description). If not specified, |
|
25795 * `mediumDate` is used. |
|
25796 * @param {string=} timezone Timezone to be used for formatting. Right now, only `'UTC'` is supported. |
|
25797 * If not specified, the timezone of the browser will be used. |
|
25798 * @returns {string} Formatted string or the input if input is not recognized as date/millis. |
|
25799 * |
|
25800 * @example |
|
25801 <example> |
|
25802 <file name="index.html"> |
|
25803 <span ng-non-bindable>{{1288323623006 | date:'medium'}}</span>: |
|
25804 <span>{{1288323623006 | date:'medium'}}</span><br> |
|
25805 <span ng-non-bindable>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span>: |
|
25806 <span>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span><br> |
|
25807 <span ng-non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>: |
|
25808 <span>{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}</span><br> |
|
25809 <span ng-non-bindable>{{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}</span>: |
|
25810 <span>{{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}</span><br> |
|
25811 </file> |
|
25812 <file name="protractor.js" type="protractor"> |
|
25813 it('should format date', function() { |
|
25814 expect(element(by.binding("1288323623006 | date:'medium'")).getText()). |
|
25815 toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); |
|
25816 expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). |
|
25817 toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); |
|
25818 expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). |
|
25819 toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); |
|
25820 expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). |
|
25821 toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/); |
|
25822 }); |
|
25823 </file> |
|
25824 </example> |
|
25825 */ |
|
25826 dateFilter.$inject = ['$locale']; |
|
25827 function dateFilter($locale) { |
|
25828 |
|
25829 |
|
25830 var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/; |
|
25831 // 1 2 3 4 5 6 7 8 9 10 11 |
|
25832 function jsonStringToDate(string) { |
|
25833 var match; |
|
25834 if (match = string.match(R_ISO8601_STR)) { |
|
25835 var date = new Date(0), |
|
25836 tzHour = 0, |
|
25837 tzMin = 0, |
|
25838 dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear, |
|
25839 timeSetter = match[8] ? date.setUTCHours : date.setHours; |
|
25840 |
|
25841 if (match[9]) { |
|
25842 tzHour = int(match[9] + match[10]); |
|
25843 tzMin = int(match[9] + match[11]); |
|
25844 } |
|
25845 dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3])); |
|
25846 var h = int(match[4]||0) - tzHour; |
|
25847 var m = int(match[5]||0) - tzMin; |
|
25848 var s = int(match[6]||0); |
|
25849 var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000); |
|
25850 timeSetter.call(date, h, m, s, ms); |
|
25851 return date; |
|
25852 } |
|
25853 return string; |
|
25854 } |
|
25855 |
|
25856 |
|
25857 return function(date, format, timezone) { |
|
25858 var text = '', |
|
25859 parts = [], |
|
25860 fn, match; |
|
25861 |
|
25862 format = format || 'mediumDate'; |
|
25863 format = $locale.DATETIME_FORMATS[format] || format; |
|
25864 if (isString(date)) { |
|
25865 date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date); |
|
25866 } |
|
25867 |
|
25868 if (isNumber(date)) { |
|
25869 date = new Date(date); |
|
25870 } |
|
25871 |
|
25872 if (!isDate(date)) { |
|
25873 return date; |
|
25874 } |
|
25875 |
|
25876 while(format) { |
|
25877 match = DATE_FORMATS_SPLIT.exec(format); |
|
25878 if (match) { |
|
25879 parts = concat(parts, match, 1); |
|
25880 format = parts.pop(); |
|
25881 } else { |
|
25882 parts.push(format); |
|
25883 format = null; |
|
25884 } |
|
25885 } |
|
25886 |
|
25887 if (timezone && timezone === 'UTC') { |
|
25888 date = new Date(date.getTime()); |
|
25889 date.setMinutes(date.getMinutes() + date.getTimezoneOffset()); |
|
25890 } |
|
25891 forEach(parts, function(value){ |
|
25892 fn = DATE_FORMATS[value]; |
|
25893 text += fn ? fn(date, $locale.DATETIME_FORMATS) |
|
25894 : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); |
|
25895 }); |
|
25896 |
|
25897 return text; |
|
25898 }; |
|
25899 } |
|
25900 |
|
25901 |
|
25902 /** |
|
25903 * @ngdoc filter |
|
25904 * @name json |
|
25905 * @kind function |
|
25906 * |
|
25907 * @description |
|
25908 * Allows you to convert a JavaScript object into JSON string. |
|
25909 * |
|
25910 * This filter is mostly useful for debugging. When using the double curly {{value}} notation |
|
25911 * the binding is automatically converted to JSON. |
|
25912 * |
|
25913 * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. |
|
25914 * @returns {string} JSON string. |
|
25915 * |
|
25916 * |
|
25917 * @example |
|
25918 <example> |
|
25919 <file name="index.html"> |
|
25920 <pre>{{ {'name':'value'} | json }}</pre> |
|
25921 </file> |
|
25922 <file name="protractor.js" type="protractor"> |
|
25923 it('should jsonify filtered objects', function() { |
|
25924 expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/); |
|
25925 }); |
|
25926 </file> |
|
25927 </example> |
|
25928 * |
|
25929 */ |
|
25930 function jsonFilter() { |
|
25931 return function(object) { |
|
25932 return toJson(object, true); |
|
25933 }; |
|
25934 } |
|
25935 |
|
25936 |
|
25937 /** |
|
25938 * @ngdoc filter |
|
25939 * @name lowercase |
|
25940 * @kind function |
|
25941 * @description |
|
25942 * Converts string to lowercase. |
|
25943 * @see angular.lowercase |
|
25944 */ |
|
25945 var lowercaseFilter = valueFn(lowercase); |
|
25946 |
|
25947 |
|
25948 /** |
|
25949 * @ngdoc filter |
|
25950 * @name uppercase |
|
25951 * @kind function |
|
25952 * @description |
|
25953 * Converts string to uppercase. |
|
25954 * @see angular.uppercase |
|
25955 */ |
|
25956 var uppercaseFilter = valueFn(uppercase); |
|
25957 |
|
25958 /** |
|
25959 * @ngdoc filter |
|
25960 * @name limitTo |
|
25961 * @kind function |
|
25962 * |
|
25963 * @description |
|
25964 * Creates a new array or string containing only a specified number of elements. The elements |
|
25965 * are taken from either the beginning or the end of the source array, string or number, as specified by |
|
25966 * the value and sign (positive or negative) of `limit`. If a number is used as input, it is |
|
25967 * converted to a string. |
|
25968 * |
|
25969 * @param {Array|string|number} input Source array, string or number to be limited. |
|
25970 * @param {string|number} limit The length of the returned array or string. If the `limit` number |
|
25971 * is positive, `limit` number of items from the beginning of the source array/string are copied. |
|
25972 * If the number is negative, `limit` number of items from the end of the source array/string |
|
25973 * are copied. The `limit` will be trimmed if it exceeds `array.length` |
|
25974 * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array |
|
25975 * had less than `limit` elements. |
|
25976 * |
|
25977 * @example |
|
25978 <example module="limitToExample"> |
|
25979 <file name="index.html"> |
|
25980 <script> |
|
25981 angular.module('limitToExample', []) |
|
25982 .controller('ExampleController', ['$scope', function($scope) { |
|
25983 $scope.numbers = [1,2,3,4,5,6,7,8,9]; |
|
25984 $scope.letters = "abcdefghi"; |
|
25985 $scope.longNumber = 2345432342; |
|
25986 $scope.numLimit = 3; |
|
25987 $scope.letterLimit = 3; |
|
25988 $scope.longNumberLimit = 3; |
|
25989 }]); |
|
25990 </script> |
|
25991 <div ng-controller="ExampleController"> |
|
25992 Limit {{numbers}} to: <input type="number" step="1" ng-model="numLimit"> |
|
25993 <p>Output numbers: {{ numbers | limitTo:numLimit }}</p> |
|
25994 Limit {{letters}} to: <input type="number" step="1" ng-model="letterLimit"> |
|
25995 <p>Output letters: {{ letters | limitTo:letterLimit }}</p> |
|
25996 Limit {{longNumber}} to: <input type="integer" ng-model="longNumberLimit"> |
|
25997 <p>Output long number: {{ longNumber | limitTo:longNumberLimit }}</p> |
|
25998 </div> |
|
25999 </file> |
|
26000 <file name="protractor.js" type="protractor"> |
|
26001 var numLimitInput = element(by.model('numLimit')); |
|
26002 var letterLimitInput = element(by.model('letterLimit')); |
|
26003 var longNumberLimitInput = element(by.model('longNumberLimit')); |
|
26004 var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); |
|
26005 var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); |
|
26006 var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit')); |
|
26007 |
|
26008 it('should limit the number array to first three items', function() { |
|
26009 expect(numLimitInput.getAttribute('value')).toBe('3'); |
|
26010 expect(letterLimitInput.getAttribute('value')).toBe('3'); |
|
26011 expect(longNumberLimitInput.getAttribute('value')).toBe('3'); |
|
26012 expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); |
|
26013 expect(limitedLetters.getText()).toEqual('Output letters: abc'); |
|
26014 expect(limitedLongNumber.getText()).toEqual('Output long number: 234'); |
|
26015 }); |
|
26016 |
|
26017 // There is a bug in safari and protractor that doesn't like the minus key |
|
26018 // it('should update the output when -3 is entered', function() { |
|
26019 // numLimitInput.clear(); |
|
26020 // numLimitInput.sendKeys('-3'); |
|
26021 // letterLimitInput.clear(); |
|
26022 // letterLimitInput.sendKeys('-3'); |
|
26023 // longNumberLimitInput.clear(); |
|
26024 // longNumberLimitInput.sendKeys('-3'); |
|
26025 // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); |
|
26026 // expect(limitedLetters.getText()).toEqual('Output letters: ghi'); |
|
26027 // expect(limitedLongNumber.getText()).toEqual('Output long number: 342'); |
|
26028 // }); |
|
26029 |
|
26030 it('should not exceed the maximum size of input array', function() { |
|
26031 numLimitInput.clear(); |
|
26032 numLimitInput.sendKeys('100'); |
|
26033 letterLimitInput.clear(); |
|
26034 letterLimitInput.sendKeys('100'); |
|
26035 longNumberLimitInput.clear(); |
|
26036 longNumberLimitInput.sendKeys('100'); |
|
26037 expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); |
|
26038 expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); |
|
26039 expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342'); |
|
26040 }); |
|
26041 </file> |
|
26042 </example> |
|
26043 */ |
|
26044 function limitToFilter(){ |
|
26045 return function(input, limit) { |
|
26046 if (isNumber(input)) input = input.toString(); |
|
26047 if (!isArray(input) && !isString(input)) return input; |
|
26048 |
|
26049 if (Math.abs(Number(limit)) === Infinity) { |
|
26050 limit = Number(limit); |
|
26051 } else { |
|
26052 limit = int(limit); |
|
26053 } |
|
26054 |
|
26055 if (isString(input)) { |
|
26056 //NaN check on limit |
|
26057 if (limit) { |
|
26058 return limit >= 0 ? input.slice(0, limit) : input.slice(limit, input.length); |
|
26059 } else { |
|
26060 return ""; |
|
26061 } |
|
26062 } |
|
26063 |
|
26064 var out = [], |
|
26065 i, n; |
|
26066 |
|
26067 // if abs(limit) exceeds maximum length, trim it |
|
26068 if (limit > input.length) |
|
26069 limit = input.length; |
|
26070 else if (limit < -input.length) |
|
26071 limit = -input.length; |
|
26072 |
|
26073 if (limit > 0) { |
|
26074 i = 0; |
|
26075 n = limit; |
|
26076 } else { |
|
26077 i = input.length + limit; |
|
26078 n = input.length; |
|
26079 } |
|
26080 |
|
26081 for (; i<n; i++) { |
|
26082 out.push(input[i]); |
|
26083 } |
|
26084 |
|
26085 return out; |
|
26086 }; |
|
26087 } |
|
26088 |
|
26089 /** |
|
26090 * @ngdoc filter |
|
26091 * @name orderBy |
|
26092 * @kind function |
|
26093 * |
|
26094 * @description |
|
26095 * Orders a specified `array` by the `expression` predicate. It is ordered alphabetically |
|
26096 * for strings and numerically for numbers. Note: if you notice numbers are not being sorted |
|
26097 * correctly, make sure they are actually being saved as numbers and not strings. |
|
26098 * |
|
26099 * @param {Array} array The array to sort. |
|
26100 * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be |
|
26101 * used by the comparator to determine the order of elements. |
|
26102 * |
|
26103 * Can be one of: |
|
26104 * |
|
26105 * - `function`: Getter function. The result of this function will be sorted using the |
|
26106 * `<`, `=`, `>` operator. |
|
26107 * - `string`: An Angular expression. The result of this expression is used to compare elements |
|
26108 * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by |
|
26109 * 3 first characters of a property called `name`). The result of a constant expression |
|
26110 * is interpreted as a property name to be used in comparisons (for example `"special name"` |
|
26111 * to sort object by the value of their `special name` property). An expression can be |
|
26112 * optionally prefixed with `+` or `-` to control ascending or descending sort order |
|
26113 * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array |
|
26114 * element itself is used to compare where sorting. |
|
26115 * - `Array`: An array of function or string predicates. The first predicate in the array |
|
26116 * is used for sorting, but when two items are equivalent, the next predicate is used. |
|
26117 * |
|
26118 * If the predicate is missing or empty then it defaults to `'+'`. |
|
26119 * |
|
26120 * @param {boolean=} reverse Reverse the order of the array. |
|
26121 * @returns {Array} Sorted copy of the source array. |
|
26122 * |
|
26123 * @example |
|
26124 <example module="orderByExample"> |
|
26125 <file name="index.html"> |
|
26126 <script> |
|
26127 angular.module('orderByExample', []) |
|
26128 .controller('ExampleController', ['$scope', function($scope) { |
|
26129 $scope.friends = |
|
26130 [{name:'John', phone:'555-1212', age:10}, |
|
26131 {name:'Mary', phone:'555-9876', age:19}, |
|
26132 {name:'Mike', phone:'555-4321', age:21}, |
|
26133 {name:'Adam', phone:'555-5678', age:35}, |
|
26134 {name:'Julie', phone:'555-8765', age:29}]; |
|
26135 $scope.predicate = '-age'; |
|
26136 }]); |
|
26137 </script> |
|
26138 <div ng-controller="ExampleController"> |
|
26139 <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> |
|
26140 <hr/> |
|
26141 [ <a href="" ng-click="predicate=''">unsorted</a> ] |
|
26142 <table class="friend"> |
|
26143 <tr> |
|
26144 <th><a href="" ng-click="predicate = 'name'; reverse=false">Name</a> |
|
26145 (<a href="" ng-click="predicate = '-name'; reverse=false">^</a>)</th> |
|
26146 <th><a href="" ng-click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th> |
|
26147 <th><a href="" ng-click="predicate = 'age'; reverse=!reverse">Age</a></th> |
|
26148 </tr> |
|
26149 <tr ng-repeat="friend in friends | orderBy:predicate:reverse"> |
|
26150 <td>{{friend.name}}</td> |
|
26151 <td>{{friend.phone}}</td> |
|
26152 <td>{{friend.age}}</td> |
|
26153 </tr> |
|
26154 </table> |
|
26155 </div> |
|
26156 </file> |
|
26157 </example> |
|
26158 * |
|
26159 * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the |
|
26160 * filter routine with `$filter('orderBy')`, and calling the returned filter routine with the |
|
26161 * desired parameters. |
|
26162 * |
|
26163 * Example: |
|
26164 * |
|
26165 * @example |
|
26166 <example module="orderByExample"> |
|
26167 <file name="index.html"> |
|
26168 <div ng-controller="ExampleController"> |
|
26169 <table class="friend"> |
|
26170 <tr> |
|
26171 <th><a href="" ng-click="reverse=false;order('name', false)">Name</a> |
|
26172 (<a href="" ng-click="order('-name',false)">^</a>)</th> |
|
26173 <th><a href="" ng-click="reverse=!reverse;order('phone', reverse)">Phone Number</a></th> |
|
26174 <th><a href="" ng-click="reverse=!reverse;order('age',reverse)">Age</a></th> |
|
26175 </tr> |
|
26176 <tr ng-repeat="friend in friends"> |
|
26177 <td>{{friend.name}}</td> |
|
26178 <td>{{friend.phone}}</td> |
|
26179 <td>{{friend.age}}</td> |
|
26180 </tr> |
|
26181 </table> |
|
26182 </div> |
|
26183 </file> |
|
26184 |
|
26185 <file name="script.js"> |
|
26186 angular.module('orderByExample', []) |
|
26187 .controller('ExampleController', ['$scope', '$filter', function($scope, $filter) { |
|
26188 var orderBy = $filter('orderBy'); |
|
26189 $scope.friends = [ |
|
26190 { name: 'John', phone: '555-1212', age: 10 }, |
|
26191 { name: 'Mary', phone: '555-9876', age: 19 }, |
|
26192 { name: 'Mike', phone: '555-4321', age: 21 }, |
|
26193 { name: 'Adam', phone: '555-5678', age: 35 }, |
|
26194 { name: 'Julie', phone: '555-8765', age: 29 } |
|
26195 ]; |
|
26196 $scope.order = function(predicate, reverse) { |
|
26197 $scope.friends = orderBy($scope.friends, predicate, reverse); |
|
26198 }; |
|
26199 $scope.order('-age',false); |
|
26200 }]); |
|
26201 </file> |
|
26202 </example> |
|
26203 */ |
|
26204 orderByFilter.$inject = ['$parse']; |
|
26205 function orderByFilter($parse){ |
|
26206 return function(array, sortPredicate, reverseOrder) { |
|
26207 if (!(isArrayLike(array))) return array; |
|
26208 sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; |
|
26209 if (sortPredicate.length === 0) { sortPredicate = ['+']; } |
|
26210 sortPredicate = sortPredicate.map(function(predicate){ |
|
26211 var descending = false, get = predicate || identity; |
|
26212 if (isString(predicate)) { |
|
26213 if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { |
|
26214 descending = predicate.charAt(0) == '-'; |
|
26215 predicate = predicate.substring(1); |
|
26216 } |
|
26217 if ( predicate === '' ) { |
|
26218 // Effectively no predicate was passed so we compare identity |
|
26219 return reverseComparator(function(a,b) { |
|
26220 return compare(a, b); |
|
26221 }, descending); |
|
26222 } |
|
26223 get = $parse(predicate); |
|
26224 if (get.constant) { |
|
26225 var key = get(); |
|
26226 return reverseComparator(function(a,b) { |
|
26227 return compare(a[key], b[key]); |
|
26228 }, descending); |
|
26229 } |
|
26230 } |
|
26231 return reverseComparator(function(a,b){ |
|
26232 return compare(get(a),get(b)); |
|
26233 }, descending); |
|
26234 }); |
|
26235 var arrayCopy = []; |
|
26236 for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } |
|
26237 return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); |
|
26238 |
|
26239 function comparator(o1, o2){ |
|
26240 for ( var i = 0; i < sortPredicate.length; i++) { |
|
26241 var comp = sortPredicate[i](o1, o2); |
|
26242 if (comp !== 0) return comp; |
|
26243 } |
|
26244 return 0; |
|
26245 } |
|
26246 function reverseComparator(comp, descending) { |
|
26247 return descending |
|
26248 ? function(a,b){return comp(b,a);} |
|
26249 : comp; |
|
26250 } |
|
26251 function compare(v1, v2){ |
|
26252 var t1 = typeof v1; |
|
26253 var t2 = typeof v2; |
|
26254 if (t1 == t2) { |
|
26255 if (isDate(v1) && isDate(v2)) { |
|
26256 v1 = v1.valueOf(); |
|
26257 v2 = v2.valueOf(); |
|
26258 } |
|
26259 if (t1 == "string") { |
|
26260 v1 = v1.toLowerCase(); |
|
26261 v2 = v2.toLowerCase(); |
|
26262 } |
|
26263 if (v1 === v2) return 0; |
|
26264 return v1 < v2 ? -1 : 1; |
|
26265 } else { |
|
26266 return t1 < t2 ? -1 : 1; |
|
26267 } |
|
26268 } |
|
26269 }; |
|
26270 } |
|
26271 |
|
26272 function ngDirective(directive) { |
|
26273 if (isFunction(directive)) { |
|
26274 directive = { |
|
26275 link: directive |
|
26276 }; |
|
26277 } |
|
26278 directive.restrict = directive.restrict || 'AC'; |
|
26279 return valueFn(directive); |
|
26280 } |
|
26281 |
|
26282 /** |
|
26283 * @ngdoc directive |
|
26284 * @name a |
|
26285 * @restrict E |
|
26286 * |
|
26287 * @description |
|
26288 * Modifies the default behavior of the html A tag so that the default action is prevented when |
|
26289 * the href attribute is empty. |
|
26290 * |
|
26291 * This change permits the easy creation of action links with the `ngClick` directive |
|
26292 * without changing the location or causing page reloads, e.g.: |
|
26293 * `<a href="" ng-click="list.addItem()">Add Item</a>` |
|
26294 */ |
|
26295 var htmlAnchorDirective = valueFn({ |
|
26296 restrict: 'E', |
|
26297 compile: function(element, attr) { |
|
26298 if (!attr.href && !attr.xlinkHref && !attr.name) { |
|
26299 return function(scope, element) { |
|
26300 // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. |
|
26301 var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? |
|
26302 'xlink:href' : 'href'; |
|
26303 element.on('click', function(event){ |
|
26304 // if we have no href url, then don't navigate anywhere. |
|
26305 if (!element.attr(href)) { |
|
26306 event.preventDefault(); |
|
26307 } |
|
26308 }); |
|
26309 }; |
|
26310 } |
|
26311 } |
|
26312 }); |
|
26313 |
|
26314 /** |
|
26315 * @ngdoc directive |
|
26316 * @name ngHref |
|
26317 * @restrict A |
|
26318 * @priority 99 |
|
26319 * |
|
26320 * @description |
|
26321 * Using Angular markup like `{{hash}}` in an href attribute will |
|
26322 * make the link go to the wrong URL if the user clicks it before |
|
26323 * Angular has a chance to replace the `{{hash}}` markup with its |
|
26324 * value. Until Angular replaces the markup the link will be broken |
|
26325 * and will most likely return a 404 error. |
|
26326 * |
|
26327 * The `ngHref` directive solves this problem. |
|
26328 * |
|
26329 * The wrong way to write it: |
|
26330 * ```html |
|
26331 * <a href="http://www.gravatar.com/avatar/{{hash}}">link1</a> |
|
26332 * ``` |
|
26333 * |
|
26334 * The correct way to write it: |
|
26335 * ```html |
|
26336 * <a ng-href="http://www.gravatar.com/avatar/{{hash}}">link1</a> |
|
26337 * ``` |
|
26338 * |
|
26339 * @element A |
|
26340 * @param {template} ngHref any string which can contain `{{}}` markup. |
|
26341 * |
|
26342 * @example |
|
26343 * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes |
|
26344 * in links and their different behaviors: |
|
26345 <example> |
|
26346 <file name="index.html"> |
|
26347 <input ng-model="value" /><br /> |
|
26348 <a id="link-1" href ng-click="value = 1">link 1</a> (link, don't reload)<br /> |
|
26349 <a id="link-2" href="" ng-click="value = 2">link 2</a> (link, don't reload)<br /> |
|
26350 <a id="link-3" ng-href="/{{'123'}}">link 3</a> (link, reload!)<br /> |
|
26351 <a id="link-4" href="" name="xx" ng-click="value = 4">anchor</a> (link, don't reload)<br /> |
|
26352 <a id="link-5" name="xxx" ng-click="value = 5">anchor</a> (no link)<br /> |
|
26353 <a id="link-6" ng-href="{{value}}">link</a> (link, change location) |
|
26354 </file> |
|
26355 <file name="protractor.js" type="protractor"> |
|
26356 it('should execute ng-click but not reload when href without value', function() { |
|
26357 element(by.id('link-1')).click(); |
|
26358 expect(element(by.model('value')).getAttribute('value')).toEqual('1'); |
|
26359 expect(element(by.id('link-1')).getAttribute('href')).toBe(''); |
|
26360 }); |
|
26361 |
|
26362 it('should execute ng-click but not reload when href empty string', function() { |
|
26363 element(by.id('link-2')).click(); |
|
26364 expect(element(by.model('value')).getAttribute('value')).toEqual('2'); |
|
26365 expect(element(by.id('link-2')).getAttribute('href')).toBe(''); |
|
26366 }); |
|
26367 |
|
26368 it('should execute ng-click and change url when ng-href specified', function() { |
|
26369 expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/); |
|
26370 |
|
26371 element(by.id('link-3')).click(); |
|
26372 |
|
26373 // At this point, we navigate away from an Angular page, so we need |
|
26374 // to use browser.driver to get the base webdriver. |
|
26375 |
|
26376 browser.wait(function() { |
|
26377 return browser.driver.getCurrentUrl().then(function(url) { |
|
26378 return url.match(/\/123$/); |
|
26379 }); |
|
26380 }, 5000, 'page should navigate to /123'); |
|
26381 }); |
|
26382 |
|
26383 xit('should execute ng-click but not reload when href empty string and name specified', function() { |
|
26384 element(by.id('link-4')).click(); |
|
26385 expect(element(by.model('value')).getAttribute('value')).toEqual('4'); |
|
26386 expect(element(by.id('link-4')).getAttribute('href')).toBe(''); |
|
26387 }); |
|
26388 |
|
26389 it('should execute ng-click but not reload when no href but name specified', function() { |
|
26390 element(by.id('link-5')).click(); |
|
26391 expect(element(by.model('value')).getAttribute('value')).toEqual('5'); |
|
26392 expect(element(by.id('link-5')).getAttribute('href')).toBe(null); |
|
26393 }); |
|
26394 |
|
26395 it('should only change url when only ng-href', function() { |
|
26396 element(by.model('value')).clear(); |
|
26397 element(by.model('value')).sendKeys('6'); |
|
26398 expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/); |
|
26399 |
|
26400 element(by.id('link-6')).click(); |
|
26401 |
|
26402 // At this point, we navigate away from an Angular page, so we need |
|
26403 // to use browser.driver to get the base webdriver. |
|
26404 browser.wait(function() { |
|
26405 return browser.driver.getCurrentUrl().then(function(url) { |
|
26406 return url.match(/\/6$/); |
|
26407 }); |
|
26408 }, 5000, 'page should navigate to /6'); |
|
26409 }); |
|
26410 </file> |
|
26411 </example> |
|
26412 */ |
|
26413 |
|
26414 /** |
|
26415 * @ngdoc directive |
|
26416 * @name ngSrc |
|
26417 * @restrict A |
|
26418 * @priority 99 |
|
26419 * |
|
26420 * @description |
|
26421 * Using Angular markup like `{{hash}}` in a `src` attribute doesn't |
|
26422 * work right: The browser will fetch from the URL with the literal |
|
26423 * text `{{hash}}` until Angular replaces the expression inside |
|
26424 * `{{hash}}`. The `ngSrc` directive solves this problem. |
|
26425 * |
|
26426 * The buggy way to write it: |
|
26427 * ```html |
|
26428 * <img src="http://www.gravatar.com/avatar/{{hash}}"/> |
|
26429 * ``` |
|
26430 * |
|
26431 * The correct way to write it: |
|
26432 * ```html |
|
26433 * <img ng-src="http://www.gravatar.com/avatar/{{hash}}"/> |
|
26434 * ``` |
|
26435 * |
|
26436 * @element IMG |
|
26437 * @param {template} ngSrc any string which can contain `{{}}` markup. |
|
26438 */ |
|
26439 |
|
26440 /** |
|
26441 * @ngdoc directive |
|
26442 * @name ngSrcset |
|
26443 * @restrict A |
|
26444 * @priority 99 |
|
26445 * |
|
26446 * @description |
|
26447 * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't |
|
26448 * work right: The browser will fetch from the URL with the literal |
|
26449 * text `{{hash}}` until Angular replaces the expression inside |
|
26450 * `{{hash}}`. The `ngSrcset` directive solves this problem. |
|
26451 * |
|
26452 * The buggy way to write it: |
|
26453 * ```html |
|
26454 * <img srcset="http://www.gravatar.com/avatar/{{hash}} 2x"/> |
|
26455 * ``` |
|
26456 * |
|
26457 * The correct way to write it: |
|
26458 * ```html |
|
26459 * <img ng-srcset="http://www.gravatar.com/avatar/{{hash}} 2x"/> |
|
26460 * ``` |
|
26461 * |
|
26462 * @element IMG |
|
26463 * @param {template} ngSrcset any string which can contain `{{}}` markup. |
|
26464 */ |
|
26465 |
|
26466 /** |
|
26467 * @ngdoc directive |
|
26468 * @name ngDisabled |
|
26469 * @restrict A |
|
26470 * @priority 100 |
|
26471 * |
|
26472 * @description |
|
26473 * |
|
26474 * We shouldn't do this, because it will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: |
|
26475 * ```html |
|
26476 * <div ng-init="scope = { isDisabled: false }"> |
|
26477 * <button disabled="{{scope.isDisabled}}">Disabled</button> |
|
26478 * </div> |
|
26479 * ``` |
|
26480 * |
|
26481 * The HTML specification does not require browsers to preserve the values of boolean attributes |
|
26482 * such as disabled. (Their presence means true and their absence means false.) |
|
26483 * If we put an Angular interpolation expression into such an attribute then the |
|
26484 * binding information would be lost when the browser removes the attribute. |
|
26485 * The `ngDisabled` directive solves this problem for the `disabled` attribute. |
|
26486 * This complementary directive is not removed by the browser and so provides |
|
26487 * a permanent reliable place to store the binding information. |
|
26488 * |
|
26489 * @example |
|
26490 <example> |
|
26491 <file name="index.html"> |
|
26492 Click me to toggle: <input type="checkbox" ng-model="checked"><br/> |
|
26493 <button ng-model="button" ng-disabled="checked">Button</button> |
|
26494 </file> |
|
26495 <file name="protractor.js" type="protractor"> |
|
26496 it('should toggle button', function() { |
|
26497 expect(element(by.css('button')).getAttribute('disabled')).toBeFalsy(); |
|
26498 element(by.model('checked')).click(); |
|
26499 expect(element(by.css('button')).getAttribute('disabled')).toBeTruthy(); |
|
26500 }); |
|
26501 </file> |
|
26502 </example> |
|
26503 * |
|
26504 * @element INPUT |
|
26505 * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, |
|
26506 * then special attribute "disabled" will be set on the element |
|
26507 */ |
|
26508 |
|
26509 |
|
26510 /** |
|
26511 * @ngdoc directive |
|
26512 * @name ngChecked |
|
26513 * @restrict A |
|
26514 * @priority 100 |
|
26515 * |
|
26516 * @description |
|
26517 * The HTML specification does not require browsers to preserve the values of boolean attributes |
|
26518 * such as checked. (Their presence means true and their absence means false.) |
|
26519 * If we put an Angular interpolation expression into such an attribute then the |
|
26520 * binding information would be lost when the browser removes the attribute. |
|
26521 * The `ngChecked` directive solves this problem for the `checked` attribute. |
|
26522 * This complementary directive is not removed by the browser and so provides |
|
26523 * a permanent reliable place to store the binding information. |
|
26524 * @example |
|
26525 <example> |
|
26526 <file name="index.html"> |
|
26527 Check me to check both: <input type="checkbox" ng-model="master"><br/> |
|
26528 <input id="checkSlave" type="checkbox" ng-checked="master"> |
|
26529 </file> |
|
26530 <file name="protractor.js" type="protractor"> |
|
26531 it('should check both checkBoxes', function() { |
|
26532 expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy(); |
|
26533 element(by.model('master')).click(); |
|
26534 expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy(); |
|
26535 }); |
|
26536 </file> |
|
26537 </example> |
|
26538 * |
|
26539 * @element INPUT |
|
26540 * @param {expression} ngChecked If the {@link guide/expression expression} is truthy, |
|
26541 * then special attribute "checked" will be set on the element |
|
26542 */ |
|
26543 |
|
26544 |
|
26545 /** |
|
26546 * @ngdoc directive |
|
26547 * @name ngReadonly |
|
26548 * @restrict A |
|
26549 * @priority 100 |
|
26550 * |
|
26551 * @description |
|
26552 * The HTML specification does not require browsers to preserve the values of boolean attributes |
|
26553 * such as readonly. (Their presence means true and their absence means false.) |
|
26554 * If we put an Angular interpolation expression into such an attribute then the |
|
26555 * binding information would be lost when the browser removes the attribute. |
|
26556 * The `ngReadonly` directive solves this problem for the `readonly` attribute. |
|
26557 * This complementary directive is not removed by the browser and so provides |
|
26558 * a permanent reliable place to store the binding information. |
|
26559 * @example |
|
26560 <example> |
|
26561 <file name="index.html"> |
|
26562 Check me to make text readonly: <input type="checkbox" ng-model="checked"><br/> |
|
26563 <input type="text" ng-readonly="checked" value="I'm Angular"/> |
|
26564 </file> |
|
26565 <file name="protractor.js" type="protractor"> |
|
26566 it('should toggle readonly attr', function() { |
|
26567 expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeFalsy(); |
|
26568 element(by.model('checked')).click(); |
|
26569 expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeTruthy(); |
|
26570 }); |
|
26571 </file> |
|
26572 </example> |
|
26573 * |
|
26574 * @element INPUT |
|
26575 * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy, |
|
26576 * then special attribute "readonly" will be set on the element |
|
26577 */ |
|
26578 |
|
26579 |
|
26580 /** |
|
26581 * @ngdoc directive |
|
26582 * @name ngSelected |
|
26583 * @restrict A |
|
26584 * @priority 100 |
|
26585 * |
|
26586 * @description |
|
26587 * The HTML specification does not require browsers to preserve the values of boolean attributes |
|
26588 * such as selected. (Their presence means true and their absence means false.) |
|
26589 * If we put an Angular interpolation expression into such an attribute then the |
|
26590 * binding information would be lost when the browser removes the attribute. |
|
26591 * The `ngSelected` directive solves this problem for the `selected` attribute. |
|
26592 * This complementary directive is not removed by the browser and so provides |
|
26593 * a permanent reliable place to store the binding information. |
|
26594 * |
|
26595 * @example |
|
26596 <example> |
|
26597 <file name="index.html"> |
|
26598 Check me to select: <input type="checkbox" ng-model="selected"><br/> |
|
26599 <select> |
|
26600 <option>Hello!</option> |
|
26601 <option id="greet" ng-selected="selected">Greetings!</option> |
|
26602 </select> |
|
26603 </file> |
|
26604 <file name="protractor.js" type="protractor"> |
|
26605 it('should select Greetings!', function() { |
|
26606 expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy(); |
|
26607 element(by.model('selected')).click(); |
|
26608 expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy(); |
|
26609 }); |
|
26610 </file> |
|
26611 </example> |
|
26612 * |
|
26613 * @element OPTION |
|
26614 * @param {expression} ngSelected If the {@link guide/expression expression} is truthy, |
|
26615 * then special attribute "selected" will be set on the element |
|
26616 */ |
|
26617 |
|
26618 /** |
|
26619 * @ngdoc directive |
|
26620 * @name ngOpen |
|
26621 * @restrict A |
|
26622 * @priority 100 |
|
26623 * |
|
26624 * @description |
|
26625 * The HTML specification does not require browsers to preserve the values of boolean attributes |
|
26626 * such as open. (Their presence means true and their absence means false.) |
|
26627 * If we put an Angular interpolation expression into such an attribute then the |
|
26628 * binding information would be lost when the browser removes the attribute. |
|
26629 * The `ngOpen` directive solves this problem for the `open` attribute. |
|
26630 * This complementary directive is not removed by the browser and so provides |
|
26631 * a permanent reliable place to store the binding information. |
|
26632 * @example |
|
26633 <example> |
|
26634 <file name="index.html"> |
|
26635 Check me check multiple: <input type="checkbox" ng-model="open"><br/> |
|
26636 <details id="details" ng-open="open"> |
|
26637 <summary>Show/Hide me</summary> |
|
26638 </details> |
|
26639 </file> |
|
26640 <file name="protractor.js" type="protractor"> |
|
26641 it('should toggle open', function() { |
|
26642 expect(element(by.id('details')).getAttribute('open')).toBeFalsy(); |
|
26643 element(by.model('open')).click(); |
|
26644 expect(element(by.id('details')).getAttribute('open')).toBeTruthy(); |
|
26645 }); |
|
26646 </file> |
|
26647 </example> |
|
26648 * |
|
26649 * @element DETAILS |
|
26650 * @param {expression} ngOpen If the {@link guide/expression expression} is truthy, |
|
26651 * then special attribute "open" will be set on the element |
|
26652 */ |
|
26653 |
|
26654 var ngAttributeAliasDirectives = {}; |
|
26655 |
|
26656 |
|
26657 // boolean attrs are evaluated |
|
26658 forEach(BOOLEAN_ATTR, function(propName, attrName) { |
|
26659 // binding to multiple is not supported |
|
26660 if (propName == "multiple") return; |
|
26661 |
|
26662 var normalized = directiveNormalize('ng-' + attrName); |
|
26663 ngAttributeAliasDirectives[normalized] = function() { |
|
26664 return { |
|
26665 restrict: 'A', |
|
26666 priority: 100, |
|
26667 link: function(scope, element, attr) { |
|
26668 scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { |
|
26669 attr.$set(attrName, !!value); |
|
26670 }); |
|
26671 } |
|
26672 }; |
|
26673 }; |
|
26674 }); |
|
26675 |
|
26676 // aliased input attrs are evaluated |
|
26677 forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { |
|
26678 ngAttributeAliasDirectives[ngAttr] = function() { |
|
26679 return { |
|
26680 priority: 100, |
|
26681 link: function(scope, element, attr) { |
|
26682 //special case ngPattern when a literal regular expression value |
|
26683 //is used as the expression (this way we don't have to watch anything). |
|
26684 if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") { |
|
26685 var match = attr.ngPattern.match(REGEX_STRING_REGEXP); |
|
26686 if (match) { |
|
26687 attr.$set("ngPattern", new RegExp(match[1], match[2])); |
|
26688 return; |
|
26689 } |
|
26690 } |
|
26691 |
|
26692 scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) { |
|
26693 attr.$set(ngAttr, value); |
|
26694 }); |
|
26695 } |
|
26696 }; |
|
26697 }; |
|
26698 }); |
|
26699 |
|
26700 // ng-src, ng-srcset, ng-href are interpolated |
|
26701 forEach(['src', 'srcset', 'href'], function(attrName) { |
|
26702 var normalized = directiveNormalize('ng-' + attrName); |
|
26703 ngAttributeAliasDirectives[normalized] = function() { |
|
26704 return { |
|
26705 priority: 99, // it needs to run after the attributes are interpolated |
|
26706 link: function(scope, element, attr) { |
|
26707 var propName = attrName, |
|
26708 name = attrName; |
|
26709 |
|
26710 if (attrName === 'href' && |
|
26711 toString.call(element.prop('href')) === '[object SVGAnimatedString]') { |
|
26712 name = 'xlinkHref'; |
|
26713 attr.$attr[name] = 'xlink:href'; |
|
26714 propName = null; |
|
26715 } |
|
26716 |
|
26717 attr.$observe(normalized, function(value) { |
|
26718 if (!value) { |
|
26719 if (attrName === 'href') { |
|
26720 attr.$set(name, null); |
|
26721 } |
|
26722 return; |
|
26723 } |
|
26724 |
|
26725 attr.$set(name, value); |
|
26726 |
|
26727 // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist |
|
26728 // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need |
|
26729 // to set the property as well to achieve the desired effect. |
|
26730 // we use attr[attrName] value since $set can sanitize the url. |
|
26731 if (msie && propName) element.prop(propName, attr[name]); |
|
26732 }); |
|
26733 } |
|
26734 }; |
|
26735 }; |
|
26736 }); |
|
26737 |
|
26738 /* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true |
|
26739 */ |
|
26740 var nullFormCtrl = { |
|
26741 $addControl: noop, |
|
26742 $$renameControl: nullFormRenameControl, |
|
26743 $removeControl: noop, |
|
26744 $setValidity: noop, |
|
26745 $setDirty: noop, |
|
26746 $setPristine: noop, |
|
26747 $setSubmitted: noop |
|
26748 }, |
|
26749 SUBMITTED_CLASS = 'ng-submitted'; |
|
26750 |
|
26751 function nullFormRenameControl(control, name) { |
|
26752 control.$name = name; |
|
26753 } |
|
26754 |
|
26755 /** |
|
26756 * @ngdoc type |
|
26757 * @name form.FormController |
|
26758 * |
|
26759 * @property {boolean} $pristine True if user has not interacted with the form yet. |
|
26760 * @property {boolean} $dirty True if user has already interacted with the form. |
|
26761 * @property {boolean} $valid True if all of the containing forms and controls are valid. |
|
26762 * @property {boolean} $invalid True if at least one containing control or form is invalid. |
|
26763 * @property {boolean} $submitted True if user has submitted the form even if its invalid. |
|
26764 * |
|
26765 * @property {Object} $error Is an object hash, containing references to controls or |
|
26766 * forms with failing validators, where: |
|
26767 * |
|
26768 * - keys are validation tokens (error names), |
|
26769 * - values are arrays of controls or forms that have a failing validator for given error name. |
|
26770 * |
|
26771 * Built-in validation tokens: |
|
26772 * |
|
26773 * - `email` |
|
26774 * - `max` |
|
26775 * - `maxlength` |
|
26776 * - `min` |
|
26777 * - `minlength` |
|
26778 * - `number` |
|
26779 * - `pattern` |
|
26780 * - `required` |
|
26781 * - `url` |
|
26782 * |
|
26783 * @description |
|
26784 * `FormController` keeps track of all its controls and nested forms as well as the state of them, |
|
26785 * such as being valid/invalid or dirty/pristine. |
|
26786 * |
|
26787 * Each {@link ng.directive:form form} directive creates an instance |
|
26788 * of `FormController`. |
|
26789 * |
|
26790 */ |
|
26791 //asks for $scope to fool the BC controller module |
|
26792 FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; |
|
26793 function FormController(element, attrs, $scope, $animate, $interpolate) { |
|
26794 var form = this, |
|
26795 controls = []; |
|
26796 |
|
26797 var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl; |
|
26798 |
|
26799 // init state |
|
26800 form.$error = {}; |
|
26801 form.$$success = {}; |
|
26802 form.$pending = undefined; |
|
26803 form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); |
|
26804 form.$dirty = false; |
|
26805 form.$pristine = true; |
|
26806 form.$valid = true; |
|
26807 form.$invalid = false; |
|
26808 form.$submitted = false; |
|
26809 |
|
26810 parentForm.$addControl(form); |
|
26811 |
|
26812 /** |
|
26813 * @ngdoc method |
|
26814 * @name form.FormController#$rollbackViewValue |
|
26815 * |
|
26816 * @description |
|
26817 * Rollback all form controls pending updates to the `$modelValue`. |
|
26818 * |
|
26819 * Updates may be pending by a debounced event or because the input is waiting for a some future |
|
26820 * event defined in `ng-model-options`. This method is typically needed by the reset button of |
|
26821 * a form that uses `ng-model-options` to pend updates. |
|
26822 */ |
|
26823 form.$rollbackViewValue = function() { |
|
26824 forEach(controls, function(control) { |
|
26825 control.$rollbackViewValue(); |
|
26826 }); |
|
26827 }; |
|
26828 |
|
26829 /** |
|
26830 * @ngdoc method |
|
26831 * @name form.FormController#$commitViewValue |
|
26832 * |
|
26833 * @description |
|
26834 * Commit all form controls pending updates to the `$modelValue`. |
|
26835 * |
|
26836 * Updates may be pending by a debounced event or because the input is waiting for a some future |
|
26837 * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` |
|
26838 * usually handles calling this in response to input events. |
|
26839 */ |
|
26840 form.$commitViewValue = function() { |
|
26841 forEach(controls, function(control) { |
|
26842 control.$commitViewValue(); |
|
26843 }); |
|
26844 }; |
|
26845 |
|
26846 /** |
|
26847 * @ngdoc method |
|
26848 * @name form.FormController#$addControl |
|
26849 * |
|
26850 * @description |
|
26851 * Register a control with the form. |
|
26852 * |
|
26853 * Input elements using ngModelController do this automatically when they are linked. |
|
26854 */ |
|
26855 form.$addControl = function(control) { |
|
26856 // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored |
|
26857 // and not added to the scope. Now we throw an error. |
|
26858 assertNotHasOwnProperty(control.$name, 'input'); |
|
26859 controls.push(control); |
|
26860 |
|
26861 if (control.$name) { |
|
26862 form[control.$name] = control; |
|
26863 } |
|
26864 }; |
|
26865 |
|
26866 // Private API: rename a form control |
|
26867 form.$$renameControl = function(control, newName) { |
|
26868 var oldName = control.$name; |
|
26869 |
|
26870 if (form[oldName] === control) { |
|
26871 delete form[oldName]; |
|
26872 } |
|
26873 form[newName] = control; |
|
26874 control.$name = newName; |
|
26875 }; |
|
26876 |
|
26877 /** |
|
26878 * @ngdoc method |
|
26879 * @name form.FormController#$removeControl |
|
26880 * |
|
26881 * @description |
|
26882 * Deregister a control from the form. |
|
26883 * |
|
26884 * Input elements using ngModelController do this automatically when they are destroyed. |
|
26885 */ |
|
26886 form.$removeControl = function(control) { |
|
26887 if (control.$name && form[control.$name] === control) { |
|
26888 delete form[control.$name]; |
|
26889 } |
|
26890 forEach(form.$pending, function(value, name) { |
|
26891 form.$setValidity(name, null, control); |
|
26892 }); |
|
26893 forEach(form.$error, function(value, name) { |
|
26894 form.$setValidity(name, null, control); |
|
26895 }); |
|
26896 |
|
26897 arrayRemove(controls, control); |
|
26898 }; |
|
26899 |
|
26900 |
|
26901 /** |
|
26902 * @ngdoc method |
|
26903 * @name form.FormController#$setValidity |
|
26904 * |
|
26905 * @description |
|
26906 * Sets the validity of a form control. |
|
26907 * |
|
26908 * This method will also propagate to parent forms. |
|
26909 */ |
|
26910 addSetValidityMethod({ |
|
26911 ctrl: this, |
|
26912 $element: element, |
|
26913 set: function(object, property, control) { |
|
26914 var list = object[property]; |
|
26915 if (!list) { |
|
26916 object[property] = [control]; |
|
26917 } else { |
|
26918 var index = list.indexOf(control); |
|
26919 if (index === -1) { |
|
26920 list.push(control); |
|
26921 } |
|
26922 } |
|
26923 }, |
|
26924 unset: function(object, property, control) { |
|
26925 var list = object[property]; |
|
26926 if (!list) { |
|
26927 return; |
|
26928 } |
|
26929 arrayRemove(list, control); |
|
26930 if (list.length === 0) { |
|
26931 delete object[property]; |
|
26932 } |
|
26933 }, |
|
26934 parentForm: parentForm, |
|
26935 $animate: $animate |
|
26936 }); |
|
26937 |
|
26938 /** |
|
26939 * @ngdoc method |
|
26940 * @name form.FormController#$setDirty |
|
26941 * |
|
26942 * @description |
|
26943 * Sets the form to a dirty state. |
|
26944 * |
|
26945 * This method can be called to add the 'ng-dirty' class and set the form to a dirty |
|
26946 * state (ng-dirty class). This method will also propagate to parent forms. |
|
26947 */ |
|
26948 form.$setDirty = function() { |
|
26949 $animate.removeClass(element, PRISTINE_CLASS); |
|
26950 $animate.addClass(element, DIRTY_CLASS); |
|
26951 form.$dirty = true; |
|
26952 form.$pristine = false; |
|
26953 parentForm.$setDirty(); |
|
26954 }; |
|
26955 |
|
26956 /** |
|
26957 * @ngdoc method |
|
26958 * @name form.FormController#$setPristine |
|
26959 * |
|
26960 * @description |
|
26961 * Sets the form to its pristine state. |
|
26962 * |
|
26963 * This method can be called to remove the 'ng-dirty' class and set the form to its pristine |
|
26964 * state (ng-pristine class). This method will also propagate to all the controls contained |
|
26965 * in this form. |
|
26966 * |
|
26967 * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after |
|
26968 * saving or resetting it. |
|
26969 */ |
|
26970 form.$setPristine = function () { |
|
26971 $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); |
|
26972 form.$dirty = false; |
|
26973 form.$pristine = true; |
|
26974 form.$submitted = false; |
|
26975 forEach(controls, function(control) { |
|
26976 control.$setPristine(); |
|
26977 }); |
|
26978 }; |
|
26979 |
|
26980 /** |
|
26981 * @ngdoc method |
|
26982 * @name form.FormController#$setUntouched |
|
26983 * |
|
26984 * @description |
|
26985 * Sets the form to its untouched state. |
|
26986 * |
|
26987 * This method can be called to remove the 'ng-touched' class and set the form controls to their |
|
26988 * untouched state (ng-untouched class). |
|
26989 * |
|
26990 * Setting a form controls back to their untouched state is often useful when setting the form |
|
26991 * back to its pristine state. |
|
26992 */ |
|
26993 form.$setUntouched = function () { |
|
26994 forEach(controls, function(control) { |
|
26995 control.$setUntouched(); |
|
26996 }); |
|
26997 }; |
|
26998 |
|
26999 /** |
|
27000 * @ngdoc method |
|
27001 * @name form.FormController#$setSubmitted |
|
27002 * |
|
27003 * @description |
|
27004 * Sets the form to its submitted state. |
|
27005 */ |
|
27006 form.$setSubmitted = function () { |
|
27007 $animate.addClass(element, SUBMITTED_CLASS); |
|
27008 form.$submitted = true; |
|
27009 parentForm.$setSubmitted(); |
|
27010 }; |
|
27011 } |
|
27012 |
|
27013 /** |
|
27014 * @ngdoc directive |
|
27015 * @name ngForm |
|
27016 * @restrict EAC |
|
27017 * |
|
27018 * @description |
|
27019 * Nestable alias of {@link ng.directive:form `form`} directive. HTML |
|
27020 * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a |
|
27021 * sub-group of controls needs to be determined. |
|
27022 * |
|
27023 * Note: the purpose of `ngForm` is to group controls, |
|
27024 * but not to be a replacement for the `<form>` tag with all of its capabilities |
|
27025 * (e.g. posting to the server, ...). |
|
27026 * |
|
27027 * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into |
|
27028 * related scope, under this name. |
|
27029 * |
|
27030 */ |
|
27031 |
|
27032 /** |
|
27033 * @ngdoc directive |
|
27034 * @name form |
|
27035 * @restrict E |
|
27036 * |
|
27037 * @description |
|
27038 * Directive that instantiates |
|
27039 * {@link form.FormController FormController}. |
|
27040 * |
|
27041 * If the `name` attribute is specified, the form controller is published onto the current scope under |
|
27042 * this name. |
|
27043 * |
|
27044 * # Alias: {@link ng.directive:ngForm `ngForm`} |
|
27045 * |
|
27046 * In Angular forms can be nested. This means that the outer form is valid when all of the child |
|
27047 * forms are valid as well. However, browsers do not allow nesting of `<form>` elements, so |
|
27048 * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to |
|
27049 * `<form>` but can be nested. This allows you to have nested forms, which is very useful when |
|
27050 * using Angular validation directives in forms that are dynamically generated using the |
|
27051 * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name` |
|
27052 * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an |
|
27053 * `ngForm` directive and nest these in an outer `form` element. |
|
27054 * |
|
27055 * |
|
27056 * # CSS classes |
|
27057 * - `ng-valid` is set if the form is valid. |
|
27058 * - `ng-invalid` is set if the form is invalid. |
|
27059 * - `ng-pristine` is set if the form is pristine. |
|
27060 * - `ng-dirty` is set if the form is dirty. |
|
27061 * - `ng-submitted` is set if the form was submitted. |
|
27062 * |
|
27063 * Keep in mind that ngAnimate can detect each of these classes when added and removed. |
|
27064 * |
|
27065 * |
|
27066 * # Submitting a form and preventing the default action |
|
27067 * |
|
27068 * Since the role of forms in client-side Angular applications is different than in classical |
|
27069 * roundtrip apps, it is desirable for the browser not to translate the form submission into a full |
|
27070 * page reload that sends the data to the server. Instead some javascript logic should be triggered |
|
27071 * to handle the form submission in an application-specific way. |
|
27072 * |
|
27073 * For this reason, Angular prevents the default action (form submission to the server) unless the |
|
27074 * `<form>` element has an `action` attribute specified. |
|
27075 * |
|
27076 * You can use one of the following two ways to specify what javascript method should be called when |
|
27077 * a form is submitted: |
|
27078 * |
|
27079 * - {@link ng.directive:ngSubmit ngSubmit} directive on the form element |
|
27080 * - {@link ng.directive:ngClick ngClick} directive on the first |
|
27081 * button or input field of type submit (input[type=submit]) |
|
27082 * |
|
27083 * To prevent double execution of the handler, use only one of the {@link ng.directive:ngSubmit ngSubmit} |
|
27084 * or {@link ng.directive:ngClick ngClick} directives. |
|
27085 * This is because of the following form submission rules in the HTML specification: |
|
27086 * |
|
27087 * - If a form has only one input field then hitting enter in this field triggers form submit |
|
27088 * (`ngSubmit`) |
|
27089 * - if a form has 2+ input fields and no buttons or input[type=submit] then hitting enter |
|
27090 * doesn't trigger submit |
|
27091 * - if a form has one or more input fields and one or more buttons or input[type=submit] then |
|
27092 * hitting enter in any of the input fields will trigger the click handler on the *first* button or |
|
27093 * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) |
|
27094 * |
|
27095 * Any pending `ngModelOptions` changes will take place immediately when an enclosing form is |
|
27096 * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` |
|
27097 * to have access to the updated model. |
|
27098 * |
|
27099 * ## Animation Hooks |
|
27100 * |
|
27101 * Animations in ngForm are triggered when any of the associated CSS classes are added and removed. |
|
27102 * These classes are: `.ng-pristine`, `.ng-dirty`, `.ng-invalid` and `.ng-valid` as well as any |
|
27103 * other validations that are performed within the form. Animations in ngForm are similar to how |
|
27104 * they work in ngClass and animations can be hooked into using CSS transitions, keyframes as well |
|
27105 * as JS animations. |
|
27106 * |
|
27107 * The following example shows a simple way to utilize CSS transitions to style a form element |
|
27108 * that has been rendered as invalid after it has been validated: |
|
27109 * |
|
27110 * <pre> |
|
27111 * //be sure to include ngAnimate as a module to hook into more |
|
27112 * //advanced animations |
|
27113 * .my-form { |
|
27114 * transition:0.5s linear all; |
|
27115 * background: white; |
|
27116 * } |
|
27117 * .my-form.ng-invalid { |
|
27118 * background: red; |
|
27119 * color:white; |
|
27120 * } |
|
27121 * </pre> |
|
27122 * |
|
27123 * @example |
|
27124 <example deps="angular-animate.js" animations="true" fixBase="true" module="formExample"> |
|
27125 <file name="index.html"> |
|
27126 <script> |
|
27127 angular.module('formExample', []) |
|
27128 .controller('FormController', ['$scope', function($scope) { |
|
27129 $scope.userType = 'guest'; |
|
27130 }]); |
|
27131 </script> |
|
27132 <style> |
|
27133 .my-form { |
|
27134 -webkit-transition:all linear 0.5s; |
|
27135 transition:all linear 0.5s; |
|
27136 background: transparent; |
|
27137 } |
|
27138 .my-form.ng-invalid { |
|
27139 background: red; |
|
27140 } |
|
27141 </style> |
|
27142 <form name="myForm" ng-controller="FormController" class="my-form"> |
|
27143 userType: <input name="input" ng-model="userType" required> |
|
27144 <span class="error" ng-show="myForm.input.$error.required">Required!</span><br> |
|
27145 <tt>userType = {{userType}}</tt><br> |
|
27146 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br> |
|
27147 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br> |
|
27148 <tt>myForm.$valid = {{myForm.$valid}}</tt><br> |
|
27149 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br> |
|
27150 </form> |
|
27151 </file> |
|
27152 <file name="protractor.js" type="protractor"> |
|
27153 it('should initialize to model', function() { |
|
27154 var userType = element(by.binding('userType')); |
|
27155 var valid = element(by.binding('myForm.input.$valid')); |
|
27156 |
|
27157 expect(userType.getText()).toContain('guest'); |
|
27158 expect(valid.getText()).toContain('true'); |
|
27159 }); |
|
27160 |
|
27161 it('should be invalid if empty', function() { |
|
27162 var userType = element(by.binding('userType')); |
|
27163 var valid = element(by.binding('myForm.input.$valid')); |
|
27164 var userInput = element(by.model('userType')); |
|
27165 |
|
27166 userInput.clear(); |
|
27167 userInput.sendKeys(''); |
|
27168 |
|
27169 expect(userType.getText()).toEqual('userType ='); |
|
27170 expect(valid.getText()).toContain('false'); |
|
27171 }); |
|
27172 </file> |
|
27173 </example> |
|
27174 * |
|
27175 * @param {string=} name Name of the form. If specified, the form controller will be published into |
|
27176 * related scope, under this name. |
|
27177 */ |
|
27178 var formDirectiveFactory = function(isNgForm) { |
|
27179 return ['$timeout', function($timeout) { |
|
27180 var formDirective = { |
|
27181 name: 'form', |
|
27182 restrict: isNgForm ? 'EAC' : 'E', |
|
27183 controller: FormController, |
|
27184 compile: function ngFormCompile(formElement) { |
|
27185 // Setup initial state of the control |
|
27186 formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS); |
|
27187 |
|
27188 return { |
|
27189 pre: function ngFormPreLink(scope, formElement, attr, controller) { |
|
27190 // if `action` attr is not present on the form, prevent the default action (submission) |
|
27191 if (!('action' in attr)) { |
|
27192 // we can't use jq events because if a form is destroyed during submission the default |
|
27193 // action is not prevented. see #1238 |
|
27194 // |
|
27195 // IE 9 is not affected because it doesn't fire a submit event and try to do a full |
|
27196 // page reload if the form was destroyed by submission of the form via a click handler |
|
27197 // on a button in the form. Looks like an IE9 specific bug. |
|
27198 var handleFormSubmission = function(event) { |
|
27199 scope.$apply(function() { |
|
27200 controller.$commitViewValue(); |
|
27201 controller.$setSubmitted(); |
|
27202 }); |
|
27203 |
|
27204 event.preventDefault |
|
27205 ? event.preventDefault() |
|
27206 : event.returnValue = false; // IE |
|
27207 }; |
|
27208 |
|
27209 addEventListenerFn(formElement[0], 'submit', handleFormSubmission); |
|
27210 |
|
27211 // unregister the preventDefault listener so that we don't not leak memory but in a |
|
27212 // way that will achieve the prevention of the default action. |
|
27213 formElement.on('$destroy', function() { |
|
27214 $timeout(function() { |
|
27215 removeEventListenerFn(formElement[0], 'submit', handleFormSubmission); |
|
27216 }, 0, false); |
|
27217 }); |
|
27218 } |
|
27219 |
|
27220 var parentFormCtrl = controller.$$parentForm, |
|
27221 alias = controller.$name; |
|
27222 |
|
27223 if (alias) { |
|
27224 setter(scope, alias, controller, alias); |
|
27225 attr.$observe(attr.name ? 'name' : 'ngForm', function(newValue) { |
|
27226 if (alias === newValue) return; |
|
27227 setter(scope, alias, undefined, alias); |
|
27228 alias = newValue; |
|
27229 setter(scope, alias, controller, alias); |
|
27230 parentFormCtrl.$$renameControl(controller, alias); |
|
27231 }); |
|
27232 } |
|
27233 if (parentFormCtrl !== nullFormCtrl) { |
|
27234 formElement.on('$destroy', function() { |
|
27235 parentFormCtrl.$removeControl(controller); |
|
27236 if (alias) { |
|
27237 setter(scope, alias, undefined, alias); |
|
27238 } |
|
27239 extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards |
|
27240 }); |
|
27241 } |
|
27242 } |
|
27243 }; |
|
27244 } |
|
27245 }; |
|
27246 |
|
27247 return formDirective; |
|
27248 }]; |
|
27249 }; |
|
27250 |
|
27251 var formDirective = formDirectiveFactory(); |
|
27252 var ngFormDirective = formDirectiveFactory(true); |
|
27253 |
|
27254 /* global VALID_CLASS: true, |
|
27255 INVALID_CLASS: true, |
|
27256 PRISTINE_CLASS: true, |
|
27257 DIRTY_CLASS: true, |
|
27258 UNTOUCHED_CLASS: true, |
|
27259 TOUCHED_CLASS: true, |
|
27260 */ |
|
27261 |
|
27262 // Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 |
|
27263 var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/; |
|
27264 var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; |
|
27265 var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; |
|
27266 var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; |
|
27267 var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/; |
|
27268 var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; |
|
27269 var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; |
|
27270 var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; |
|
27271 var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; |
|
27272 var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; |
|
27273 |
|
27274 var $ngModelMinErr = new minErr('ngModel'); |
|
27275 |
|
27276 var inputType = { |
|
27277 |
|
27278 /** |
|
27279 * @ngdoc input |
|
27280 * @name input[text] |
|
27281 * |
|
27282 * @description |
|
27283 * Standard HTML text input with angular data binding, inherited by most of the `input` elements. |
|
27284 * |
|
27285 * *NOTE* Not every feature offered is available for all input types. |
|
27286 * |
|
27287 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
27288 * @param {string=} name Property name of the form under which the control is published. |
|
27289 * @param {string=} required Adds `required` validation error key if the value is not entered. |
|
27290 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to |
|
27291 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of |
|
27292 * `required` when you want to data-bind to the `required` attribute. |
|
27293 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than |
|
27294 * minlength. |
|
27295 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than |
|
27296 * maxlength. |
|
27297 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the |
|
27298 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for |
|
27299 * patterns defined as scope expressions. |
|
27300 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
27301 * interaction with the input element. |
|
27302 * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. |
|
27303 * This parameter is ignored for input[type=password] controls, which will never trim the |
|
27304 * input. |
|
27305 * |
|
27306 * @example |
|
27307 <example name="text-input-directive" module="textInputExample"> |
|
27308 <file name="index.html"> |
|
27309 <script> |
|
27310 angular.module('textInputExample', []) |
|
27311 .controller('ExampleController', ['$scope', function($scope) { |
|
27312 $scope.text = 'guest'; |
|
27313 $scope.word = /^\s*\w*\s*$/; |
|
27314 }]); |
|
27315 </script> |
|
27316 <form name="myForm" ng-controller="ExampleController"> |
|
27317 Single word: <input type="text" name="input" ng-model="text" |
|
27318 ng-pattern="word" required ng-trim="false"> |
|
27319 <span class="error" ng-show="myForm.input.$error.required"> |
|
27320 Required!</span> |
|
27321 <span class="error" ng-show="myForm.input.$error.pattern"> |
|
27322 Single word only!</span> |
|
27323 |
|
27324 <tt>text = {{text}}</tt><br/> |
|
27325 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> |
|
27326 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> |
|
27327 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> |
|
27328 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> |
|
27329 </form> |
|
27330 </file> |
|
27331 <file name="protractor.js" type="protractor"> |
|
27332 var text = element(by.binding('text')); |
|
27333 var valid = element(by.binding('myForm.input.$valid')); |
|
27334 var input = element(by.model('text')); |
|
27335 |
|
27336 it('should initialize to model', function() { |
|
27337 expect(text.getText()).toContain('guest'); |
|
27338 expect(valid.getText()).toContain('true'); |
|
27339 }); |
|
27340 |
|
27341 it('should be invalid if empty', function() { |
|
27342 input.clear(); |
|
27343 input.sendKeys(''); |
|
27344 |
|
27345 expect(text.getText()).toEqual('text ='); |
|
27346 expect(valid.getText()).toContain('false'); |
|
27347 }); |
|
27348 |
|
27349 it('should be invalid if multi word', function() { |
|
27350 input.clear(); |
|
27351 input.sendKeys('hello world'); |
|
27352 |
|
27353 expect(valid.getText()).toContain('false'); |
|
27354 }); |
|
27355 </file> |
|
27356 </example> |
|
27357 */ |
|
27358 'text': textInputType, |
|
27359 |
|
27360 /** |
|
27361 * @ngdoc input |
|
27362 * @name input[date] |
|
27363 * |
|
27364 * @description |
|
27365 * Input with date validation and transformation. In browsers that do not yet support |
|
27366 * the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601 |
|
27367 * date format (yyyy-MM-dd), for example: `2009-01-06`. Since many |
|
27368 * modern browsers do not yet support this input type, it is important to provide cues to users on the |
|
27369 * expected input format via a placeholder or label. The model must always be a Date object. |
|
27370 * |
|
27371 * The timezone to be used to read/write the `Date` instance in the model can be defined using |
|
27372 * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. |
|
27373 * |
|
27374 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
27375 * @param {string=} name Property name of the form under which the control is published. |
|
27376 * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a |
|
27377 * valid ISO date string (yyyy-MM-dd). |
|
27378 * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be |
|
27379 * a valid ISO date string (yyyy-MM-dd). |
|
27380 * @param {string=} required Sets `required` validation error key if the value is not entered. |
|
27381 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to |
|
27382 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of |
|
27383 * `required` when you want to data-bind to the `required` attribute. |
|
27384 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
27385 * interaction with the input element. |
|
27386 * |
|
27387 * @example |
|
27388 <example name="date-input-directive" module="dateInputExample"> |
|
27389 <file name="index.html"> |
|
27390 <script> |
|
27391 angular.module('dateInputExample', []) |
|
27392 .controller('DateController', ['$scope', function($scope) { |
|
27393 $scope.value = new Date(2013, 9, 22); |
|
27394 }]); |
|
27395 </script> |
|
27396 <form name="myForm" ng-controller="DateController as dateCtrl"> |
|
27397 Pick a date in 2013: |
|
27398 <input type="date" id="exampleInput" name="input" ng-model="value" |
|
27399 placeholder="yyyy-MM-dd" min="2013-01-01" max="2013-12-31" required /> |
|
27400 <span class="error" ng-show="myForm.input.$error.required"> |
|
27401 Required!</span> |
|
27402 <span class="error" ng-show="myForm.input.$error.date"> |
|
27403 Not a valid date!</span> |
|
27404 <tt>value = {{value | date: "yyyy-MM-dd"}}</tt><br/> |
|
27405 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> |
|
27406 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> |
|
27407 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> |
|
27408 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> |
|
27409 </form> |
|
27410 </file> |
|
27411 <file name="protractor.js" type="protractor"> |
|
27412 var value = element(by.binding('value | date: "yyyy-MM-dd"')); |
|
27413 var valid = element(by.binding('myForm.input.$valid')); |
|
27414 var input = element(by.model('value')); |
|
27415 |
|
27416 // currently protractor/webdriver does not support |
|
27417 // sending keys to all known HTML5 input controls |
|
27418 // for various browsers (see https://github.com/angular/protractor/issues/562). |
|
27419 function setInput(val) { |
|
27420 // set the value of the element and force validation. |
|
27421 var scr = "var ipt = document.getElementById('exampleInput'); " + |
|
27422 "ipt.value = '" + val + "';" + |
|
27423 "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; |
|
27424 browser.executeScript(scr); |
|
27425 } |
|
27426 |
|
27427 it('should initialize to model', function() { |
|
27428 expect(value.getText()).toContain('2013-10-22'); |
|
27429 expect(valid.getText()).toContain('myForm.input.$valid = true'); |
|
27430 }); |
|
27431 |
|
27432 it('should be invalid if empty', function() { |
|
27433 setInput(''); |
|
27434 expect(value.getText()).toEqual('value ='); |
|
27435 expect(valid.getText()).toContain('myForm.input.$valid = false'); |
|
27436 }); |
|
27437 |
|
27438 it('should be invalid if over max', function() { |
|
27439 setInput('2015-01-01'); |
|
27440 expect(value.getText()).toContain(''); |
|
27441 expect(valid.getText()).toContain('myForm.input.$valid = false'); |
|
27442 }); |
|
27443 </file> |
|
27444 </example> |
|
27445 */ |
|
27446 'date': createDateInputType('date', DATE_REGEXP, |
|
27447 createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']), |
|
27448 'yyyy-MM-dd'), |
|
27449 |
|
27450 /** |
|
27451 * @ngdoc input |
|
27452 * @name input[dateTimeLocal] |
|
27453 * |
|
27454 * @description |
|
27455 * Input with datetime validation and transformation. In browsers that do not yet support |
|
27456 * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 |
|
27457 * local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`. The model must be a Date object. |
|
27458 * |
|
27459 * The timezone to be used to read/write the `Date` instance in the model can be defined using |
|
27460 * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. |
|
27461 * |
|
27462 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
27463 * @param {string=} name Property name of the form under which the control is published. |
|
27464 * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a |
|
27465 * valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). |
|
27466 * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be |
|
27467 * a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). |
|
27468 * @param {string=} required Sets `required` validation error key if the value is not entered. |
|
27469 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to |
|
27470 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of |
|
27471 * `required` when you want to data-bind to the `required` attribute. |
|
27472 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
27473 * interaction with the input element. |
|
27474 * |
|
27475 * @example |
|
27476 <example name="datetimelocal-input-directive" module="dateExample"> |
|
27477 <file name="index.html"> |
|
27478 <script> |
|
27479 angular.module('dateExample', []) |
|
27480 .controller('DateController', ['$scope', function($scope) { |
|
27481 $scope.value = new Date(2010, 11, 28, 14, 57); |
|
27482 }]); |
|
27483 </script> |
|
27484 <form name="myForm" ng-controller="DateController as dateCtrl"> |
|
27485 Pick a date between in 2013: |
|
27486 <input type="datetime-local" id="exampleInput" name="input" ng-model="value" |
|
27487 placeholder="yyyy-MM-ddTHH:mm:ss" min="2001-01-01T00:00:00" max="2013-12-31T00:00:00" required /> |
|
27488 <span class="error" ng-show="myForm.input.$error.required"> |
|
27489 Required!</span> |
|
27490 <span class="error" ng-show="myForm.input.$error.datetimelocal"> |
|
27491 Not a valid date!</span> |
|
27492 <tt>value = {{value | date: "yyyy-MM-ddTHH:mm:ss"}}</tt><br/> |
|
27493 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> |
|
27494 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> |
|
27495 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> |
|
27496 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> |
|
27497 </form> |
|
27498 </file> |
|
27499 <file name="protractor.js" type="protractor"> |
|
27500 var value = element(by.binding('value | date: "yyyy-MM-ddTHH:mm:ss"')); |
|
27501 var valid = element(by.binding('myForm.input.$valid')); |
|
27502 var input = element(by.model('value')); |
|
27503 |
|
27504 // currently protractor/webdriver does not support |
|
27505 // sending keys to all known HTML5 input controls |
|
27506 // for various browsers (https://github.com/angular/protractor/issues/562). |
|
27507 function setInput(val) { |
|
27508 // set the value of the element and force validation. |
|
27509 var scr = "var ipt = document.getElementById('exampleInput'); " + |
|
27510 "ipt.value = '" + val + "';" + |
|
27511 "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; |
|
27512 browser.executeScript(scr); |
|
27513 } |
|
27514 |
|
27515 it('should initialize to model', function() { |
|
27516 expect(value.getText()).toContain('2010-12-28T14:57:00'); |
|
27517 expect(valid.getText()).toContain('myForm.input.$valid = true'); |
|
27518 }); |
|
27519 |
|
27520 it('should be invalid if empty', function() { |
|
27521 setInput(''); |
|
27522 expect(value.getText()).toEqual('value ='); |
|
27523 expect(valid.getText()).toContain('myForm.input.$valid = false'); |
|
27524 }); |
|
27525 |
|
27526 it('should be invalid if over max', function() { |
|
27527 setInput('2015-01-01T23:59:00'); |
|
27528 expect(value.getText()).toContain(''); |
|
27529 expect(valid.getText()).toContain('myForm.input.$valid = false'); |
|
27530 }); |
|
27531 </file> |
|
27532 </example> |
|
27533 */ |
|
27534 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP, |
|
27535 createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']), |
|
27536 'yyyy-MM-ddTHH:mm:ss.sss'), |
|
27537 |
|
27538 /** |
|
27539 * @ngdoc input |
|
27540 * @name input[time] |
|
27541 * |
|
27542 * @description |
|
27543 * Input with time validation and transformation. In browsers that do not yet support |
|
27544 * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 |
|
27545 * local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a |
|
27546 * Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`. |
|
27547 * |
|
27548 * The timezone to be used to read/write the `Date` instance in the model can be defined using |
|
27549 * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. |
|
27550 * |
|
27551 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
27552 * @param {string=} name Property name of the form under which the control is published. |
|
27553 * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a |
|
27554 * valid ISO time format (HH:mm:ss). |
|
27555 * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be a |
|
27556 * valid ISO time format (HH:mm:ss). |
|
27557 * @param {string=} required Sets `required` validation error key if the value is not entered. |
|
27558 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to |
|
27559 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of |
|
27560 * `required` when you want to data-bind to the `required` attribute. |
|
27561 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
27562 * interaction with the input element. |
|
27563 * |
|
27564 * @example |
|
27565 <example name="time-input-directive" module="timeExample"> |
|
27566 <file name="index.html"> |
|
27567 <script> |
|
27568 angular.module('timeExample', []) |
|
27569 .controller('DateController', ['$scope', function($scope) { |
|
27570 $scope.value = new Date(1970, 0, 1, 14, 57, 0); |
|
27571 }]); |
|
27572 </script> |
|
27573 <form name="myForm" ng-controller="DateController as dateCtrl"> |
|
27574 Pick a between 8am and 5pm: |
|
27575 <input type="time" id="exampleInput" name="input" ng-model="value" |
|
27576 placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required /> |
|
27577 <span class="error" ng-show="myForm.input.$error.required"> |
|
27578 Required!</span> |
|
27579 <span class="error" ng-show="myForm.input.$error.time"> |
|
27580 Not a valid date!</span> |
|
27581 <tt>value = {{value | date: "HH:mm:ss"}}</tt><br/> |
|
27582 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> |
|
27583 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> |
|
27584 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> |
|
27585 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> |
|
27586 </form> |
|
27587 </file> |
|
27588 <file name="protractor.js" type="protractor"> |
|
27589 var value = element(by.binding('value | date: "HH:mm:ss"')); |
|
27590 var valid = element(by.binding('myForm.input.$valid')); |
|
27591 var input = element(by.model('value')); |
|
27592 |
|
27593 // currently protractor/webdriver does not support |
|
27594 // sending keys to all known HTML5 input controls |
|
27595 // for various browsers (https://github.com/angular/protractor/issues/562). |
|
27596 function setInput(val) { |
|
27597 // set the value of the element and force validation. |
|
27598 var scr = "var ipt = document.getElementById('exampleInput'); " + |
|
27599 "ipt.value = '" + val + "';" + |
|
27600 "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; |
|
27601 browser.executeScript(scr); |
|
27602 } |
|
27603 |
|
27604 it('should initialize to model', function() { |
|
27605 expect(value.getText()).toContain('14:57:00'); |
|
27606 expect(valid.getText()).toContain('myForm.input.$valid = true'); |
|
27607 }); |
|
27608 |
|
27609 it('should be invalid if empty', function() { |
|
27610 setInput(''); |
|
27611 expect(value.getText()).toEqual('value ='); |
|
27612 expect(valid.getText()).toContain('myForm.input.$valid = false'); |
|
27613 }); |
|
27614 |
|
27615 it('should be invalid if over max', function() { |
|
27616 setInput('23:59:00'); |
|
27617 expect(value.getText()).toContain(''); |
|
27618 expect(valid.getText()).toContain('myForm.input.$valid = false'); |
|
27619 }); |
|
27620 </file> |
|
27621 </example> |
|
27622 */ |
|
27623 'time': createDateInputType('time', TIME_REGEXP, |
|
27624 createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']), |
|
27625 'HH:mm:ss.sss'), |
|
27626 |
|
27627 /** |
|
27628 * @ngdoc input |
|
27629 * @name input[week] |
|
27630 * |
|
27631 * @description |
|
27632 * Input with week-of-the-year validation and transformation to Date. In browsers that do not yet support |
|
27633 * the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 |
|
27634 * week format (yyyy-W##), for example: `2013-W02`. The model must always be a Date object. |
|
27635 * |
|
27636 * The timezone to be used to read/write the `Date` instance in the model can be defined using |
|
27637 * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. |
|
27638 * |
|
27639 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
27640 * @param {string=} name Property name of the form under which the control is published. |
|
27641 * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a |
|
27642 * valid ISO week format (yyyy-W##). |
|
27643 * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be |
|
27644 * a valid ISO week format (yyyy-W##). |
|
27645 * @param {string=} required Sets `required` validation error key if the value is not entered. |
|
27646 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to |
|
27647 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of |
|
27648 * `required` when you want to data-bind to the `required` attribute. |
|
27649 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
27650 * interaction with the input element. |
|
27651 * |
|
27652 * @example |
|
27653 <example name="week-input-directive" module="weekExample"> |
|
27654 <file name="index.html"> |
|
27655 <script> |
|
27656 angular.module('weekExample', []) |
|
27657 .controller('DateController', ['$scope', function($scope) { |
|
27658 $scope.value = new Date(2013, 0, 3); |
|
27659 }]); |
|
27660 </script> |
|
27661 <form name="myForm" ng-controller="DateController as dateCtrl"> |
|
27662 Pick a date between in 2013: |
|
27663 <input id="exampleInput" type="week" name="input" ng-model="value" |
|
27664 placeholder="YYYY-W##" min="2012-W32" max="2013-W52" required /> |
|
27665 <span class="error" ng-show="myForm.input.$error.required"> |
|
27666 Required!</span> |
|
27667 <span class="error" ng-show="myForm.input.$error.week"> |
|
27668 Not a valid date!</span> |
|
27669 <tt>value = {{value | date: "yyyy-Www"}}</tt><br/> |
|
27670 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> |
|
27671 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> |
|
27672 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> |
|
27673 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> |
|
27674 </form> |
|
27675 </file> |
|
27676 <file name="protractor.js" type="protractor"> |
|
27677 var value = element(by.binding('value | date: "yyyy-Www"')); |
|
27678 var valid = element(by.binding('myForm.input.$valid')); |
|
27679 var input = element(by.model('value')); |
|
27680 |
|
27681 // currently protractor/webdriver does not support |
|
27682 // sending keys to all known HTML5 input controls |
|
27683 // for various browsers (https://github.com/angular/protractor/issues/562). |
|
27684 function setInput(val) { |
|
27685 // set the value of the element and force validation. |
|
27686 var scr = "var ipt = document.getElementById('exampleInput'); " + |
|
27687 "ipt.value = '" + val + "';" + |
|
27688 "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; |
|
27689 browser.executeScript(scr); |
|
27690 } |
|
27691 |
|
27692 it('should initialize to model', function() { |
|
27693 expect(value.getText()).toContain('2013-W01'); |
|
27694 expect(valid.getText()).toContain('myForm.input.$valid = true'); |
|
27695 }); |
|
27696 |
|
27697 it('should be invalid if empty', function() { |
|
27698 setInput(''); |
|
27699 expect(value.getText()).toEqual('value ='); |
|
27700 expect(valid.getText()).toContain('myForm.input.$valid = false'); |
|
27701 }); |
|
27702 |
|
27703 it('should be invalid if over max', function() { |
|
27704 setInput('2015-W01'); |
|
27705 expect(value.getText()).toContain(''); |
|
27706 expect(valid.getText()).toContain('myForm.input.$valid = false'); |
|
27707 }); |
|
27708 </file> |
|
27709 </example> |
|
27710 */ |
|
27711 'week': createDateInputType('week', WEEK_REGEXP, weekParser, 'yyyy-Www'), |
|
27712 |
|
27713 /** |
|
27714 * @ngdoc input |
|
27715 * @name input[month] |
|
27716 * |
|
27717 * @description |
|
27718 * Input with month validation and transformation. In browsers that do not yet support |
|
27719 * the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 |
|
27720 * month format (yyyy-MM), for example: `2009-01`. The model must always be a Date object. In the event the model is |
|
27721 * not set to the first of the month, the first of that model's month is assumed. |
|
27722 * |
|
27723 * The timezone to be used to read/write the `Date` instance in the model can be defined using |
|
27724 * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. |
|
27725 * |
|
27726 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
27727 * @param {string=} name Property name of the form under which the control is published. |
|
27728 * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be |
|
27729 * a valid ISO month format (yyyy-MM). |
|
27730 * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must |
|
27731 * be a valid ISO month format (yyyy-MM). |
|
27732 * @param {string=} required Sets `required` validation error key if the value is not entered. |
|
27733 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to |
|
27734 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of |
|
27735 * `required` when you want to data-bind to the `required` attribute. |
|
27736 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
27737 * interaction with the input element. |
|
27738 * |
|
27739 * @example |
|
27740 <example name="month-input-directive" module="monthExample"> |
|
27741 <file name="index.html"> |
|
27742 <script> |
|
27743 angular.module('monthExample', []) |
|
27744 .controller('DateController', ['$scope', function($scope) { |
|
27745 $scope.value = new Date(2013, 9, 1); |
|
27746 }]); |
|
27747 </script> |
|
27748 <form name="myForm" ng-controller="DateController as dateCtrl"> |
|
27749 Pick a month int 2013: |
|
27750 <input id="exampleInput" type="month" name="input" ng-model="value" |
|
27751 placeholder="yyyy-MM" min="2013-01" max="2013-12" required /> |
|
27752 <span class="error" ng-show="myForm.input.$error.required"> |
|
27753 Required!</span> |
|
27754 <span class="error" ng-show="myForm.input.$error.month"> |
|
27755 Not a valid month!</span> |
|
27756 <tt>value = {{value | date: "yyyy-MM"}}</tt><br/> |
|
27757 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> |
|
27758 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> |
|
27759 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> |
|
27760 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> |
|
27761 </form> |
|
27762 </file> |
|
27763 <file name="protractor.js" type="protractor"> |
|
27764 var value = element(by.binding('value | date: "yyyy-MM"')); |
|
27765 var valid = element(by.binding('myForm.input.$valid')); |
|
27766 var input = element(by.model('value')); |
|
27767 |
|
27768 // currently protractor/webdriver does not support |
|
27769 // sending keys to all known HTML5 input controls |
|
27770 // for various browsers (https://github.com/angular/protractor/issues/562). |
|
27771 function setInput(val) { |
|
27772 // set the value of the element and force validation. |
|
27773 var scr = "var ipt = document.getElementById('exampleInput'); " + |
|
27774 "ipt.value = '" + val + "';" + |
|
27775 "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; |
|
27776 browser.executeScript(scr); |
|
27777 } |
|
27778 |
|
27779 it('should initialize to model', function() { |
|
27780 expect(value.getText()).toContain('2013-10'); |
|
27781 expect(valid.getText()).toContain('myForm.input.$valid = true'); |
|
27782 }); |
|
27783 |
|
27784 it('should be invalid if empty', function() { |
|
27785 setInput(''); |
|
27786 expect(value.getText()).toEqual('value ='); |
|
27787 expect(valid.getText()).toContain('myForm.input.$valid = false'); |
|
27788 }); |
|
27789 |
|
27790 it('should be invalid if over max', function() { |
|
27791 setInput('2015-01'); |
|
27792 expect(value.getText()).toContain(''); |
|
27793 expect(valid.getText()).toContain('myForm.input.$valid = false'); |
|
27794 }); |
|
27795 </file> |
|
27796 </example> |
|
27797 */ |
|
27798 'month': createDateInputType('month', MONTH_REGEXP, |
|
27799 createDateParser(MONTH_REGEXP, ['yyyy', 'MM']), |
|
27800 'yyyy-MM'), |
|
27801 |
|
27802 /** |
|
27803 * @ngdoc input |
|
27804 * @name input[number] |
|
27805 * |
|
27806 * @description |
|
27807 * Text input with number validation and transformation. Sets the `number` validation |
|
27808 * error if not a valid number. |
|
27809 * |
|
27810 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
27811 * @param {string=} name Property name of the form under which the control is published. |
|
27812 * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. |
|
27813 * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. |
|
27814 * @param {string=} required Sets `required` validation error key if the value is not entered. |
|
27815 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to |
|
27816 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of |
|
27817 * `required` when you want to data-bind to the `required` attribute. |
|
27818 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than |
|
27819 * minlength. |
|
27820 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than |
|
27821 * maxlength. |
|
27822 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the |
|
27823 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for |
|
27824 * patterns defined as scope expressions. |
|
27825 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
27826 * interaction with the input element. |
|
27827 * |
|
27828 * @example |
|
27829 <example name="number-input-directive" module="numberExample"> |
|
27830 <file name="index.html"> |
|
27831 <script> |
|
27832 angular.module('numberExample', []) |
|
27833 .controller('ExampleController', ['$scope', function($scope) { |
|
27834 $scope.value = 12; |
|
27835 }]); |
|
27836 </script> |
|
27837 <form name="myForm" ng-controller="ExampleController"> |
|
27838 Number: <input type="number" name="input" ng-model="value" |
|
27839 min="0" max="99" required> |
|
27840 <span class="error" ng-show="myForm.input.$error.required"> |
|
27841 Required!</span> |
|
27842 <span class="error" ng-show="myForm.input.$error.number"> |
|
27843 Not valid number!</span> |
|
27844 <tt>value = {{value}}</tt><br/> |
|
27845 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> |
|
27846 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> |
|
27847 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> |
|
27848 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> |
|
27849 </form> |
|
27850 </file> |
|
27851 <file name="protractor.js" type="protractor"> |
|
27852 var value = element(by.binding('value')); |
|
27853 var valid = element(by.binding('myForm.input.$valid')); |
|
27854 var input = element(by.model('value')); |
|
27855 |
|
27856 it('should initialize to model', function() { |
|
27857 expect(value.getText()).toContain('12'); |
|
27858 expect(valid.getText()).toContain('true'); |
|
27859 }); |
|
27860 |
|
27861 it('should be invalid if empty', function() { |
|
27862 input.clear(); |
|
27863 input.sendKeys(''); |
|
27864 expect(value.getText()).toEqual('value ='); |
|
27865 expect(valid.getText()).toContain('false'); |
|
27866 }); |
|
27867 |
|
27868 it('should be invalid if over max', function() { |
|
27869 input.clear(); |
|
27870 input.sendKeys('123'); |
|
27871 expect(value.getText()).toEqual('value ='); |
|
27872 expect(valid.getText()).toContain('false'); |
|
27873 }); |
|
27874 </file> |
|
27875 </example> |
|
27876 */ |
|
27877 'number': numberInputType, |
|
27878 |
|
27879 |
|
27880 /** |
|
27881 * @ngdoc input |
|
27882 * @name input[url] |
|
27883 * |
|
27884 * @description |
|
27885 * Text input with URL validation. Sets the `url` validation error key if the content is not a |
|
27886 * valid URL. |
|
27887 * |
|
27888 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
27889 * @param {string=} name Property name of the form under which the control is published. |
|
27890 * @param {string=} required Sets `required` validation error key if the value is not entered. |
|
27891 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to |
|
27892 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of |
|
27893 * `required` when you want to data-bind to the `required` attribute. |
|
27894 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than |
|
27895 * minlength. |
|
27896 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than |
|
27897 * maxlength. |
|
27898 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the |
|
27899 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for |
|
27900 * patterns defined as scope expressions. |
|
27901 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
27902 * interaction with the input element. |
|
27903 * |
|
27904 * @example |
|
27905 <example name="url-input-directive" module="urlExample"> |
|
27906 <file name="index.html"> |
|
27907 <script> |
|
27908 angular.module('urlExample', []) |
|
27909 .controller('ExampleController', ['$scope', function($scope) { |
|
27910 $scope.text = 'http://google.com'; |
|
27911 }]); |
|
27912 </script> |
|
27913 <form name="myForm" ng-controller="ExampleController"> |
|
27914 URL: <input type="url" name="input" ng-model="text" required> |
|
27915 <span class="error" ng-show="myForm.input.$error.required"> |
|
27916 Required!</span> |
|
27917 <span class="error" ng-show="myForm.input.$error.url"> |
|
27918 Not valid url!</span> |
|
27919 <tt>text = {{text}}</tt><br/> |
|
27920 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> |
|
27921 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> |
|
27922 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> |
|
27923 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> |
|
27924 <tt>myForm.$error.url = {{!!myForm.$error.url}}</tt><br/> |
|
27925 </form> |
|
27926 </file> |
|
27927 <file name="protractor.js" type="protractor"> |
|
27928 var text = element(by.binding('text')); |
|
27929 var valid = element(by.binding('myForm.input.$valid')); |
|
27930 var input = element(by.model('text')); |
|
27931 |
|
27932 it('should initialize to model', function() { |
|
27933 expect(text.getText()).toContain('http://google.com'); |
|
27934 expect(valid.getText()).toContain('true'); |
|
27935 }); |
|
27936 |
|
27937 it('should be invalid if empty', function() { |
|
27938 input.clear(); |
|
27939 input.sendKeys(''); |
|
27940 |
|
27941 expect(text.getText()).toEqual('text ='); |
|
27942 expect(valid.getText()).toContain('false'); |
|
27943 }); |
|
27944 |
|
27945 it('should be invalid if not url', function() { |
|
27946 input.clear(); |
|
27947 input.sendKeys('box'); |
|
27948 |
|
27949 expect(valid.getText()).toContain('false'); |
|
27950 }); |
|
27951 </file> |
|
27952 </example> |
|
27953 */ |
|
27954 'url': urlInputType, |
|
27955 |
|
27956 |
|
27957 /** |
|
27958 * @ngdoc input |
|
27959 * @name input[email] |
|
27960 * |
|
27961 * @description |
|
27962 * Text input with email validation. Sets the `email` validation error key if not a valid email |
|
27963 * address. |
|
27964 * |
|
27965 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
27966 * @param {string=} name Property name of the form under which the control is published. |
|
27967 * @param {string=} required Sets `required` validation error key if the value is not entered. |
|
27968 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to |
|
27969 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of |
|
27970 * `required` when you want to data-bind to the `required` attribute. |
|
27971 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than |
|
27972 * minlength. |
|
27973 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than |
|
27974 * maxlength. |
|
27975 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the |
|
27976 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for |
|
27977 * patterns defined as scope expressions. |
|
27978 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
27979 * interaction with the input element. |
|
27980 * |
|
27981 * @example |
|
27982 <example name="email-input-directive" module="emailExample"> |
|
27983 <file name="index.html"> |
|
27984 <script> |
|
27985 angular.module('emailExample', []) |
|
27986 .controller('ExampleController', ['$scope', function($scope) { |
|
27987 $scope.text = 'me@example.com'; |
|
27988 }]); |
|
27989 </script> |
|
27990 <form name="myForm" ng-controller="ExampleController"> |
|
27991 Email: <input type="email" name="input" ng-model="text" required> |
|
27992 <span class="error" ng-show="myForm.input.$error.required"> |
|
27993 Required!</span> |
|
27994 <span class="error" ng-show="myForm.input.$error.email"> |
|
27995 Not valid email!</span> |
|
27996 <tt>text = {{text}}</tt><br/> |
|
27997 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> |
|
27998 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> |
|
27999 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> |
|
28000 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> |
|
28001 <tt>myForm.$error.email = {{!!myForm.$error.email}}</tt><br/> |
|
28002 </form> |
|
28003 </file> |
|
28004 <file name="protractor.js" type="protractor"> |
|
28005 var text = element(by.binding('text')); |
|
28006 var valid = element(by.binding('myForm.input.$valid')); |
|
28007 var input = element(by.model('text')); |
|
28008 |
|
28009 it('should initialize to model', function() { |
|
28010 expect(text.getText()).toContain('me@example.com'); |
|
28011 expect(valid.getText()).toContain('true'); |
|
28012 }); |
|
28013 |
|
28014 it('should be invalid if empty', function() { |
|
28015 input.clear(); |
|
28016 input.sendKeys(''); |
|
28017 expect(text.getText()).toEqual('text ='); |
|
28018 expect(valid.getText()).toContain('false'); |
|
28019 }); |
|
28020 |
|
28021 it('should be invalid if not email', function() { |
|
28022 input.clear(); |
|
28023 input.sendKeys('xxx'); |
|
28024 |
|
28025 expect(valid.getText()).toContain('false'); |
|
28026 }); |
|
28027 </file> |
|
28028 </example> |
|
28029 */ |
|
28030 'email': emailInputType, |
|
28031 |
|
28032 |
|
28033 /** |
|
28034 * @ngdoc input |
|
28035 * @name input[radio] |
|
28036 * |
|
28037 * @description |
|
28038 * HTML radio button. |
|
28039 * |
|
28040 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
28041 * @param {string} value The value to which the expression should be set when selected. |
|
28042 * @param {string=} name Property name of the form under which the control is published. |
|
28043 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
28044 * interaction with the input element. |
|
28045 * @param {string} ngValue Angular expression which sets the value to which the expression should |
|
28046 * be set when selected. |
|
28047 * |
|
28048 * @example |
|
28049 <example name="radio-input-directive" module="radioExample"> |
|
28050 <file name="index.html"> |
|
28051 <script> |
|
28052 angular.module('radioExample', []) |
|
28053 .controller('ExampleController', ['$scope', function($scope) { |
|
28054 $scope.color = 'blue'; |
|
28055 $scope.specialValue = { |
|
28056 "id": "12345", |
|
28057 "value": "green" |
|
28058 }; |
|
28059 }]); |
|
28060 </script> |
|
28061 <form name="myForm" ng-controller="ExampleController"> |
|
28062 <input type="radio" ng-model="color" value="red"> Red <br/> |
|
28063 <input type="radio" ng-model="color" ng-value="specialValue"> Green <br/> |
|
28064 <input type="radio" ng-model="color" value="blue"> Blue <br/> |
|
28065 <tt>color = {{color | json}}</tt><br/> |
|
28066 </form> |
|
28067 Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`. |
|
28068 </file> |
|
28069 <file name="protractor.js" type="protractor"> |
|
28070 it('should change state', function() { |
|
28071 var color = element(by.binding('color')); |
|
28072 |
|
28073 expect(color.getText()).toContain('blue'); |
|
28074 |
|
28075 element.all(by.model('color')).get(0).click(); |
|
28076 |
|
28077 expect(color.getText()).toContain('red'); |
|
28078 }); |
|
28079 </file> |
|
28080 </example> |
|
28081 */ |
|
28082 'radio': radioInputType, |
|
28083 |
|
28084 |
|
28085 /** |
|
28086 * @ngdoc input |
|
28087 * @name input[checkbox] |
|
28088 * |
|
28089 * @description |
|
28090 * HTML checkbox. |
|
28091 * |
|
28092 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
28093 * @param {string=} name Property name of the form under which the control is published. |
|
28094 * @param {expression=} ngTrueValue The value to which the expression should be set when selected. |
|
28095 * @param {expression=} ngFalseValue The value to which the expression should be set when not selected. |
|
28096 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
28097 * interaction with the input element. |
|
28098 * |
|
28099 * @example |
|
28100 <example name="checkbox-input-directive" module="checkboxExample"> |
|
28101 <file name="index.html"> |
|
28102 <script> |
|
28103 angular.module('checkboxExample', []) |
|
28104 .controller('ExampleController', ['$scope', function($scope) { |
|
28105 $scope.value1 = true; |
|
28106 $scope.value2 = 'YES' |
|
28107 }]); |
|
28108 </script> |
|
28109 <form name="myForm" ng-controller="ExampleController"> |
|
28110 Value1: <input type="checkbox" ng-model="value1"> <br/> |
|
28111 Value2: <input type="checkbox" ng-model="value2" |
|
28112 ng-true-value="'YES'" ng-false-value="'NO'"> <br/> |
|
28113 <tt>value1 = {{value1}}</tt><br/> |
|
28114 <tt>value2 = {{value2}}</tt><br/> |
|
28115 </form> |
|
28116 </file> |
|
28117 <file name="protractor.js" type="protractor"> |
|
28118 it('should change state', function() { |
|
28119 var value1 = element(by.binding('value1')); |
|
28120 var value2 = element(by.binding('value2')); |
|
28121 |
|
28122 expect(value1.getText()).toContain('true'); |
|
28123 expect(value2.getText()).toContain('YES'); |
|
28124 |
|
28125 element(by.model('value1')).click(); |
|
28126 element(by.model('value2')).click(); |
|
28127 |
|
28128 expect(value1.getText()).toContain('false'); |
|
28129 expect(value2.getText()).toContain('NO'); |
|
28130 }); |
|
28131 </file> |
|
28132 </example> |
|
28133 */ |
|
28134 'checkbox': checkboxInputType, |
|
28135 |
|
28136 'hidden': noop, |
|
28137 'button': noop, |
|
28138 'submit': noop, |
|
28139 'reset': noop, |
|
28140 'file': noop |
|
28141 }; |
|
28142 |
|
28143 function testFlags(validity, flags) { |
|
28144 var i, flag; |
|
28145 if (flags) { |
|
28146 for (i=0; i<flags.length; ++i) { |
|
28147 flag = flags[i]; |
|
28148 if (validity[flag]) { |
|
28149 return true; |
|
28150 } |
|
28151 } |
|
28152 } |
|
28153 return false; |
|
28154 } |
|
28155 |
|
28156 function stringBasedInputType(ctrl) { |
|
28157 ctrl.$formatters.push(function(value) { |
|
28158 return ctrl.$isEmpty(value) ? value : value.toString(); |
|
28159 }); |
|
28160 } |
|
28161 |
|
28162 function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { |
|
28163 baseInputType(scope, element, attr, ctrl, $sniffer, $browser); |
|
28164 stringBasedInputType(ctrl); |
|
28165 } |
|
28166 |
|
28167 function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { |
|
28168 var validity = element.prop(VALIDITY_STATE_PROPERTY); |
|
28169 var placeholder = element[0].placeholder, noevent = {}; |
|
28170 var type = lowercase(element[0].type); |
|
28171 |
|
28172 // In composition mode, users are still inputing intermediate text buffer, |
|
28173 // hold the listener until composition is done. |
|
28174 // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent |
|
28175 if (!$sniffer.android) { |
|
28176 var composing = false; |
|
28177 |
|
28178 element.on('compositionstart', function(data) { |
|
28179 composing = true; |
|
28180 }); |
|
28181 |
|
28182 element.on('compositionend', function() { |
|
28183 composing = false; |
|
28184 listener(); |
|
28185 }); |
|
28186 } |
|
28187 |
|
28188 var listener = function(ev) { |
|
28189 if (composing) return; |
|
28190 var value = element.val(), |
|
28191 event = ev && ev.type; |
|
28192 |
|
28193 // IE (11 and under) seem to emit an 'input' event if the placeholder value changes. |
|
28194 // We don't want to dirty the value when this happens, so we abort here. Unfortunately, |
|
28195 // IE also sends input events for other non-input-related things, (such as focusing on a |
|
28196 // form control), so this change is not entirely enough to solve this. |
|
28197 if (msie && (ev || noevent).type === 'input' && element[0].placeholder !== placeholder) { |
|
28198 placeholder = element[0].placeholder; |
|
28199 return; |
|
28200 } |
|
28201 |
|
28202 // By default we will trim the value |
|
28203 // If the attribute ng-trim exists we will avoid trimming |
|
28204 // If input type is 'password', the value is never trimmed |
|
28205 if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) { |
|
28206 value = trim(value); |
|
28207 } |
|
28208 |
|
28209 // If a control is suffering from bad input (due to native validators), browsers discard its |
|
28210 // value, so it may be necessary to revalidate (by calling $setViewValue again) even if the |
|
28211 // control's value is the same empty value twice in a row. |
|
28212 if (ctrl.$viewValue !== value || (value === '' && ctrl.$$hasNativeValidators)) { |
|
28213 ctrl.$setViewValue(value, event); |
|
28214 } |
|
28215 }; |
|
28216 |
|
28217 // if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the |
|
28218 // input event on backspace, delete or cut |
|
28219 if ($sniffer.hasEvent('input')) { |
|
28220 element.on('input', listener); |
|
28221 } else { |
|
28222 var timeout; |
|
28223 |
|
28224 var deferListener = function(ev) { |
|
28225 if (!timeout) { |
|
28226 timeout = $browser.defer(function() { |
|
28227 listener(ev); |
|
28228 timeout = null; |
|
28229 }); |
|
28230 } |
|
28231 }; |
|
28232 |
|
28233 element.on('keydown', function(event) { |
|
28234 var key = event.keyCode; |
|
28235 |
|
28236 // ignore |
|
28237 // command modifiers arrows |
|
28238 if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; |
|
28239 |
|
28240 deferListener(event); |
|
28241 }); |
|
28242 |
|
28243 // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it |
|
28244 if ($sniffer.hasEvent('paste')) { |
|
28245 element.on('paste cut', deferListener); |
|
28246 } |
|
28247 } |
|
28248 |
|
28249 // if user paste into input using mouse on older browser |
|
28250 // or form autocomplete on newer browser, we need "change" event to catch it |
|
28251 element.on('change', listener); |
|
28252 |
|
28253 ctrl.$render = function() { |
|
28254 element.val(ctrl.$isEmpty(ctrl.$modelValue) ? '' : ctrl.$viewValue); |
|
28255 }; |
|
28256 } |
|
28257 |
|
28258 function weekParser(isoWeek, existingDate) { |
|
28259 if (isDate(isoWeek)) { |
|
28260 return isoWeek; |
|
28261 } |
|
28262 |
|
28263 if (isString(isoWeek)) { |
|
28264 WEEK_REGEXP.lastIndex = 0; |
|
28265 var parts = WEEK_REGEXP.exec(isoWeek); |
|
28266 if (parts) { |
|
28267 var year = +parts[1], |
|
28268 week = +parts[2], |
|
28269 hours = 0, |
|
28270 minutes = 0, |
|
28271 seconds = 0, |
|
28272 milliseconds = 0, |
|
28273 firstThurs = getFirstThursdayOfYear(year), |
|
28274 addDays = (week - 1) * 7; |
|
28275 |
|
28276 if (existingDate) { |
|
28277 hours = existingDate.getHours(); |
|
28278 minutes = existingDate.getMinutes(); |
|
28279 seconds = existingDate.getSeconds(); |
|
28280 milliseconds = existingDate.getMilliseconds(); |
|
28281 } |
|
28282 |
|
28283 return new Date(year, 0, firstThurs.getDate() + addDays, hours, minutes, seconds, milliseconds); |
|
28284 } |
|
28285 } |
|
28286 |
|
28287 return NaN; |
|
28288 } |
|
28289 |
|
28290 function createDateParser(regexp, mapping) { |
|
28291 return function(iso, date) { |
|
28292 var parts, map; |
|
28293 |
|
28294 if (isDate(iso)) { |
|
28295 return iso; |
|
28296 } |
|
28297 |
|
28298 if (isString(iso)) { |
|
28299 // When a date is JSON'ified to wraps itself inside of an extra |
|
28300 // set of double quotes. This makes the date parsing code unable |
|
28301 // to match the date string and parse it as a date. |
|
28302 if (iso.charAt(0) == '"' && iso.charAt(iso.length-1) == '"') { |
|
28303 iso = iso.substring(1, iso.length-1); |
|
28304 } |
|
28305 if (ISO_DATE_REGEXP.test(iso)) { |
|
28306 return new Date(iso); |
|
28307 } |
|
28308 regexp.lastIndex = 0; |
|
28309 parts = regexp.exec(iso); |
|
28310 |
|
28311 if (parts) { |
|
28312 parts.shift(); |
|
28313 if (date) { |
|
28314 map = { |
|
28315 yyyy: date.getFullYear(), |
|
28316 MM: date.getMonth() + 1, |
|
28317 dd: date.getDate(), |
|
28318 HH: date.getHours(), |
|
28319 mm: date.getMinutes(), |
|
28320 ss: date.getSeconds(), |
|
28321 sss: date.getMilliseconds() / 1000 |
|
28322 }; |
|
28323 } else { |
|
28324 map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 }; |
|
28325 } |
|
28326 |
|
28327 forEach(parts, function(part, index) { |
|
28328 if (index < mapping.length) { |
|
28329 map[mapping[index]] = +part; |
|
28330 } |
|
28331 }); |
|
28332 return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); |
|
28333 } |
|
28334 } |
|
28335 |
|
28336 return NaN; |
|
28337 }; |
|
28338 } |
|
28339 |
|
28340 function createDateInputType(type, regexp, parseDate, format) { |
|
28341 return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { |
|
28342 badInputChecker(scope, element, attr, ctrl); |
|
28343 baseInputType(scope, element, attr, ctrl, $sniffer, $browser); |
|
28344 var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; |
|
28345 var previousDate; |
|
28346 |
|
28347 ctrl.$$parserName = type; |
|
28348 ctrl.$parsers.push(function(value) { |
|
28349 if (ctrl.$isEmpty(value)) return null; |
|
28350 if (regexp.test(value)) { |
|
28351 // Note: We cannot read ctrl.$modelValue, as there might be a different |
|
28352 // parser/formatter in the processing chain so that the model |
|
28353 // contains some different data format! |
|
28354 var parsedDate = parseDate(value, previousDate); |
|
28355 if (timezone === 'UTC') { |
|
28356 parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset()); |
|
28357 } |
|
28358 return parsedDate; |
|
28359 } |
|
28360 return undefined; |
|
28361 }); |
|
28362 |
|
28363 ctrl.$formatters.push(function(value) { |
|
28364 if (!ctrl.$isEmpty(value)) { |
|
28365 if (!isDate(value)) { |
|
28366 throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); |
|
28367 } |
|
28368 previousDate = value; |
|
28369 if (previousDate && timezone === 'UTC') { |
|
28370 var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); |
|
28371 previousDate = new Date(previousDate.getTime() + timezoneOffset); |
|
28372 } |
|
28373 return $filter('date')(value, format, timezone); |
|
28374 } else { |
|
28375 previousDate = null; |
|
28376 } |
|
28377 return ''; |
|
28378 }); |
|
28379 |
|
28380 if (isDefined(attr.min) || attr.ngMin) { |
|
28381 var minVal; |
|
28382 ctrl.$validators.min = function(value) { |
|
28383 return ctrl.$isEmpty(value) || isUndefined(minVal) || parseDate(value) >= minVal; |
|
28384 }; |
|
28385 attr.$observe('min', function(val) { |
|
28386 minVal = parseObservedDateValue(val); |
|
28387 ctrl.$validate(); |
|
28388 }); |
|
28389 } |
|
28390 |
|
28391 if (isDefined(attr.max) || attr.ngMax) { |
|
28392 var maxVal; |
|
28393 ctrl.$validators.max = function(value) { |
|
28394 return ctrl.$isEmpty(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; |
|
28395 }; |
|
28396 attr.$observe('max', function(val) { |
|
28397 maxVal = parseObservedDateValue(val); |
|
28398 ctrl.$validate(); |
|
28399 }); |
|
28400 } |
|
28401 // Override the standard $isEmpty to detect invalid dates as well |
|
28402 ctrl.$isEmpty = function(value) { |
|
28403 // Invalid Date: getTime() returns NaN |
|
28404 return !value || (value.getTime && value.getTime() !== value.getTime()); |
|
28405 }; |
|
28406 |
|
28407 function parseObservedDateValue(val) { |
|
28408 return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; |
|
28409 } |
|
28410 }; |
|
28411 } |
|
28412 |
|
28413 function badInputChecker(scope, element, attr, ctrl) { |
|
28414 var node = element[0]; |
|
28415 var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); |
|
28416 if (nativeValidation) { |
|
28417 ctrl.$parsers.push(function(value) { |
|
28418 var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; |
|
28419 // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430): |
|
28420 // - also sets validity.badInput (should only be validity.typeMismatch). |
|
28421 // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email) |
|
28422 // - can ignore this case as we can still read out the erroneous email... |
|
28423 return validity.badInput && !validity.typeMismatch ? undefined : value; |
|
28424 }); |
|
28425 } |
|
28426 } |
|
28427 |
|
28428 function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { |
|
28429 badInputChecker(scope, element, attr, ctrl); |
|
28430 baseInputType(scope, element, attr, ctrl, $sniffer, $browser); |
|
28431 |
|
28432 ctrl.$$parserName = 'number'; |
|
28433 ctrl.$parsers.push(function(value) { |
|
28434 if (ctrl.$isEmpty(value)) return null; |
|
28435 if (NUMBER_REGEXP.test(value)) return parseFloat(value); |
|
28436 return undefined; |
|
28437 }); |
|
28438 |
|
28439 ctrl.$formatters.push(function(value) { |
|
28440 if (!ctrl.$isEmpty(value)) { |
|
28441 if (!isNumber(value)) { |
|
28442 throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); |
|
28443 } |
|
28444 value = value.toString(); |
|
28445 } |
|
28446 return value; |
|
28447 }); |
|
28448 |
|
28449 if (attr.min || attr.ngMin) { |
|
28450 var minVal; |
|
28451 ctrl.$validators.min = function(value) { |
|
28452 return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; |
|
28453 }; |
|
28454 |
|
28455 attr.$observe('min', function(val) { |
|
28456 if (isDefined(val) && !isNumber(val)) { |
|
28457 val = parseFloat(val, 10); |
|
28458 } |
|
28459 minVal = isNumber(val) && !isNaN(val) ? val : undefined; |
|
28460 // TODO(matsko): implement validateLater to reduce number of validations |
|
28461 ctrl.$validate(); |
|
28462 }); |
|
28463 } |
|
28464 |
|
28465 if (attr.max || attr.ngMax) { |
|
28466 var maxVal; |
|
28467 ctrl.$validators.max = function(value) { |
|
28468 return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; |
|
28469 }; |
|
28470 |
|
28471 attr.$observe('max', function(val) { |
|
28472 if (isDefined(val) && !isNumber(val)) { |
|
28473 val = parseFloat(val, 10); |
|
28474 } |
|
28475 maxVal = isNumber(val) && !isNaN(val) ? val : undefined; |
|
28476 // TODO(matsko): implement validateLater to reduce number of validations |
|
28477 ctrl.$validate(); |
|
28478 }); |
|
28479 } |
|
28480 } |
|
28481 |
|
28482 function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { |
|
28483 // Note: no badInputChecker here by purpose as `url` is only a validation |
|
28484 // in browsers, i.e. we can always read out input.value even if it is not valid! |
|
28485 baseInputType(scope, element, attr, ctrl, $sniffer, $browser); |
|
28486 stringBasedInputType(ctrl); |
|
28487 |
|
28488 ctrl.$$parserName = 'url'; |
|
28489 ctrl.$validators.url = function(value) { |
|
28490 return ctrl.$isEmpty(value) || URL_REGEXP.test(value); |
|
28491 }; |
|
28492 } |
|
28493 |
|
28494 function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { |
|
28495 // Note: no badInputChecker here by purpose as `url` is only a validation |
|
28496 // in browsers, i.e. we can always read out input.value even if it is not valid! |
|
28497 baseInputType(scope, element, attr, ctrl, $sniffer, $browser); |
|
28498 stringBasedInputType(ctrl); |
|
28499 |
|
28500 ctrl.$$parserName = 'email'; |
|
28501 ctrl.$validators.email = function(value) { |
|
28502 return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); |
|
28503 }; |
|
28504 } |
|
28505 |
|
28506 function radioInputType(scope, element, attr, ctrl) { |
|
28507 // make the name unique, if not defined |
|
28508 if (isUndefined(attr.name)) { |
|
28509 element.attr('name', nextUid()); |
|
28510 } |
|
28511 |
|
28512 var listener = function(ev) { |
|
28513 if (element[0].checked) { |
|
28514 ctrl.$setViewValue(attr.value, ev && ev.type); |
|
28515 } |
|
28516 }; |
|
28517 |
|
28518 element.on('click', listener); |
|
28519 |
|
28520 ctrl.$render = function() { |
|
28521 var value = attr.value; |
|
28522 element[0].checked = (value == ctrl.$viewValue); |
|
28523 }; |
|
28524 |
|
28525 attr.$observe('value', ctrl.$render); |
|
28526 } |
|
28527 |
|
28528 function parseConstantExpr($parse, context, name, expression, fallback) { |
|
28529 var parseFn; |
|
28530 if (isDefined(expression)) { |
|
28531 parseFn = $parse(expression); |
|
28532 if (!parseFn.constant) { |
|
28533 throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' + |
|
28534 '`{1}`.', name, expression); |
|
28535 } |
|
28536 return parseFn(context); |
|
28537 } |
|
28538 return fallback; |
|
28539 } |
|
28540 |
|
28541 function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { |
|
28542 var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true); |
|
28543 var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false); |
|
28544 |
|
28545 var listener = function(ev) { |
|
28546 ctrl.$setViewValue(element[0].checked, ev && ev.type); |
|
28547 }; |
|
28548 |
|
28549 element.on('click', listener); |
|
28550 |
|
28551 ctrl.$render = function() { |
|
28552 element[0].checked = ctrl.$viewValue; |
|
28553 }; |
|
28554 |
|
28555 // Override the standard `$isEmpty` because an empty checkbox is never equal to the trueValue |
|
28556 ctrl.$isEmpty = function(value) { |
|
28557 return value !== trueValue; |
|
28558 }; |
|
28559 |
|
28560 ctrl.$formatters.push(function(value) { |
|
28561 return equals(value, trueValue); |
|
28562 }); |
|
28563 |
|
28564 ctrl.$parsers.push(function(value) { |
|
28565 return value ? trueValue : falseValue; |
|
28566 }); |
|
28567 } |
|
28568 |
|
28569 |
|
28570 /** |
|
28571 * @ngdoc directive |
|
28572 * @name textarea |
|
28573 * @restrict E |
|
28574 * |
|
28575 * @description |
|
28576 * HTML textarea element control with angular data-binding. The data-binding and validation |
|
28577 * properties of this element are exactly the same as those of the |
|
28578 * {@link ng.directive:input input element}. |
|
28579 * |
|
28580 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
28581 * @param {string=} name Property name of the form under which the control is published. |
|
28582 * @param {string=} required Sets `required` validation error key if the value is not entered. |
|
28583 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to |
|
28584 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of |
|
28585 * `required` when you want to data-bind to the `required` attribute. |
|
28586 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than |
|
28587 * minlength. |
|
28588 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than |
|
28589 * maxlength. |
|
28590 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the |
|
28591 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for |
|
28592 * patterns defined as scope expressions. |
|
28593 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
28594 * interaction with the input element. |
|
28595 * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. |
|
28596 */ |
|
28597 |
|
28598 |
|
28599 /** |
|
28600 * @ngdoc directive |
|
28601 * @name input |
|
28602 * @restrict E |
|
28603 * |
|
28604 * @description |
|
28605 * HTML input element control with angular data-binding. Input control follows HTML5 input types |
|
28606 * and polyfills the HTML5 validation behavior for older browsers. |
|
28607 * |
|
28608 * *NOTE* Not every feature offered is available for all input types. |
|
28609 * |
|
28610 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
28611 * @param {string=} name Property name of the form under which the control is published. |
|
28612 * @param {string=} required Sets `required` validation error key if the value is not entered. |
|
28613 * @param {boolean=} ngRequired Sets `required` attribute if set to true |
|
28614 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than |
|
28615 * minlength. |
|
28616 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than |
|
28617 * maxlength. |
|
28618 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the |
|
28619 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for |
|
28620 * patterns defined as scope expressions. |
|
28621 * @param {string=} ngChange Angular expression to be executed when input changes due to user |
|
28622 * interaction with the input element. |
|
28623 * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. |
|
28624 * This parameter is ignored for input[type=password] controls, which will never trim the |
|
28625 * input. |
|
28626 * |
|
28627 * @example |
|
28628 <example name="input-directive" module="inputExample"> |
|
28629 <file name="index.html"> |
|
28630 <script> |
|
28631 angular.module('inputExample', []) |
|
28632 .controller('ExampleController', ['$scope', function($scope) { |
|
28633 $scope.user = {name: 'guest', last: 'visitor'}; |
|
28634 }]); |
|
28635 </script> |
|
28636 <div ng-controller="ExampleController"> |
|
28637 <form name="myForm"> |
|
28638 User name: <input type="text" name="userName" ng-model="user.name" required> |
|
28639 <span class="error" ng-show="myForm.userName.$error.required"> |
|
28640 Required!</span><br> |
|
28641 Last name: <input type="text" name="lastName" ng-model="user.last" |
|
28642 ng-minlength="3" ng-maxlength="10"> |
|
28643 <span class="error" ng-show="myForm.lastName.$error.minlength"> |
|
28644 Too short!</span> |
|
28645 <span class="error" ng-show="myForm.lastName.$error.maxlength"> |
|
28646 Too long!</span><br> |
|
28647 </form> |
|
28648 <hr> |
|
28649 <tt>user = {{user}}</tt><br/> |
|
28650 <tt>myForm.userName.$valid = {{myForm.userName.$valid}}</tt><br> |
|
28651 <tt>myForm.userName.$error = {{myForm.userName.$error}}</tt><br> |
|
28652 <tt>myForm.lastName.$valid = {{myForm.lastName.$valid}}</tt><br> |
|
28653 <tt>myForm.lastName.$error = {{myForm.lastName.$error}}</tt><br> |
|
28654 <tt>myForm.$valid = {{myForm.$valid}}</tt><br> |
|
28655 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br> |
|
28656 <tt>myForm.$error.minlength = {{!!myForm.$error.minlength}}</tt><br> |
|
28657 <tt>myForm.$error.maxlength = {{!!myForm.$error.maxlength}}</tt><br> |
|
28658 </div> |
|
28659 </file> |
|
28660 <file name="protractor.js" type="protractor"> |
|
28661 var user = element(by.exactBinding('user')); |
|
28662 var userNameValid = element(by.binding('myForm.userName.$valid')); |
|
28663 var lastNameValid = element(by.binding('myForm.lastName.$valid')); |
|
28664 var lastNameError = element(by.binding('myForm.lastName.$error')); |
|
28665 var formValid = element(by.binding('myForm.$valid')); |
|
28666 var userNameInput = element(by.model('user.name')); |
|
28667 var userLastInput = element(by.model('user.last')); |
|
28668 |
|
28669 it('should initialize to model', function() { |
|
28670 expect(user.getText()).toContain('{"name":"guest","last":"visitor"}'); |
|
28671 expect(userNameValid.getText()).toContain('true'); |
|
28672 expect(formValid.getText()).toContain('true'); |
|
28673 }); |
|
28674 |
|
28675 it('should be invalid if empty when required', function() { |
|
28676 userNameInput.clear(); |
|
28677 userNameInput.sendKeys(''); |
|
28678 |
|
28679 expect(user.getText()).toContain('{"last":"visitor"}'); |
|
28680 expect(userNameValid.getText()).toContain('false'); |
|
28681 expect(formValid.getText()).toContain('false'); |
|
28682 }); |
|
28683 |
|
28684 it('should be valid if empty when min length is set', function() { |
|
28685 userLastInput.clear(); |
|
28686 userLastInput.sendKeys(''); |
|
28687 |
|
28688 expect(user.getText()).toContain('{"name":"guest","last":""}'); |
|
28689 expect(lastNameValid.getText()).toContain('true'); |
|
28690 expect(formValid.getText()).toContain('true'); |
|
28691 }); |
|
28692 |
|
28693 it('should be invalid if less than required min length', function() { |
|
28694 userLastInput.clear(); |
|
28695 userLastInput.sendKeys('xx'); |
|
28696 |
|
28697 expect(user.getText()).toContain('{"name":"guest"}'); |
|
28698 expect(lastNameValid.getText()).toContain('false'); |
|
28699 expect(lastNameError.getText()).toContain('minlength'); |
|
28700 expect(formValid.getText()).toContain('false'); |
|
28701 }); |
|
28702 |
|
28703 it('should be invalid if longer than max length', function() { |
|
28704 userLastInput.clear(); |
|
28705 userLastInput.sendKeys('some ridiculously long name'); |
|
28706 |
|
28707 expect(user.getText()).toContain('{"name":"guest"}'); |
|
28708 expect(lastNameValid.getText()).toContain('false'); |
|
28709 expect(lastNameError.getText()).toContain('maxlength'); |
|
28710 expect(formValid.getText()).toContain('false'); |
|
28711 }); |
|
28712 </file> |
|
28713 </example> |
|
28714 */ |
|
28715 var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', |
|
28716 function($browser, $sniffer, $filter, $parse) { |
|
28717 return { |
|
28718 restrict: 'E', |
|
28719 require: ['?ngModel'], |
|
28720 link: { |
|
28721 pre: function(scope, element, attr, ctrls) { |
|
28722 if (ctrls[0]) { |
|
28723 (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, |
|
28724 $browser, $filter, $parse); |
|
28725 } |
|
28726 } |
|
28727 } |
|
28728 }; |
|
28729 }]; |
|
28730 |
|
28731 var VALID_CLASS = 'ng-valid', |
|
28732 INVALID_CLASS = 'ng-invalid', |
|
28733 PRISTINE_CLASS = 'ng-pristine', |
|
28734 DIRTY_CLASS = 'ng-dirty', |
|
28735 UNTOUCHED_CLASS = 'ng-untouched', |
|
28736 TOUCHED_CLASS = 'ng-touched', |
|
28737 PENDING_CLASS = 'ng-pending'; |
|
28738 |
|
28739 /** |
|
28740 * @ngdoc type |
|
28741 * @name ngModel.NgModelController |
|
28742 * |
|
28743 * @property {string} $viewValue Actual string value in the view. |
|
28744 * @property {*} $modelValue The value in the model, that the control is bound to. |
|
28745 * @property {Array.<Function>} $parsers Array of functions to execute, as a pipeline, whenever |
|
28746 the control reads value from the DOM. Each function is called, in turn, passing the value |
|
28747 through to the next. The last return value is used to populate the model. |
|
28748 Used to sanitize / convert the value as well as validation. For validation, |
|
28749 the parsers should update the validity state using |
|
28750 {@link ngModel.NgModelController#$setValidity $setValidity()}, |
|
28751 and return `undefined` for invalid values. |
|
28752 |
|
28753 * |
|
28754 * @property {Array.<Function>} $formatters Array of functions to execute, as a pipeline, whenever |
|
28755 the model value changes. Each function is called, in turn, passing the value through to the |
|
28756 next. Used to format / convert values for display in the control and validation. |
|
28757 * ```js |
|
28758 * function formatter(value) { |
|
28759 * if (value) { |
|
28760 * return value.toUpperCase(); |
|
28761 * } |
|
28762 * } |
|
28763 * ngModel.$formatters.push(formatter); |
|
28764 * ``` |
|
28765 * |
|
28766 * @property {Object.<string, function>} $validators A collection of validators that are applied |
|
28767 * whenever the model value changes. The key value within the object refers to the name of the |
|
28768 * validator while the function refers to the validation operation. The validation operation is |
|
28769 * provided with the model value as an argument and must return a true or false value depending |
|
28770 * on the response of that validation. |
|
28771 * |
|
28772 * ```js |
|
28773 * ngModel.$validators.validCharacters = function(modelValue, viewValue) { |
|
28774 * var value = modelValue || viewValue; |
|
28775 * return /[0-9]+/.test(value) && |
|
28776 * /[a-z]+/.test(value) && |
|
28777 * /[A-Z]+/.test(value) && |
|
28778 * /\W+/.test(value); |
|
28779 * }; |
|
28780 * ``` |
|
28781 * |
|
28782 * @property {Object.<string, function>} $asyncValidators A collection of validations that are expected to |
|
28783 * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided |
|
28784 * is expected to return a promise when it is run during the model validation process. Once the promise |
|
28785 * is delivered then the validation status will be set to true when fulfilled and false when rejected. |
|
28786 * When the asynchronous validators are triggered, each of the validators will run in parallel and the model |
|
28787 * value will only be updated once all validators have been fulfilled. Also, keep in mind that all |
|
28788 * asynchronous validators will only run once all synchronous validators have passed. |
|
28789 * |
|
28790 * Please note that if $http is used then it is important that the server returns a success HTTP response code |
|
28791 * in order to fulfill the validation and a status level of `4xx` in order to reject the validation. |
|
28792 * |
|
28793 * ```js |
|
28794 * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) { |
|
28795 * var value = modelValue || viewValue; |
|
28796 * |
|
28797 * // Lookup user by username |
|
28798 * return $http.get('/api/users/' + value). |
|
28799 * then(function resolved() { |
|
28800 * //username exists, this means validation fails |
|
28801 * return $q.reject('exists'); |
|
28802 * }, function rejected() { |
|
28803 * //username does not exist, therefore this validation passes |
|
28804 * return true; |
|
28805 * }); |
|
28806 * }; |
|
28807 * ``` |
|
28808 * |
|
28809 * @param {string} name The name of the validator. |
|
28810 * @param {Function} validationFn The validation function that will be run. |
|
28811 * |
|
28812 * @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the |
|
28813 * view value has changed. It is called with no arguments, and its return value is ignored. |
|
28814 * This can be used in place of additional $watches against the model value. |
|
28815 * |
|
28816 * @property {Object} $error An object hash with all failing validator ids as keys. |
|
28817 * @property {Object} $pending An object hash with all pending validator ids as keys. |
|
28818 * |
|
28819 * @property {boolean} $untouched True if control has not lost focus yet. |
|
28820 * @property {boolean} $touched True if control has lost focus. |
|
28821 * @property {boolean} $pristine True if user has not interacted with the control yet. |
|
28822 * @property {boolean} $dirty True if user has already interacted with the control. |
|
28823 * @property {boolean} $valid True if there is no error. |
|
28824 * @property {boolean} $invalid True if at least one error on the control. |
|
28825 * |
|
28826 * @description |
|
28827 * |
|
28828 * `NgModelController` provides API for the `ng-model` directive. The controller contains |
|
28829 * services for data-binding, validation, CSS updates, and value formatting and parsing. It |
|
28830 * purposefully does not contain any logic which deals with DOM rendering or listening to |
|
28831 * DOM events. Such DOM related logic should be provided by other directives which make use of |
|
28832 * `NgModelController` for data-binding. |
|
28833 * |
|
28834 * ## Custom Control Example |
|
28835 * This example shows how to use `NgModelController` with a custom control to achieve |
|
28836 * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) |
|
28837 * collaborate together to achieve the desired result. |
|
28838 * |
|
28839 * Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element |
|
28840 * contents be edited in place by the user. This will not work on older browsers. |
|
28841 * |
|
28842 * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize} |
|
28843 * module to automatically remove "bad" content like inline event listener (e.g. `<span onclick="...">`). |
|
28844 * However, as we are using `$sce` the model can still decide to to provide unsafe content if it marks |
|
28845 * that content using the `$sce` service. |
|
28846 * |
|
28847 * <example name="NgModelController" module="customControl" deps="angular-sanitize.js"> |
|
28848 <file name="style.css"> |
|
28849 [contenteditable] { |
|
28850 border: 1px solid black; |
|
28851 background-color: white; |
|
28852 min-height: 20px; |
|
28853 } |
|
28854 |
|
28855 .ng-invalid { |
|
28856 border: 1px solid red; |
|
28857 } |
|
28858 |
|
28859 </file> |
|
28860 <file name="script.js"> |
|
28861 angular.module('customControl', ['ngSanitize']). |
|
28862 directive('contenteditable', ['$sce', function($sce) { |
|
28863 return { |
|
28864 restrict: 'A', // only activate on element attribute |
|
28865 require: '?ngModel', // get a hold of NgModelController |
|
28866 link: function(scope, element, attrs, ngModel) { |
|
28867 if (!ngModel) return; // do nothing if no ng-model |
|
28868 |
|
28869 // Specify how UI should be updated |
|
28870 ngModel.$render = function() { |
|
28871 element.html($sce.getTrustedHtml(ngModel.$viewValue || '')); |
|
28872 }; |
|
28873 |
|
28874 // Listen for change events to enable binding |
|
28875 element.on('blur keyup change', function() { |
|
28876 scope.$apply(read); |
|
28877 }); |
|
28878 read(); // initialize |
|
28879 |
|
28880 // Write data to the model |
|
28881 function read() { |
|
28882 var html = element.html(); |
|
28883 // When we clear the content editable the browser leaves a <br> behind |
|
28884 // If strip-br attribute is provided then we strip this out |
|
28885 if ( attrs.stripBr && html == '<br>' ) { |
|
28886 html = ''; |
|
28887 } |
|
28888 ngModel.$setViewValue(html); |
|
28889 } |
|
28890 } |
|
28891 }; |
|
28892 }]); |
|
28893 </file> |
|
28894 <file name="index.html"> |
|
28895 <form name="myForm"> |
|
28896 <div contenteditable |
|
28897 name="myWidget" ng-model="userContent" |
|
28898 strip-br="true" |
|
28899 required>Change me!</div> |
|
28900 <span ng-show="myForm.myWidget.$error.required">Required!</span> |
|
28901 <hr> |
|
28902 <textarea ng-model="userContent"></textarea> |
|
28903 </form> |
|
28904 </file> |
|
28905 <file name="protractor.js" type="protractor"> |
|
28906 it('should data-bind and become invalid', function() { |
|
28907 if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') { |
|
28908 // SafariDriver can't handle contenteditable |
|
28909 // and Firefox driver can't clear contenteditables very well |
|
28910 return; |
|
28911 } |
|
28912 var contentEditable = element(by.css('[contenteditable]')); |
|
28913 var content = 'Change me!'; |
|
28914 |
|
28915 expect(contentEditable.getText()).toEqual(content); |
|
28916 |
|
28917 contentEditable.clear(); |
|
28918 contentEditable.sendKeys(protractor.Key.BACK_SPACE); |
|
28919 expect(contentEditable.getText()).toEqual(''); |
|
28920 expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/); |
|
28921 }); |
|
28922 </file> |
|
28923 * </example> |
|
28924 * |
|
28925 * |
|
28926 */ |
|
28927 var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', |
|
28928 function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) { |
|
28929 this.$viewValue = Number.NaN; |
|
28930 this.$modelValue = Number.NaN; |
|
28931 this.$validators = {}; |
|
28932 this.$asyncValidators = {}; |
|
28933 this.$parsers = []; |
|
28934 this.$formatters = []; |
|
28935 this.$viewChangeListeners = []; |
|
28936 this.$untouched = true; |
|
28937 this.$touched = false; |
|
28938 this.$pristine = true; |
|
28939 this.$dirty = false; |
|
28940 this.$valid = true; |
|
28941 this.$invalid = false; |
|
28942 this.$error = {}; // keep invalid keys here |
|
28943 this.$$success = {}; // keep valid keys here |
|
28944 this.$pending = undefined; // keep pending keys here |
|
28945 this.$name = $interpolate($attr.name || '', false)($scope); |
|
28946 |
|
28947 |
|
28948 var parsedNgModel = $parse($attr.ngModel), |
|
28949 pendingDebounce = null, |
|
28950 ctrl = this; |
|
28951 |
|
28952 var ngModelGet = function ngModelGet() { |
|
28953 var modelValue = parsedNgModel($scope); |
|
28954 if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) { |
|
28955 modelValue = modelValue(); |
|
28956 } |
|
28957 return modelValue; |
|
28958 }; |
|
28959 |
|
28960 var ngModelSet = function ngModelSet(newValue) { |
|
28961 var getterSetter; |
|
28962 if (ctrl.$options && ctrl.$options.getterSetter && |
|
28963 isFunction(getterSetter = parsedNgModel($scope))) { |
|
28964 |
|
28965 getterSetter(ctrl.$modelValue); |
|
28966 } else { |
|
28967 parsedNgModel.assign($scope, ctrl.$modelValue); |
|
28968 } |
|
28969 }; |
|
28970 |
|
28971 this.$$setOptions = function(options) { |
|
28972 ctrl.$options = options; |
|
28973 |
|
28974 if (!parsedNgModel.assign && (!options || !options.getterSetter)) { |
|
28975 throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", |
|
28976 $attr.ngModel, startingTag($element)); |
|
28977 } |
|
28978 }; |
|
28979 |
|
28980 /** |
|
28981 * @ngdoc method |
|
28982 * @name ngModel.NgModelController#$render |
|
28983 * |
|
28984 * @description |
|
28985 * Called when the view needs to be updated. It is expected that the user of the ng-model |
|
28986 * directive will implement this method. |
|
28987 * |
|
28988 * The `$render()` method is invoked in the following situations: |
|
28989 * |
|
28990 * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last |
|
28991 * committed value then `$render()` is called to update the input control. |
|
28992 * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and |
|
28993 * the `$viewValue` are different to last time. |
|
28994 * |
|
28995 * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of |
|
28996 * `$modelValue` and `$viewValue` are actually different to their previous value. If `$modelValue` |
|
28997 * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be |
|
28998 * invoked if you only change a property on the objects. |
|
28999 */ |
|
29000 this.$render = noop; |
|
29001 |
|
29002 /** |
|
29003 * @ngdoc method |
|
29004 * @name ngModel.NgModelController#$isEmpty |
|
29005 * |
|
29006 * @description |
|
29007 * This is called when we need to determine if the value of the input is empty. |
|
29008 * |
|
29009 * For instance, the required directive does this to work out if the input has data or not. |
|
29010 * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. |
|
29011 * |
|
29012 * You can override this for input directives whose concept of being empty is different to the |
|
29013 * default. The `checkboxInputType` directive does this because in its case a value of `false` |
|
29014 * implies empty. |
|
29015 * |
|
29016 * @param {*} value Model value to check. |
|
29017 * @returns {boolean} True if `value` is empty. |
|
29018 */ |
|
29019 this.$isEmpty = function(value) { |
|
29020 return isUndefined(value) || value === '' || value === null || value !== value; |
|
29021 }; |
|
29022 |
|
29023 var parentForm = $element.inheritedData('$formController') || nullFormCtrl, |
|
29024 currentValidationRunId = 0; |
|
29025 |
|
29026 /** |
|
29027 * @ngdoc method |
|
29028 * @name ngModel.NgModelController#$setValidity |
|
29029 * |
|
29030 * @description |
|
29031 * Change the validity state, and notifies the form. |
|
29032 * |
|
29033 * This method can be called within $parsers/$formatters. However, if possible, please use the |
|
29034 * `ngModel.$validators` pipeline which is designed to call this method automatically. |
|
29035 * |
|
29036 * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign |
|
29037 * to `$error[validationErrorKey]` and `$pending[validationErrorKey]` |
|
29038 * so that it is available for data-binding. |
|
29039 * The `validationErrorKey` should be in camelCase and will get converted into dash-case |
|
29040 * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` |
|
29041 * class and can be bound to as `{{someForm.someControl.$error.myError}}` . |
|
29042 * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), |
|
29043 * or skipped (null). |
|
29044 */ |
|
29045 addSetValidityMethod({ |
|
29046 ctrl: this, |
|
29047 $element: $element, |
|
29048 set: function(object, property) { |
|
29049 object[property] = true; |
|
29050 }, |
|
29051 unset: function(object, property) { |
|
29052 delete object[property]; |
|
29053 }, |
|
29054 parentForm: parentForm, |
|
29055 $animate: $animate |
|
29056 }); |
|
29057 |
|
29058 /** |
|
29059 * @ngdoc method |
|
29060 * @name ngModel.NgModelController#$setPristine |
|
29061 * |
|
29062 * @description |
|
29063 * Sets the control to its pristine state. |
|
29064 * |
|
29065 * This method can be called to remove the 'ng-dirty' class and set the control to its pristine |
|
29066 * state (ng-pristine class). A model is considered to be pristine when the model has not been changed |
|
29067 * from when first compiled within then form. |
|
29068 */ |
|
29069 this.$setPristine = function () { |
|
29070 ctrl.$dirty = false; |
|
29071 ctrl.$pristine = true; |
|
29072 $animate.removeClass($element, DIRTY_CLASS); |
|
29073 $animate.addClass($element, PRISTINE_CLASS); |
|
29074 }; |
|
29075 |
|
29076 /** |
|
29077 * @ngdoc method |
|
29078 * @name ngModel.NgModelController#$setUntouched |
|
29079 * |
|
29080 * @description |
|
29081 * Sets the control to its untouched state. |
|
29082 * |
|
29083 * This method can be called to remove the 'ng-touched' class and set the control to its |
|
29084 * untouched state (ng-untouched class). Upon compilation, a model is set as untouched |
|
29085 * by default, however this function can be used to restore that state if the model has |
|
29086 * already been touched by the user. |
|
29087 */ |
|
29088 this.$setUntouched = function() { |
|
29089 ctrl.$touched = false; |
|
29090 ctrl.$untouched = true; |
|
29091 $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS); |
|
29092 }; |
|
29093 |
|
29094 /** |
|
29095 * @ngdoc method |
|
29096 * @name ngModel.NgModelController#$setTouched |
|
29097 * |
|
29098 * @description |
|
29099 * Sets the control to its touched state. |
|
29100 * |
|
29101 * This method can be called to remove the 'ng-untouched' class and set the control to its |
|
29102 * touched state (ng-touched class). A model is considered to be touched when the user has |
|
29103 * first interacted (focussed) on the model input element and then shifted focus away (blurred) |
|
29104 * from the input element. |
|
29105 */ |
|
29106 this.$setTouched = function() { |
|
29107 ctrl.$touched = true; |
|
29108 ctrl.$untouched = false; |
|
29109 $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS); |
|
29110 }; |
|
29111 |
|
29112 /** |
|
29113 * @ngdoc method |
|
29114 * @name ngModel.NgModelController#$rollbackViewValue |
|
29115 * |
|
29116 * @description |
|
29117 * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`, |
|
29118 * which may be caused by a pending debounced event or because the input is waiting for a some |
|
29119 * future event. |
|
29120 * |
|
29121 * If you have an input that uses `ng-model-options` to set up debounced events or events such |
|
29122 * as blur you can have a situation where there is a period when the `$viewValue` |
|
29123 * is out of synch with the ngModel's `$modelValue`. |
|
29124 * |
|
29125 * In this case, you can run into difficulties if you try to update the ngModel's `$modelValue` |
|
29126 * programmatically before these debounced/future events have resolved/occurred, because Angular's |
|
29127 * dirty checking mechanism is not able to tell whether the model has actually changed or not. |
|
29128 * |
|
29129 * The `$rollbackViewValue()` method should be called before programmatically changing the model of an |
|
29130 * input which may have such events pending. This is important in order to make sure that the |
|
29131 * input field will be updated with the new model value and any pending operations are cancelled. |
|
29132 * |
|
29133 * <example name="ng-model-cancel-update" module="cancel-update-example"> |
|
29134 * <file name="app.js"> |
|
29135 * angular.module('cancel-update-example', []) |
|
29136 * |
|
29137 * .controller('CancelUpdateController', ['$scope', function($scope) { |
|
29138 * $scope.resetWithCancel = function (e) { |
|
29139 * if (e.keyCode == 27) { |
|
29140 * $scope.myForm.myInput1.$rollbackViewValue(); |
|
29141 * $scope.myValue = ''; |
|
29142 * } |
|
29143 * }; |
|
29144 * $scope.resetWithoutCancel = function (e) { |
|
29145 * if (e.keyCode == 27) { |
|
29146 * $scope.myValue = ''; |
|
29147 * } |
|
29148 * }; |
|
29149 * }]); |
|
29150 * </file> |
|
29151 * <file name="index.html"> |
|
29152 * <div ng-controller="CancelUpdateController"> |
|
29153 * <p>Try typing something in each input. See that the model only updates when you |
|
29154 * blur off the input. |
|
29155 * </p> |
|
29156 * <p>Now see what happens if you start typing then press the Escape key</p> |
|
29157 * |
|
29158 * <form name="myForm" ng-model-options="{ updateOn: 'blur' }"> |
|
29159 * <p>With $rollbackViewValue()</p> |
|
29160 * <input name="myInput1" ng-model="myValue" ng-keydown="resetWithCancel($event)"><br/> |
|
29161 * myValue: "{{ myValue }}" |
|
29162 * |
|
29163 * <p>Without $rollbackViewValue()</p> |
|
29164 * <input name="myInput2" ng-model="myValue" ng-keydown="resetWithoutCancel($event)"><br/> |
|
29165 * myValue: "{{ myValue }}" |
|
29166 * </form> |
|
29167 * </div> |
|
29168 * </file> |
|
29169 * </example> |
|
29170 */ |
|
29171 this.$rollbackViewValue = function() { |
|
29172 $timeout.cancel(pendingDebounce); |
|
29173 ctrl.$viewValue = ctrl.$$lastCommittedViewValue; |
|
29174 ctrl.$render(); |
|
29175 }; |
|
29176 |
|
29177 /** |
|
29178 * @ngdoc method |
|
29179 * @name ngModel.NgModelController#$validate |
|
29180 * |
|
29181 * @description |
|
29182 * Runs each of the registered validators (first synchronous validators and then asynchronous validators). |
|
29183 */ |
|
29184 this.$validate = function() { |
|
29185 // ignore $validate before model is initialized |
|
29186 if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { |
|
29187 return; |
|
29188 } |
|
29189 this.$$parseAndValidate(); |
|
29190 }; |
|
29191 |
|
29192 this.$$runValidators = function(parseValid, modelValue, viewValue, doneCallback) { |
|
29193 currentValidationRunId++; |
|
29194 var localValidationRunId = currentValidationRunId; |
|
29195 |
|
29196 // check parser error |
|
29197 if (!processParseErrors(parseValid)) { |
|
29198 validationDone(false); |
|
29199 return; |
|
29200 } |
|
29201 if (!processSyncValidators()) { |
|
29202 validationDone(false); |
|
29203 return; |
|
29204 } |
|
29205 processAsyncValidators(); |
|
29206 |
|
29207 function processParseErrors(parseValid) { |
|
29208 var errorKey = ctrl.$$parserName || 'parse'; |
|
29209 if (parseValid === undefined) { |
|
29210 setValidity(errorKey, null); |
|
29211 } else { |
|
29212 setValidity(errorKey, parseValid); |
|
29213 if (!parseValid) { |
|
29214 forEach(ctrl.$validators, function(v, name) { |
|
29215 setValidity(name, null); |
|
29216 }); |
|
29217 forEach(ctrl.$asyncValidators, function(v, name) { |
|
29218 setValidity(name, null); |
|
29219 }); |
|
29220 return false; |
|
29221 } |
|
29222 } |
|
29223 return true; |
|
29224 } |
|
29225 |
|
29226 function processSyncValidators() { |
|
29227 var syncValidatorsValid = true; |
|
29228 forEach(ctrl.$validators, function(validator, name) { |
|
29229 var result = validator(modelValue, viewValue); |
|
29230 syncValidatorsValid = syncValidatorsValid && result; |
|
29231 setValidity(name, result); |
|
29232 }); |
|
29233 if (!syncValidatorsValid) { |
|
29234 forEach(ctrl.$asyncValidators, function(v, name) { |
|
29235 setValidity(name, null); |
|
29236 }); |
|
29237 return false; |
|
29238 } |
|
29239 return true; |
|
29240 } |
|
29241 |
|
29242 function processAsyncValidators() { |
|
29243 var validatorPromises = []; |
|
29244 var allValid = true; |
|
29245 forEach(ctrl.$asyncValidators, function(validator, name) { |
|
29246 var promise = validator(modelValue, viewValue); |
|
29247 if (!isPromiseLike(promise)) { |
|
29248 throw $ngModelMinErr("$asyncValidators", |
|
29249 "Expected asynchronous validator to return a promise but got '{0}' instead.", promise); |
|
29250 } |
|
29251 setValidity(name, undefined); |
|
29252 validatorPromises.push(promise.then(function() { |
|
29253 setValidity(name, true); |
|
29254 }, function(error) { |
|
29255 allValid = false; |
|
29256 setValidity(name, false); |
|
29257 })); |
|
29258 }); |
|
29259 if (!validatorPromises.length) { |
|
29260 validationDone(true); |
|
29261 } else { |
|
29262 $q.all(validatorPromises).then(function() { |
|
29263 validationDone(allValid); |
|
29264 }, noop); |
|
29265 } |
|
29266 } |
|
29267 |
|
29268 function setValidity(name, isValid) { |
|
29269 if (localValidationRunId === currentValidationRunId) { |
|
29270 ctrl.$setValidity(name, isValid); |
|
29271 } |
|
29272 } |
|
29273 |
|
29274 function validationDone(allValid) { |
|
29275 if (localValidationRunId === currentValidationRunId) { |
|
29276 |
|
29277 doneCallback(allValid); |
|
29278 } |
|
29279 } |
|
29280 }; |
|
29281 |
|
29282 /** |
|
29283 * @ngdoc method |
|
29284 * @name ngModel.NgModelController#$commitViewValue |
|
29285 * |
|
29286 * @description |
|
29287 * Commit a pending update to the `$modelValue`. |
|
29288 * |
|
29289 * Updates may be pending by a debounced event or because the input is waiting for a some future |
|
29290 * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` |
|
29291 * usually handles calling this in response to input events. |
|
29292 */ |
|
29293 this.$commitViewValue = function() { |
|
29294 var viewValue = ctrl.$viewValue; |
|
29295 |
|
29296 $timeout.cancel(pendingDebounce); |
|
29297 |
|
29298 // If the view value has not changed then we should just exit, except in the case where there is |
|
29299 // a native validator on the element. In this case the validation state may have changed even though |
|
29300 // the viewValue has stayed empty. |
|
29301 if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { |
|
29302 return; |
|
29303 } |
|
29304 ctrl.$$lastCommittedViewValue = viewValue; |
|
29305 |
|
29306 // change to dirty |
|
29307 if (ctrl.$pristine) { |
|
29308 ctrl.$dirty = true; |
|
29309 ctrl.$pristine = false; |
|
29310 $animate.removeClass($element, PRISTINE_CLASS); |
|
29311 $animate.addClass($element, DIRTY_CLASS); |
|
29312 parentForm.$setDirty(); |
|
29313 } |
|
29314 this.$$parseAndValidate(); |
|
29315 }; |
|
29316 |
|
29317 this.$$parseAndValidate = function() { |
|
29318 var viewValue = ctrl.$$lastCommittedViewValue; |
|
29319 var modelValue = viewValue; |
|
29320 var parserValid = isUndefined(modelValue) ? undefined : true; |
|
29321 |
|
29322 if (parserValid) { |
|
29323 for(var i = 0; i < ctrl.$parsers.length; i++) { |
|
29324 modelValue = ctrl.$parsers[i](modelValue); |
|
29325 if (isUndefined(modelValue)) { |
|
29326 parserValid = false; |
|
29327 break; |
|
29328 } |
|
29329 } |
|
29330 } |
|
29331 if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { |
|
29332 // ctrl.$modelValue has not been touched yet... |
|
29333 ctrl.$modelValue = ngModelGet(); |
|
29334 } |
|
29335 var prevModelValue = ctrl.$modelValue; |
|
29336 var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; |
|
29337 if (allowInvalid) { |
|
29338 ctrl.$modelValue = modelValue; |
|
29339 writeToModelIfNeeded(); |
|
29340 } |
|
29341 ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) { |
|
29342 if (!allowInvalid) { |
|
29343 // Note: Don't check ctrl.$valid here, as we could have |
|
29344 // external validators (e.g. calculated on the server), |
|
29345 // that just call $setValidity and need the model value |
|
29346 // to calculate their validity. |
|
29347 ctrl.$modelValue = allValid ? modelValue : undefined; |
|
29348 writeToModelIfNeeded(); |
|
29349 } |
|
29350 }); |
|
29351 |
|
29352 function writeToModelIfNeeded() { |
|
29353 if (ctrl.$modelValue !== prevModelValue) { |
|
29354 ctrl.$$writeModelToScope(); |
|
29355 } |
|
29356 } |
|
29357 }; |
|
29358 |
|
29359 this.$$writeModelToScope = function() { |
|
29360 ngModelSet(ctrl.$modelValue); |
|
29361 forEach(ctrl.$viewChangeListeners, function(listener) { |
|
29362 try { |
|
29363 listener(); |
|
29364 } catch(e) { |
|
29365 $exceptionHandler(e); |
|
29366 } |
|
29367 }); |
|
29368 }; |
|
29369 |
|
29370 /** |
|
29371 * @ngdoc method |
|
29372 * @name ngModel.NgModelController#$setViewValue |
|
29373 * |
|
29374 * @description |
|
29375 * Update the view value. |
|
29376 * |
|
29377 * This method should be called when an input directive want to change the view value; typically, |
|
29378 * this is done from within a DOM event handler. |
|
29379 * |
|
29380 * For example {@link ng.directive:input input} calls it when the value of the input changes and |
|
29381 * {@link ng.directive:select select} calls it when an option is selected. |
|
29382 * |
|
29383 * If the new `value` is an object (rather than a string or a number), we should make a copy of the |
|
29384 * object before passing it to `$setViewValue`. This is because `ngModel` does not perform a deep |
|
29385 * watch of objects, it only looks for a change of identity. If you only change the property of |
|
29386 * the object then ngModel will not realise that the object has changed and will not invoke the |
|
29387 * `$parsers` and `$validators` pipelines. |
|
29388 * |
|
29389 * For this reason, you should not change properties of the copy once it has been passed to |
|
29390 * `$setViewValue`. Otherwise you may cause the model value on the scope to change incorrectly. |
|
29391 * |
|
29392 * When this method is called, the new `value` will be staged for committing through the `$parsers` |
|
29393 * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged |
|
29394 * value sent directly for processing, finally to be applied to `$modelValue` and then the |
|
29395 * **expression** specified in the `ng-model` attribute. |
|
29396 * |
|
29397 * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. |
|
29398 * |
|
29399 * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn` |
|
29400 * and the `default` trigger is not listed, all those actions will remain pending until one of the |
|
29401 * `updateOn` events is triggered on the DOM element. |
|
29402 * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions} |
|
29403 * directive is used with a custom debounce for this particular event. |
|
29404 * |
|
29405 * Note that calling this function does not trigger a `$digest`. |
|
29406 * |
|
29407 * @param {string} value Value from the view. |
|
29408 * @param {string} trigger Event that triggered the update. |
|
29409 */ |
|
29410 this.$setViewValue = function(value, trigger) { |
|
29411 ctrl.$viewValue = value; |
|
29412 if (!ctrl.$options || ctrl.$options.updateOnDefault) { |
|
29413 ctrl.$$debounceViewValueCommit(trigger); |
|
29414 } |
|
29415 }; |
|
29416 |
|
29417 this.$$debounceViewValueCommit = function(trigger) { |
|
29418 var debounceDelay = 0, |
|
29419 options = ctrl.$options, |
|
29420 debounce; |
|
29421 |
|
29422 if (options && isDefined(options.debounce)) { |
|
29423 debounce = options.debounce; |
|
29424 if (isNumber(debounce)) { |
|
29425 debounceDelay = debounce; |
|
29426 } else if (isNumber(debounce[trigger])) { |
|
29427 debounceDelay = debounce[trigger]; |
|
29428 } else if (isNumber(debounce['default'])) { |
|
29429 debounceDelay = debounce['default']; |
|
29430 } |
|
29431 } |
|
29432 |
|
29433 $timeout.cancel(pendingDebounce); |
|
29434 if (debounceDelay) { |
|
29435 pendingDebounce = $timeout(function() { |
|
29436 ctrl.$commitViewValue(); |
|
29437 }, debounceDelay); |
|
29438 } else if ($rootScope.$$phase) { |
|
29439 ctrl.$commitViewValue(); |
|
29440 } else { |
|
29441 $scope.$apply(function() { |
|
29442 ctrl.$commitViewValue(); |
|
29443 }); |
|
29444 } |
|
29445 }; |
|
29446 |
|
29447 // model -> value |
|
29448 // Note: we cannot use a normal scope.$watch as we want to detect the following: |
|
29449 // 1. scope value is 'a' |
|
29450 // 2. user enters 'b' |
|
29451 // 3. ng-change kicks in and reverts scope value to 'a' |
|
29452 // -> scope value did not change since the last digest as |
|
29453 // ng-change executes in apply phase |
|
29454 // 4. view should be changed back to 'a' |
|
29455 $scope.$watch(function ngModelWatch() { |
|
29456 var modelValue = ngModelGet(); |
|
29457 |
|
29458 // if scope model value and ngModel value are out of sync |
|
29459 // TODO(perf): why not move this to the action fn? |
|
29460 if (modelValue !== ctrl.$modelValue) { |
|
29461 ctrl.$modelValue = modelValue; |
|
29462 |
|
29463 var formatters = ctrl.$formatters, |
|
29464 idx = formatters.length; |
|
29465 |
|
29466 var viewValue = modelValue; |
|
29467 while(idx--) { |
|
29468 viewValue = formatters[idx](viewValue); |
|
29469 } |
|
29470 if (ctrl.$viewValue !== viewValue) { |
|
29471 ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; |
|
29472 ctrl.$render(); |
|
29473 |
|
29474 ctrl.$$runValidators(undefined, modelValue, viewValue, noop); |
|
29475 } |
|
29476 } |
|
29477 |
|
29478 return modelValue; |
|
29479 }); |
|
29480 }]; |
|
29481 |
|
29482 |
|
29483 /** |
|
29484 * @ngdoc directive |
|
29485 * @name ngModel |
|
29486 * |
|
29487 * @element input |
|
29488 * |
|
29489 * @description |
|
29490 * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a |
|
29491 * property on the scope using {@link ngModel.NgModelController NgModelController}, |
|
29492 * which is created and exposed by this directive. |
|
29493 * |
|
29494 * `ngModel` is responsible for: |
|
29495 * |
|
29496 * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` |
|
29497 * require. |
|
29498 * - Providing validation behavior (i.e. required, number, email, url). |
|
29499 * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors). |
|
29500 * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations. |
|
29501 * - Registering the control with its parent {@link ng.directive:form form}. |
|
29502 * |
|
29503 * Note: `ngModel` will try to bind to the property given by evaluating the expression on the |
|
29504 * current scope. If the property doesn't already exist on this scope, it will be created |
|
29505 * implicitly and added to the scope. |
|
29506 * |
|
29507 * For best practices on using `ngModel`, see: |
|
29508 * |
|
29509 * - [https://github.com/angular/angular.js/wiki/Understanding-Scopes] |
|
29510 * |
|
29511 * For basic examples, how to use `ngModel`, see: |
|
29512 * |
|
29513 * - {@link ng.directive:input input} |
|
29514 * - {@link input[text] text} |
|
29515 * - {@link input[checkbox] checkbox} |
|
29516 * - {@link input[radio] radio} |
|
29517 * - {@link input[number] number} |
|
29518 * - {@link input[email] email} |
|
29519 * - {@link input[url] url} |
|
29520 * - {@link input[date] date} |
|
29521 * - {@link input[dateTimeLocal] dateTimeLocal} |
|
29522 * - {@link input[time] time} |
|
29523 * - {@link input[month] month} |
|
29524 * - {@link input[week] week} |
|
29525 * - {@link ng.directive:select select} |
|
29526 * - {@link ng.directive:textarea textarea} |
|
29527 * |
|
29528 * # CSS classes |
|
29529 * The following CSS classes are added and removed on the associated input/select/textarea element |
|
29530 * depending on the validity of the model. |
|
29531 * |
|
29532 * - `ng-valid` is set if the model is valid. |
|
29533 * - `ng-invalid` is set if the model is invalid. |
|
29534 * - `ng-pristine` is set if the model is pristine. |
|
29535 * - `ng-dirty` is set if the model is dirty. |
|
29536 * |
|
29537 * Keep in mind that ngAnimate can detect each of these classes when added and removed. |
|
29538 * |
|
29539 * ## Animation Hooks |
|
29540 * |
|
29541 * Animations within models are triggered when any of the associated CSS classes are added and removed |
|
29542 * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`, |
|
29543 * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself. |
|
29544 * The animations that are triggered within ngModel are similar to how they work in ngClass and |
|
29545 * animations can be hooked into using CSS transitions, keyframes as well as JS animations. |
|
29546 * |
|
29547 * The following example shows a simple way to utilize CSS transitions to style an input element |
|
29548 * that has been rendered as invalid after it has been validated: |
|
29549 * |
|
29550 * <pre> |
|
29551 * //be sure to include ngAnimate as a module to hook into more |
|
29552 * //advanced animations |
|
29553 * .my-input { |
|
29554 * transition:0.5s linear all; |
|
29555 * background: white; |
|
29556 * } |
|
29557 * .my-input.ng-invalid { |
|
29558 * background: red; |
|
29559 * color:white; |
|
29560 * } |
|
29561 * </pre> |
|
29562 * |
|
29563 * @example |
|
29564 * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample"> |
|
29565 <file name="index.html"> |
|
29566 <script> |
|
29567 angular.module('inputExample', []) |
|
29568 .controller('ExampleController', ['$scope', function($scope) { |
|
29569 $scope.val = '1'; |
|
29570 }]); |
|
29571 </script> |
|
29572 <style> |
|
29573 .my-input { |
|
29574 -webkit-transition:all linear 0.5s; |
|
29575 transition:all linear 0.5s; |
|
29576 background: transparent; |
|
29577 } |
|
29578 .my-input.ng-invalid { |
|
29579 color:white; |
|
29580 background: red; |
|
29581 } |
|
29582 </style> |
|
29583 Update input to see transitions when valid/invalid. |
|
29584 Integer is a valid value. |
|
29585 <form name="testForm" ng-controller="ExampleController"> |
|
29586 <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" /> |
|
29587 </form> |
|
29588 </file> |
|
29589 * </example> |
|
29590 * |
|
29591 * ## Binding to a getter/setter |
|
29592 * |
|
29593 * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a |
|
29594 * function that returns a representation of the model when called with zero arguments, and sets |
|
29595 * the internal state of a model when called with an argument. It's sometimes useful to use this |
|
29596 * for models that have an internal representation that's different than what the model exposes |
|
29597 * to the view. |
|
29598 * |
|
29599 * <div class="alert alert-success"> |
|
29600 * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more |
|
29601 * frequently than other parts of your code. |
|
29602 * </div> |
|
29603 * |
|
29604 * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that |
|
29605 * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to |
|
29606 * a `<form>`, which will enable this behavior for all `<input>`s within it. See |
|
29607 * {@link ng.directive:ngModelOptions `ngModelOptions`} for more. |
|
29608 * |
|
29609 * The following example shows how to use `ngModel` with a getter/setter: |
|
29610 * |
|
29611 * @example |
|
29612 * <example name="ngModel-getter-setter" module="getterSetterExample"> |
|
29613 <file name="index.html"> |
|
29614 <div ng-controller="ExampleController"> |
|
29615 <form name="userForm"> |
|
29616 Name: |
|
29617 <input type="text" name="userName" |
|
29618 ng-model="user.name" |
|
29619 ng-model-options="{ getterSetter: true }" /> |
|
29620 </form> |
|
29621 <pre>user.name = <span ng-bind="user.name()"></span></pre> |
|
29622 </div> |
|
29623 </file> |
|
29624 <file name="app.js"> |
|
29625 angular.module('getterSetterExample', []) |
|
29626 .controller('ExampleController', ['$scope', function($scope) { |
|
29627 var _name = 'Brian'; |
|
29628 $scope.user = { |
|
29629 name: function (newName) { |
|
29630 if (angular.isDefined(newName)) { |
|
29631 _name = newName; |
|
29632 } |
|
29633 return _name; |
|
29634 } |
|
29635 }; |
|
29636 }]); |
|
29637 </file> |
|
29638 * </example> |
|
29639 */ |
|
29640 var ngModelDirective = function() { |
|
29641 return { |
|
29642 restrict: 'A', |
|
29643 require: ['ngModel', '^?form', '^?ngModelOptions'], |
|
29644 controller: NgModelController, |
|
29645 // Prelink needs to run before any input directive |
|
29646 // so that we can set the NgModelOptions in NgModelController |
|
29647 // before anyone else uses it. |
|
29648 priority: 1, |
|
29649 compile: function ngModelCompile(element) { |
|
29650 // Setup initial state of the control |
|
29651 element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS); |
|
29652 |
|
29653 return { |
|
29654 pre: function ngModelPreLink(scope, element, attr, ctrls) { |
|
29655 var modelCtrl = ctrls[0], |
|
29656 formCtrl = ctrls[1] || nullFormCtrl; |
|
29657 |
|
29658 modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); |
|
29659 |
|
29660 // notify others, especially parent forms |
|
29661 formCtrl.$addControl(modelCtrl); |
|
29662 |
|
29663 attr.$observe('name', function(newValue) { |
|
29664 if (modelCtrl.$name !== newValue) { |
|
29665 formCtrl.$$renameControl(modelCtrl, newValue); |
|
29666 } |
|
29667 }); |
|
29668 |
|
29669 scope.$on('$destroy', function() { |
|
29670 formCtrl.$removeControl(modelCtrl); |
|
29671 }); |
|
29672 }, |
|
29673 post: function ngModelPostLink(scope, element, attr, ctrls) { |
|
29674 var modelCtrl = ctrls[0]; |
|
29675 if (modelCtrl.$options && modelCtrl.$options.updateOn) { |
|
29676 element.on(modelCtrl.$options.updateOn, function(ev) { |
|
29677 modelCtrl.$$debounceViewValueCommit(ev && ev.type); |
|
29678 }); |
|
29679 } |
|
29680 |
|
29681 element.on('blur', function(ev) { |
|
29682 if (modelCtrl.$touched) return; |
|
29683 |
|
29684 scope.$apply(function() { |
|
29685 modelCtrl.$setTouched(); |
|
29686 }); |
|
29687 }); |
|
29688 } |
|
29689 }; |
|
29690 } |
|
29691 }; |
|
29692 }; |
|
29693 |
|
29694 |
|
29695 /** |
|
29696 * @ngdoc directive |
|
29697 * @name ngChange |
|
29698 * |
|
29699 * @description |
|
29700 * Evaluate the given expression when the user changes the input. |
|
29701 * The expression is evaluated immediately, unlike the JavaScript onchange event |
|
29702 * which only triggers at the end of a change (usually, when the user leaves the |
|
29703 * form element or presses the return key). |
|
29704 * |
|
29705 * The `ngChange` expression is only evaluated when a change in the input value causes |
|
29706 * a new value to be committed to the model. |
|
29707 * |
|
29708 * It will not be evaluated: |
|
29709 * * if the value returned from the `$parsers` transformation pipeline has not changed |
|
29710 * * if the input has continued to be invalid since the model will stay `null` |
|
29711 * * if the model is changed programmatically and not by a change to the input value |
|
29712 * |
|
29713 * |
|
29714 * Note, this directive requires `ngModel` to be present. |
|
29715 * |
|
29716 * @element input |
|
29717 * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change |
|
29718 * in input value. |
|
29719 * |
|
29720 * @example |
|
29721 * <example name="ngChange-directive" module="changeExample"> |
|
29722 * <file name="index.html"> |
|
29723 * <script> |
|
29724 * angular.module('changeExample', []) |
|
29725 * .controller('ExampleController', ['$scope', function($scope) { |
|
29726 * $scope.counter = 0; |
|
29727 * $scope.change = function() { |
|
29728 * $scope.counter++; |
|
29729 * }; |
|
29730 * }]); |
|
29731 * </script> |
|
29732 * <div ng-controller="ExampleController"> |
|
29733 * <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" /> |
|
29734 * <input type="checkbox" ng-model="confirmed" id="ng-change-example2" /> |
|
29735 * <label for="ng-change-example2">Confirmed</label><br /> |
|
29736 * <tt>debug = {{confirmed}}</tt><br/> |
|
29737 * <tt>counter = {{counter}}</tt><br/> |
|
29738 * </div> |
|
29739 * </file> |
|
29740 * <file name="protractor.js" type="protractor"> |
|
29741 * var counter = element(by.binding('counter')); |
|
29742 * var debug = element(by.binding('confirmed')); |
|
29743 * |
|
29744 * it('should evaluate the expression if changing from view', function() { |
|
29745 * expect(counter.getText()).toContain('0'); |
|
29746 * |
|
29747 * element(by.id('ng-change-example1')).click(); |
|
29748 * |
|
29749 * expect(counter.getText()).toContain('1'); |
|
29750 * expect(debug.getText()).toContain('true'); |
|
29751 * }); |
|
29752 * |
|
29753 * it('should not evaluate the expression if changing from model', function() { |
|
29754 * element(by.id('ng-change-example2')).click(); |
|
29755 |
|
29756 * expect(counter.getText()).toContain('0'); |
|
29757 * expect(debug.getText()).toContain('true'); |
|
29758 * }); |
|
29759 * </file> |
|
29760 * </example> |
|
29761 */ |
|
29762 var ngChangeDirective = valueFn({ |
|
29763 restrict: 'A', |
|
29764 require: 'ngModel', |
|
29765 link: function(scope, element, attr, ctrl) { |
|
29766 ctrl.$viewChangeListeners.push(function() { |
|
29767 scope.$eval(attr.ngChange); |
|
29768 }); |
|
29769 } |
|
29770 }); |
|
29771 |
|
29772 |
|
29773 var requiredDirective = function() { |
|
29774 return { |
|
29775 restrict: 'A', |
|
29776 require: '?ngModel', |
|
29777 link: function(scope, elm, attr, ctrl) { |
|
29778 if (!ctrl) return; |
|
29779 attr.required = true; // force truthy in case we are on non input element |
|
29780 |
|
29781 ctrl.$validators.required = function(value) { |
|
29782 return !attr.required || !ctrl.$isEmpty(value); |
|
29783 }; |
|
29784 |
|
29785 attr.$observe('required', function() { |
|
29786 ctrl.$validate(); |
|
29787 }); |
|
29788 } |
|
29789 }; |
|
29790 }; |
|
29791 |
|
29792 |
|
29793 var patternDirective = function() { |
|
29794 return { |
|
29795 restrict: 'A', |
|
29796 require: '?ngModel', |
|
29797 link: function(scope, elm, attr, ctrl) { |
|
29798 if (!ctrl) return; |
|
29799 |
|
29800 var regexp, patternExp = attr.ngPattern || attr.pattern; |
|
29801 attr.$observe('pattern', function(regex) { |
|
29802 if (isString(regex) && regex.length > 0) { |
|
29803 regex = new RegExp(regex); |
|
29804 } |
|
29805 |
|
29806 if (regex && !regex.test) { |
|
29807 throw minErr('ngPattern')('noregexp', |
|
29808 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp, |
|
29809 regex, startingTag(elm)); |
|
29810 } |
|
29811 |
|
29812 regexp = regex || undefined; |
|
29813 ctrl.$validate(); |
|
29814 }); |
|
29815 |
|
29816 ctrl.$validators.pattern = function(value) { |
|
29817 return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); |
|
29818 }; |
|
29819 } |
|
29820 }; |
|
29821 }; |
|
29822 |
|
29823 |
|
29824 var maxlengthDirective = function() { |
|
29825 return { |
|
29826 restrict: 'A', |
|
29827 require: '?ngModel', |
|
29828 link: function(scope, elm, attr, ctrl) { |
|
29829 if (!ctrl) return; |
|
29830 |
|
29831 var maxlength = 0; |
|
29832 attr.$observe('maxlength', function(value) { |
|
29833 maxlength = int(value) || 0; |
|
29834 ctrl.$validate(); |
|
29835 }); |
|
29836 ctrl.$validators.maxlength = function(modelValue, viewValue) { |
|
29837 return ctrl.$isEmpty(modelValue) || viewValue.length <= maxlength; |
|
29838 }; |
|
29839 } |
|
29840 }; |
|
29841 }; |
|
29842 |
|
29843 var minlengthDirective = function() { |
|
29844 return { |
|
29845 restrict: 'A', |
|
29846 require: '?ngModel', |
|
29847 link: function(scope, elm, attr, ctrl) { |
|
29848 if (!ctrl) return; |
|
29849 |
|
29850 var minlength = 0; |
|
29851 attr.$observe('minlength', function(value) { |
|
29852 minlength = int(value) || 0; |
|
29853 ctrl.$validate(); |
|
29854 }); |
|
29855 ctrl.$validators.minlength = function(modelValue, viewValue) { |
|
29856 return ctrl.$isEmpty(modelValue) || viewValue.length >= minlength; |
|
29857 }; |
|
29858 } |
|
29859 }; |
|
29860 }; |
|
29861 |
|
29862 |
|
29863 /** |
|
29864 * @ngdoc directive |
|
29865 * @name ngList |
|
29866 * |
|
29867 * @description |
|
29868 * Text input that converts between a delimited string and an array of strings. The default |
|
29869 * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom |
|
29870 * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`. |
|
29871 * |
|
29872 * The behaviour of the directive is affected by the use of the `ngTrim` attribute. |
|
29873 * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each |
|
29874 * list item is respected. This implies that the user of the directive is responsible for |
|
29875 * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a |
|
29876 * tab or newline character. |
|
29877 * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected |
|
29878 * when joining the list items back together) and whitespace around each list item is stripped |
|
29879 * before it is added to the model. |
|
29880 * |
|
29881 * ### Example with Validation |
|
29882 * |
|
29883 * <example name="ngList-directive" module="listExample"> |
|
29884 * <file name="app.js"> |
|
29885 * angular.module('listExample', []) |
|
29886 * .controller('ExampleController', ['$scope', function($scope) { |
|
29887 * $scope.names = ['morpheus', 'neo', 'trinity']; |
|
29888 * }]); |
|
29889 * </file> |
|
29890 * <file name="index.html"> |
|
29891 * <form name="myForm" ng-controller="ExampleController"> |
|
29892 * List: <input name="namesInput" ng-model="names" ng-list required> |
|
29893 * <span class="error" ng-show="myForm.namesInput.$error.required"> |
|
29894 * Required!</span> |
|
29895 * <br> |
|
29896 * <tt>names = {{names}}</tt><br/> |
|
29897 * <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> |
|
29898 * <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/> |
|
29899 * <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> |
|
29900 * <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> |
|
29901 * </form> |
|
29902 * </file> |
|
29903 * <file name="protractor.js" type="protractor"> |
|
29904 * var listInput = element(by.model('names')); |
|
29905 * var names = element(by.exactBinding('names')); |
|
29906 * var valid = element(by.binding('myForm.namesInput.$valid')); |
|
29907 * var error = element(by.css('span.error')); |
|
29908 * |
|
29909 * it('should initialize to model', function() { |
|
29910 * expect(names.getText()).toContain('["morpheus","neo","trinity"]'); |
|
29911 * expect(valid.getText()).toContain('true'); |
|
29912 * expect(error.getCssValue('display')).toBe('none'); |
|
29913 * }); |
|
29914 * |
|
29915 * it('should be invalid if empty', function() { |
|
29916 * listInput.clear(); |
|
29917 * listInput.sendKeys(''); |
|
29918 * |
|
29919 * expect(names.getText()).toContain(''); |
|
29920 * expect(valid.getText()).toContain('false'); |
|
29921 * expect(error.getCssValue('display')).not.toBe('none'); |
|
29922 * }); |
|
29923 * </file> |
|
29924 * </example> |
|
29925 * |
|
29926 * ### Example - splitting on whitespace |
|
29927 * <example name="ngList-directive-newlines"> |
|
29928 * <file name="index.html"> |
|
29929 * <textarea ng-model="list" ng-list=" " ng-trim="false"></textarea> |
|
29930 * <pre>{{ list | json }}</pre> |
|
29931 * </file> |
|
29932 * <file name="protractor.js" type="protractor"> |
|
29933 * it("should split the text by newlines", function() { |
|
29934 * var listInput = element(by.model('list')); |
|
29935 * var output = element(by.binding('list | json')); |
|
29936 * listInput.sendKeys('abc\ndef\nghi'); |
|
29937 * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]'); |
|
29938 * }); |
|
29939 * </file> |
|
29940 * </example> |
|
29941 * |
|
29942 * @element input |
|
29943 * @param {string=} ngList optional delimiter that should be used to split the value. |
|
29944 */ |
|
29945 var ngListDirective = function() { |
|
29946 return { |
|
29947 restrict: 'A', |
|
29948 priority: 100, |
|
29949 require: 'ngModel', |
|
29950 link: function(scope, element, attr, ctrl) { |
|
29951 // We want to control whitespace trimming so we use this convoluted approach |
|
29952 // to access the ngList attribute, which doesn't pre-trim the attribute |
|
29953 var ngList = element.attr(attr.$attr.ngList) || ', '; |
|
29954 var trimValues = attr.ngTrim !== 'false'; |
|
29955 var separator = trimValues ? trim(ngList) : ngList; |
|
29956 |
|
29957 var parse = function(viewValue) { |
|
29958 // If the viewValue is invalid (say required but empty) it will be `undefined` |
|
29959 if (isUndefined(viewValue)) return; |
|
29960 |
|
29961 var list = []; |
|
29962 |
|
29963 if (viewValue) { |
|
29964 forEach(viewValue.split(separator), function(value) { |
|
29965 if (value) list.push(trimValues ? trim(value) : value); |
|
29966 }); |
|
29967 } |
|
29968 |
|
29969 return list; |
|
29970 }; |
|
29971 |
|
29972 ctrl.$parsers.push(parse); |
|
29973 ctrl.$formatters.push(function(value) { |
|
29974 if (isArray(value)) { |
|
29975 return value.join(ngList); |
|
29976 } |
|
29977 |
|
29978 return undefined; |
|
29979 }); |
|
29980 |
|
29981 // Override the standard $isEmpty because an empty array means the input is empty. |
|
29982 ctrl.$isEmpty = function(value) { |
|
29983 return !value || !value.length; |
|
29984 }; |
|
29985 } |
|
29986 }; |
|
29987 }; |
|
29988 |
|
29989 |
|
29990 var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; |
|
29991 /** |
|
29992 * @ngdoc directive |
|
29993 * @name ngValue |
|
29994 * |
|
29995 * @description |
|
29996 * Binds the given expression to the value of `input[select]` or `input[radio]`, so |
|
29997 * that when the element is selected, the `ngModel` of that element is set to the |
|
29998 * bound value. |
|
29999 * |
|
30000 * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as |
|
30001 * shown below. |
|
30002 * |
|
30003 * @element input |
|
30004 * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute |
|
30005 * of the `input` element |
|
30006 * |
|
30007 * @example |
|
30008 <example name="ngValue-directive" module="valueExample"> |
|
30009 <file name="index.html"> |
|
30010 <script> |
|
30011 angular.module('valueExample', []) |
|
30012 .controller('ExampleController', ['$scope', function($scope) { |
|
30013 $scope.names = ['pizza', 'unicorns', 'robots']; |
|
30014 $scope.my = { favorite: 'unicorns' }; |
|
30015 }]); |
|
30016 </script> |
|
30017 <form ng-controller="ExampleController"> |
|
30018 <h2>Which is your favorite?</h2> |
|
30019 <label ng-repeat="name in names" for="{{name}}"> |
|
30020 {{name}} |
|
30021 <input type="radio" |
|
30022 ng-model="my.favorite" |
|
30023 ng-value="name" |
|
30024 id="{{name}}" |
|
30025 name="favorite"> |
|
30026 </label> |
|
30027 <div>You chose {{my.favorite}}</div> |
|
30028 </form> |
|
30029 </file> |
|
30030 <file name="protractor.js" type="protractor"> |
|
30031 var favorite = element(by.binding('my.favorite')); |
|
30032 |
|
30033 it('should initialize to model', function() { |
|
30034 expect(favorite.getText()).toContain('unicorns'); |
|
30035 }); |
|
30036 it('should bind the values to the inputs', function() { |
|
30037 element.all(by.model('my.favorite')).get(0).click(); |
|
30038 expect(favorite.getText()).toContain('pizza'); |
|
30039 }); |
|
30040 </file> |
|
30041 </example> |
|
30042 */ |
|
30043 var ngValueDirective = function() { |
|
30044 return { |
|
30045 restrict: 'A', |
|
30046 priority: 100, |
|
30047 compile: function(tpl, tplAttr) { |
|
30048 if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { |
|
30049 return function ngValueConstantLink(scope, elm, attr) { |
|
30050 attr.$set('value', scope.$eval(attr.ngValue)); |
|
30051 }; |
|
30052 } else { |
|
30053 return function ngValueLink(scope, elm, attr) { |
|
30054 scope.$watch(attr.ngValue, function valueWatchAction(value) { |
|
30055 attr.$set('value', value); |
|
30056 }); |
|
30057 }; |
|
30058 } |
|
30059 } |
|
30060 }; |
|
30061 }; |
|
30062 |
|
30063 /** |
|
30064 * @ngdoc directive |
|
30065 * @name ngModelOptions |
|
30066 * |
|
30067 * @description |
|
30068 * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of |
|
30069 * events that will trigger a model update and/or a debouncing delay so that the actual update only |
|
30070 * takes place when a timer expires; this timer will be reset after another change takes place. |
|
30071 * |
|
30072 * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might |
|
30073 * be different than the value in the actual model. This means that if you update the model you |
|
30074 * should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in |
|
30075 * order to make sure it is synchronized with the model and that any debounced action is canceled. |
|
30076 * |
|
30077 * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`} |
|
30078 * method is by making sure the input is placed inside a form that has a `name` attribute. This is |
|
30079 * important because `form` controllers are published to the related scope under the name in their |
|
30080 * `name` attribute. |
|
30081 * |
|
30082 * Any pending changes will take place immediately when an enclosing form is submitted via the |
|
30083 * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` |
|
30084 * to have access to the updated model. |
|
30085 * |
|
30086 * `ngModelOptions` has an effect on the element it's declared on and its descendants. |
|
30087 * |
|
30088 * @param {Object} ngModelOptions options to apply to the current model. Valid keys are: |
|
30089 * - `updateOn`: string specifying which event should be the input bound to. You can set several |
|
30090 * events using an space delimited list. There is a special event called `default` that |
|
30091 * matches the default events belonging of the control. |
|
30092 * - `debounce`: integer value which contains the debounce model update value in milliseconds. A |
|
30093 * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a |
|
30094 * custom value for each event. For example: |
|
30095 * `ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"` |
|
30096 * - `allowInvalid`: boolean value which indicates that the model can be set with values that did |
|
30097 * not validate correctly instead of the default behavior of setting the model to undefined. |
|
30098 * - `getterSetter`: boolean value which determines whether or not to treat functions bound to |
|
30099 `ngModel` as getters/setters. |
|
30100 * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for |
|
30101 * `<input type="date">`, `<input type="time">`, ... . Right now, the only supported value is `'UTC'`, |
|
30102 * otherwise the default timezone of the browser will be used. |
|
30103 * |
|
30104 * @example |
|
30105 |
|
30106 The following example shows how to override immediate updates. Changes on the inputs within the |
|
30107 form will update the model only when the control loses focus (blur event). If `escape` key is |
|
30108 pressed while the input field is focused, the value is reset to the value in the current model. |
|
30109 |
|
30110 <example name="ngModelOptions-directive-blur" module="optionsExample"> |
|
30111 <file name="index.html"> |
|
30112 <div ng-controller="ExampleController"> |
|
30113 <form name="userForm"> |
|
30114 Name: |
|
30115 <input type="text" name="userName" |
|
30116 ng-model="user.name" |
|
30117 ng-model-options="{ updateOn: 'blur' }" |
|
30118 ng-keyup="cancel($event)" /><br /> |
|
30119 |
|
30120 Other data: |
|
30121 <input type="text" ng-model="user.data" /><br /> |
|
30122 </form> |
|
30123 <pre>user.name = <span ng-bind="user.name"></span></pre> |
|
30124 </div> |
|
30125 </file> |
|
30126 <file name="app.js"> |
|
30127 angular.module('optionsExample', []) |
|
30128 .controller('ExampleController', ['$scope', function($scope) { |
|
30129 $scope.user = { name: 'say', data: '' }; |
|
30130 |
|
30131 $scope.cancel = function (e) { |
|
30132 if (e.keyCode == 27) { |
|
30133 $scope.userForm.userName.$rollbackViewValue(); |
|
30134 } |
|
30135 }; |
|
30136 }]); |
|
30137 </file> |
|
30138 <file name="protractor.js" type="protractor"> |
|
30139 var model = element(by.binding('user.name')); |
|
30140 var input = element(by.model('user.name')); |
|
30141 var other = element(by.model('user.data')); |
|
30142 |
|
30143 it('should allow custom events', function() { |
|
30144 input.sendKeys(' hello'); |
|
30145 input.click(); |
|
30146 expect(model.getText()).toEqual('say'); |
|
30147 other.click(); |
|
30148 expect(model.getText()).toEqual('say hello'); |
|
30149 }); |
|
30150 |
|
30151 it('should $rollbackViewValue when model changes', function() { |
|
30152 input.sendKeys(' hello'); |
|
30153 expect(input.getAttribute('value')).toEqual('say hello'); |
|
30154 input.sendKeys(protractor.Key.ESCAPE); |
|
30155 expect(input.getAttribute('value')).toEqual('say'); |
|
30156 other.click(); |
|
30157 expect(model.getText()).toEqual('say'); |
|
30158 }); |
|
30159 </file> |
|
30160 </example> |
|
30161 |
|
30162 This one shows how to debounce model changes. Model will be updated only 1 sec after last change. |
|
30163 If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. |
|
30164 |
|
30165 <example name="ngModelOptions-directive-debounce" module="optionsExample"> |
|
30166 <file name="index.html"> |
|
30167 <div ng-controller="ExampleController"> |
|
30168 <form name="userForm"> |
|
30169 Name: |
|
30170 <input type="text" name="userName" |
|
30171 ng-model="user.name" |
|
30172 ng-model-options="{ debounce: 1000 }" /> |
|
30173 <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br /> |
|
30174 </form> |
|
30175 <pre>user.name = <span ng-bind="user.name"></span></pre> |
|
30176 </div> |
|
30177 </file> |
|
30178 <file name="app.js"> |
|
30179 angular.module('optionsExample', []) |
|
30180 .controller('ExampleController', ['$scope', function($scope) { |
|
30181 $scope.user = { name: 'say' }; |
|
30182 }]); |
|
30183 </file> |
|
30184 </example> |
|
30185 |
|
30186 This one shows how to bind to getter/setters: |
|
30187 |
|
30188 <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample"> |
|
30189 <file name="index.html"> |
|
30190 <div ng-controller="ExampleController"> |
|
30191 <form name="userForm"> |
|
30192 Name: |
|
30193 <input type="text" name="userName" |
|
30194 ng-model="user.name" |
|
30195 ng-model-options="{ getterSetter: true }" /> |
|
30196 </form> |
|
30197 <pre>user.name = <span ng-bind="user.name()"></span></pre> |
|
30198 </div> |
|
30199 </file> |
|
30200 <file name="app.js"> |
|
30201 angular.module('getterSetterExample', []) |
|
30202 .controller('ExampleController', ['$scope', function($scope) { |
|
30203 var _name = 'Brian'; |
|
30204 $scope.user = { |
|
30205 name: function (newName) { |
|
30206 return angular.isDefined(newName) ? (_name = newName) : _name; |
|
30207 } |
|
30208 }; |
|
30209 }]); |
|
30210 </file> |
|
30211 </example> |
|
30212 */ |
|
30213 var ngModelOptionsDirective = function() { |
|
30214 return { |
|
30215 restrict: 'A', |
|
30216 controller: ['$scope', '$attrs', function($scope, $attrs) { |
|
30217 var that = this; |
|
30218 this.$options = $scope.$eval($attrs.ngModelOptions); |
|
30219 // Allow adding/overriding bound events |
|
30220 if (this.$options.updateOn !== undefined) { |
|
30221 this.$options.updateOnDefault = false; |
|
30222 // extract "default" pseudo-event from list of events that can trigger a model update |
|
30223 this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() { |
|
30224 that.$options.updateOnDefault = true; |
|
30225 return ' '; |
|
30226 })); |
|
30227 } else { |
|
30228 this.$options.updateOnDefault = true; |
|
30229 } |
|
30230 }] |
|
30231 }; |
|
30232 }; |
|
30233 |
|
30234 // helper methods |
|
30235 function addSetValidityMethod(context) { |
|
30236 var ctrl = context.ctrl, |
|
30237 $element = context.$element, |
|
30238 classCache = {}, |
|
30239 set = context.set, |
|
30240 unset = context.unset, |
|
30241 parentForm = context.parentForm, |
|
30242 $animate = context.$animate; |
|
30243 |
|
30244 classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS)); |
|
30245 |
|
30246 ctrl.$setValidity = setValidity; |
|
30247 |
|
30248 function setValidity(validationErrorKey, state, options) { |
|
30249 if (state === undefined) { |
|
30250 createAndSet('$pending', validationErrorKey, options); |
|
30251 } else { |
|
30252 unsetAndCleanup('$pending', validationErrorKey, options); |
|
30253 } |
|
30254 if (!isBoolean(state)) { |
|
30255 unset(ctrl.$error, validationErrorKey, options); |
|
30256 unset(ctrl.$$success, validationErrorKey, options); |
|
30257 } else { |
|
30258 if (state) { |
|
30259 unset(ctrl.$error, validationErrorKey, options); |
|
30260 set(ctrl.$$success, validationErrorKey, options); |
|
30261 } else { |
|
30262 set(ctrl.$error, validationErrorKey, options); |
|
30263 unset(ctrl.$$success, validationErrorKey, options); |
|
30264 } |
|
30265 } |
|
30266 if (ctrl.$pending) { |
|
30267 cachedToggleClass(PENDING_CLASS, true); |
|
30268 ctrl.$valid = ctrl.$invalid = undefined; |
|
30269 toggleValidationCss('', null); |
|
30270 } else { |
|
30271 cachedToggleClass(PENDING_CLASS, false); |
|
30272 ctrl.$valid = isObjectEmpty(ctrl.$error); |
|
30273 ctrl.$invalid = !ctrl.$valid; |
|
30274 toggleValidationCss('', ctrl.$valid); |
|
30275 } |
|
30276 |
|
30277 // re-read the state as the set/unset methods could have |
|
30278 // combined state in ctrl.$error[validationError] (used for forms), |
|
30279 // where setting/unsetting only increments/decrements the value, |
|
30280 // and does not replace it. |
|
30281 var combinedState; |
|
30282 if (ctrl.$pending && ctrl.$pending[validationErrorKey]) { |
|
30283 combinedState = undefined; |
|
30284 } else if (ctrl.$error[validationErrorKey]) { |
|
30285 combinedState = false; |
|
30286 } else if (ctrl.$$success[validationErrorKey]) { |
|
30287 combinedState = true; |
|
30288 } else { |
|
30289 combinedState = null; |
|
30290 } |
|
30291 toggleValidationCss(validationErrorKey, combinedState); |
|
30292 parentForm.$setValidity(validationErrorKey, combinedState, ctrl); |
|
30293 } |
|
30294 |
|
30295 function createAndSet(name, value, options) { |
|
30296 if (!ctrl[name]) { |
|
30297 ctrl[name] = {}; |
|
30298 } |
|
30299 set(ctrl[name], value, options); |
|
30300 } |
|
30301 |
|
30302 function unsetAndCleanup(name, value, options) { |
|
30303 if (ctrl[name]) { |
|
30304 unset(ctrl[name], value, options); |
|
30305 } |
|
30306 if (isObjectEmpty(ctrl[name])) { |
|
30307 ctrl[name] = undefined; |
|
30308 } |
|
30309 } |
|
30310 |
|
30311 function cachedToggleClass(className, switchValue) { |
|
30312 if (switchValue && !classCache[className]) { |
|
30313 $animate.addClass($element, className); |
|
30314 classCache[className] = true; |
|
30315 } else if (!switchValue && classCache[className]) { |
|
30316 $animate.removeClass($element, className); |
|
30317 classCache[className] = false; |
|
30318 } |
|
30319 } |
|
30320 |
|
30321 function toggleValidationCss(validationErrorKey, isValid) { |
|
30322 validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; |
|
30323 |
|
30324 cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true); |
|
30325 cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false); |
|
30326 } |
|
30327 } |
|
30328 |
|
30329 function isObjectEmpty(obj) { |
|
30330 if (obj) { |
|
30331 for (var prop in obj) { |
|
30332 return false; |
|
30333 } |
|
30334 } |
|
30335 return true; |
|
30336 } |
|
30337 |
|
30338 /** |
|
30339 * @ngdoc directive |
|
30340 * @name ngBind |
|
30341 * @restrict AC |
|
30342 * |
|
30343 * @description |
|
30344 * The `ngBind` attribute tells Angular to replace the text content of the specified HTML element |
|
30345 * with the value of a given expression, and to update the text content when the value of that |
|
30346 * expression changes. |
|
30347 * |
|
30348 * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like |
|
30349 * `{{ expression }}` which is similar but less verbose. |
|
30350 * |
|
30351 * It is preferable to use `ngBind` instead of `{{ expression }}` if a template is momentarily |
|
30352 * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an |
|
30353 * element attribute, it makes the bindings invisible to the user while the page is loading. |
|
30354 * |
|
30355 * An alternative solution to this problem would be using the |
|
30356 * {@link ng.directive:ngCloak ngCloak} directive. |
|
30357 * |
|
30358 * |
|
30359 * @element ANY |
|
30360 * @param {expression} ngBind {@link guide/expression Expression} to evaluate. |
|
30361 * |
|
30362 * @example |
|
30363 * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. |
|
30364 <example module="bindExample"> |
|
30365 <file name="index.html"> |
|
30366 <script> |
|
30367 angular.module('bindExample', []) |
|
30368 .controller('ExampleController', ['$scope', function($scope) { |
|
30369 $scope.name = 'Whirled'; |
|
30370 }]); |
|
30371 </script> |
|
30372 <div ng-controller="ExampleController"> |
|
30373 Enter name: <input type="text" ng-model="name"><br> |
|
30374 Hello <span ng-bind="name"></span>! |
|
30375 </div> |
|
30376 </file> |
|
30377 <file name="protractor.js" type="protractor"> |
|
30378 it('should check ng-bind', function() { |
|
30379 var nameInput = element(by.model('name')); |
|
30380 |
|
30381 expect(element(by.binding('name')).getText()).toBe('Whirled'); |
|
30382 nameInput.clear(); |
|
30383 nameInput.sendKeys('world'); |
|
30384 expect(element(by.binding('name')).getText()).toBe('world'); |
|
30385 }); |
|
30386 </file> |
|
30387 </example> |
|
30388 */ |
|
30389 var ngBindDirective = ['$compile', function($compile) { |
|
30390 return { |
|
30391 restrict: 'AC', |
|
30392 compile: function ngBindCompile(templateElement) { |
|
30393 $compile.$$addBindingClass(templateElement); |
|
30394 return function ngBindLink(scope, element, attr) { |
|
30395 $compile.$$addBindingInfo(element, attr.ngBind); |
|
30396 element = element[0]; |
|
30397 scope.$watch(attr.ngBind, function ngBindWatchAction(value) { |
|
30398 element.textContent = value === undefined ? '' : value; |
|
30399 }); |
|
30400 }; |
|
30401 } |
|
30402 }; |
|
30403 }]; |
|
30404 |
|
30405 |
|
30406 /** |
|
30407 * @ngdoc directive |
|
30408 * @name ngBindTemplate |
|
30409 * |
|
30410 * @description |
|
30411 * The `ngBindTemplate` directive specifies that the element |
|
30412 * text content should be replaced with the interpolation of the template |
|
30413 * in the `ngBindTemplate` attribute. |
|
30414 * Unlike `ngBind`, the `ngBindTemplate` can contain multiple `{{` `}}` |
|
30415 * expressions. This directive is needed since some HTML elements |
|
30416 * (such as TITLE and OPTION) cannot contain SPAN elements. |
|
30417 * |
|
30418 * @element ANY |
|
30419 * @param {string} ngBindTemplate template of form |
|
30420 * <tt>{{</tt> <tt>expression</tt> <tt>}}</tt> to eval. |
|
30421 * |
|
30422 * @example |
|
30423 * Try it here: enter text in text box and watch the greeting change. |
|
30424 <example module="bindExample"> |
|
30425 <file name="index.html"> |
|
30426 <script> |
|
30427 angular.module('bindExample', []) |
|
30428 .controller('ExampleController', ['$scope', function ($scope) { |
|
30429 $scope.salutation = 'Hello'; |
|
30430 $scope.name = 'World'; |
|
30431 }]); |
|
30432 </script> |
|
30433 <div ng-controller="ExampleController"> |
|
30434 Salutation: <input type="text" ng-model="salutation"><br> |
|
30435 Name: <input type="text" ng-model="name"><br> |
|
30436 <pre ng-bind-template="{{salutation}} {{name}}!"></pre> |
|
30437 </div> |
|
30438 </file> |
|
30439 <file name="protractor.js" type="protractor"> |
|
30440 it('should check ng-bind', function() { |
|
30441 var salutationElem = element(by.binding('salutation')); |
|
30442 var salutationInput = element(by.model('salutation')); |
|
30443 var nameInput = element(by.model('name')); |
|
30444 |
|
30445 expect(salutationElem.getText()).toBe('Hello World!'); |
|
30446 |
|
30447 salutationInput.clear(); |
|
30448 salutationInput.sendKeys('Greetings'); |
|
30449 nameInput.clear(); |
|
30450 nameInput.sendKeys('user'); |
|
30451 |
|
30452 expect(salutationElem.getText()).toBe('Greetings user!'); |
|
30453 }); |
|
30454 </file> |
|
30455 </example> |
|
30456 */ |
|
30457 var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) { |
|
30458 return { |
|
30459 compile: function ngBindTemplateCompile(templateElement) { |
|
30460 $compile.$$addBindingClass(templateElement); |
|
30461 return function ngBindTemplateLink(scope, element, attr) { |
|
30462 var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); |
|
30463 $compile.$$addBindingInfo(element, interpolateFn.expressions); |
|
30464 element = element[0]; |
|
30465 attr.$observe('ngBindTemplate', function(value) { |
|
30466 element.textContent = value === undefined ? '' : value; |
|
30467 }); |
|
30468 }; |
|
30469 } |
|
30470 }; |
|
30471 }]; |
|
30472 |
|
30473 |
|
30474 /** |
|
30475 * @ngdoc directive |
|
30476 * @name ngBindHtml |
|
30477 * |
|
30478 * @description |
|
30479 * Creates a binding that will innerHTML the result of evaluating the `expression` into the current |
|
30480 * element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link |
|
30481 * ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize` |
|
30482 * is available, for example, by including {@link ngSanitize} in your module's dependencies (not in |
|
30483 * core Angular). In order to use {@link ngSanitize} in your module's dependencies, you need to |
|
30484 * include "angular-sanitize.js" in your application. |
|
30485 * |
|
30486 * You may also bypass sanitization for values you know are safe. To do so, bind to |
|
30487 * an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example |
|
30488 * under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}. |
|
30489 * |
|
30490 * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you |
|
30491 * will have an exception (instead of an exploit.) |
|
30492 * |
|
30493 * @element ANY |
|
30494 * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. |
|
30495 * |
|
30496 * @example |
|
30497 |
|
30498 <example module="bindHtmlExample" deps="angular-sanitize.js"> |
|
30499 <file name="index.html"> |
|
30500 <div ng-controller="ExampleController"> |
|
30501 <p ng-bind-html="myHTML"></p> |
|
30502 </div> |
|
30503 </file> |
|
30504 |
|
30505 <file name="script.js"> |
|
30506 angular.module('bindHtmlExample', ['ngSanitize']) |
|
30507 .controller('ExampleController', ['$scope', function($scope) { |
|
30508 $scope.myHTML = |
|
30509 'I am an <code>HTML</code>string with ' + |
|
30510 '<a href="#">links!</a> and other <em>stuff</em>'; |
|
30511 }]); |
|
30512 </file> |
|
30513 |
|
30514 <file name="protractor.js" type="protractor"> |
|
30515 it('should check ng-bind-html', function() { |
|
30516 expect(element(by.binding('myHTML')).getText()).toBe( |
|
30517 'I am an HTMLstring with links! and other stuff'); |
|
30518 }); |
|
30519 </file> |
|
30520 </example> |
|
30521 */ |
|
30522 var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) { |
|
30523 return { |
|
30524 restrict: 'A', |
|
30525 compile: function ngBindHtmlCompile(tElement, tAttrs) { |
|
30526 var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml); |
|
30527 var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) { |
|
30528 return (value || '').toString(); |
|
30529 }); |
|
30530 $compile.$$addBindingClass(tElement); |
|
30531 |
|
30532 return function ngBindHtmlLink(scope, element, attr) { |
|
30533 $compile.$$addBindingInfo(element, attr.ngBindHtml); |
|
30534 |
|
30535 scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() { |
|
30536 // we re-evaluate the expr because we want a TrustedValueHolderType |
|
30537 // for $sce, not a string |
|
30538 element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || ''); |
|
30539 }); |
|
30540 }; |
|
30541 } |
|
30542 }; |
|
30543 }]; |
|
30544 |
|
30545 function classDirective(name, selector) { |
|
30546 name = 'ngClass' + name; |
|
30547 return ['$animate', function($animate) { |
|
30548 return { |
|
30549 restrict: 'AC', |
|
30550 link: function(scope, element, attr) { |
|
30551 var oldVal; |
|
30552 |
|
30553 scope.$watch(attr[name], ngClassWatchAction, true); |
|
30554 |
|
30555 attr.$observe('class', function(value) { |
|
30556 ngClassWatchAction(scope.$eval(attr[name])); |
|
30557 }); |
|
30558 |
|
30559 |
|
30560 if (name !== 'ngClass') { |
|
30561 scope.$watch('$index', function($index, old$index) { |
|
30562 // jshint bitwise: false |
|
30563 var mod = $index & 1; |
|
30564 if (mod !== (old$index & 1)) { |
|
30565 var classes = arrayClasses(scope.$eval(attr[name])); |
|
30566 mod === selector ? |
|
30567 addClasses(classes) : |
|
30568 removeClasses(classes); |
|
30569 } |
|
30570 }); |
|
30571 } |
|
30572 |
|
30573 function addClasses(classes) { |
|
30574 var newClasses = digestClassCounts(classes, 1); |
|
30575 attr.$addClass(newClasses); |
|
30576 } |
|
30577 |
|
30578 function removeClasses(classes) { |
|
30579 var newClasses = digestClassCounts(classes, -1); |
|
30580 attr.$removeClass(newClasses); |
|
30581 } |
|
30582 |
|
30583 function digestClassCounts (classes, count) { |
|
30584 var classCounts = element.data('$classCounts') || {}; |
|
30585 var classesToUpdate = []; |
|
30586 forEach(classes, function (className) { |
|
30587 if (count > 0 || classCounts[className]) { |
|
30588 classCounts[className] = (classCounts[className] || 0) + count; |
|
30589 if (classCounts[className] === +(count > 0)) { |
|
30590 classesToUpdate.push(className); |
|
30591 } |
|
30592 } |
|
30593 }); |
|
30594 element.data('$classCounts', classCounts); |
|
30595 return classesToUpdate.join(' '); |
|
30596 } |
|
30597 |
|
30598 function updateClasses (oldClasses, newClasses) { |
|
30599 var toAdd = arrayDifference(newClasses, oldClasses); |
|
30600 var toRemove = arrayDifference(oldClasses, newClasses); |
|
30601 toAdd = digestClassCounts(toAdd, 1); |
|
30602 toRemove = digestClassCounts(toRemove, -1); |
|
30603 if (toAdd && toAdd.length) { |
|
30604 $animate.addClass(element, toAdd); |
|
30605 } |
|
30606 if (toRemove && toRemove.length) { |
|
30607 $animate.removeClass(element, toRemove); |
|
30608 } |
|
30609 } |
|
30610 |
|
30611 function ngClassWatchAction(newVal) { |
|
30612 if (selector === true || scope.$index % 2 === selector) { |
|
30613 var newClasses = arrayClasses(newVal || []); |
|
30614 if (!oldVal) { |
|
30615 addClasses(newClasses); |
|
30616 } else if (!equals(newVal,oldVal)) { |
|
30617 var oldClasses = arrayClasses(oldVal); |
|
30618 updateClasses(oldClasses, newClasses); |
|
30619 } |
|
30620 } |
|
30621 oldVal = shallowCopy(newVal); |
|
30622 } |
|
30623 } |
|
30624 }; |
|
30625 |
|
30626 function arrayDifference(tokens1, tokens2) { |
|
30627 var values = []; |
|
30628 |
|
30629 outer: |
|
30630 for(var i = 0; i < tokens1.length; i++) { |
|
30631 var token = tokens1[i]; |
|
30632 for(var j = 0; j < tokens2.length; j++) { |
|
30633 if(token == tokens2[j]) continue outer; |
|
30634 } |
|
30635 values.push(token); |
|
30636 } |
|
30637 return values; |
|
30638 } |
|
30639 |
|
30640 function arrayClasses (classVal) { |
|
30641 if (isArray(classVal)) { |
|
30642 return classVal; |
|
30643 } else if (isString(classVal)) { |
|
30644 return classVal.split(' '); |
|
30645 } else if (isObject(classVal)) { |
|
30646 var classes = [], i = 0; |
|
30647 forEach(classVal, function(v, k) { |
|
30648 if (v) { |
|
30649 classes = classes.concat(k.split(' ')); |
|
30650 } |
|
30651 }); |
|
30652 return classes; |
|
30653 } |
|
30654 return classVal; |
|
30655 } |
|
30656 }]; |
|
30657 } |
|
30658 |
|
30659 /** |
|
30660 * @ngdoc directive |
|
30661 * @name ngClass |
|
30662 * @restrict AC |
|
30663 * |
|
30664 * @description |
|
30665 * The `ngClass` directive allows you to dynamically set CSS classes on an HTML element by databinding |
|
30666 * an expression that represents all classes to be added. |
|
30667 * |
|
30668 * The directive operates in three different ways, depending on which of three types the expression |
|
30669 * evaluates to: |
|
30670 * |
|
30671 * 1. If the expression evaluates to a string, the string should be one or more space-delimited class |
|
30672 * names. |
|
30673 * |
|
30674 * 2. If the expression evaluates to an array, each element of the array should be a string that is |
|
30675 * one or more space-delimited class names. |
|
30676 * |
|
30677 * 3. If the expression evaluates to an object, then for each key-value pair of the |
|
30678 * object with a truthy value the corresponding key is used as a class name. |
|
30679 * |
|
30680 * The directive won't add duplicate classes if a particular class was already set. |
|
30681 * |
|
30682 * When the expression changes, the previously added classes are removed and only then the |
|
30683 * new classes are added. |
|
30684 * |
|
30685 * @animations |
|
30686 * add - happens just before the class is applied to the element |
|
30687 * remove - happens just before the class is removed from the element |
|
30688 * |
|
30689 * @element ANY |
|
30690 * @param {expression} ngClass {@link guide/expression Expression} to eval. The result |
|
30691 * of the evaluation can be a string representing space delimited class |
|
30692 * names, an array, or a map of class names to boolean values. In the case of a map, the |
|
30693 * names of the properties whose values are truthy will be added as css classes to the |
|
30694 * element. |
|
30695 * |
|
30696 * @example Example that demonstrates basic bindings via ngClass directive. |
|
30697 <example> |
|
30698 <file name="index.html"> |
|
30699 <p ng-class="{strike: deleted, bold: important, red: error}">Map Syntax Example</p> |
|
30700 <input type="checkbox" ng-model="deleted"> deleted (apply "strike" class)<br> |
|
30701 <input type="checkbox" ng-model="important"> important (apply "bold" class)<br> |
|
30702 <input type="checkbox" ng-model="error"> error (apply "red" class) |
|
30703 <hr> |
|
30704 <p ng-class="style">Using String Syntax</p> |
|
30705 <input type="text" ng-model="style" placeholder="Type: bold strike red"> |
|
30706 <hr> |
|
30707 <p ng-class="[style1, style2, style3]">Using Array Syntax</p> |
|
30708 <input ng-model="style1" placeholder="Type: bold, strike or red"><br> |
|
30709 <input ng-model="style2" placeholder="Type: bold, strike or red"><br> |
|
30710 <input ng-model="style3" placeholder="Type: bold, strike or red"><br> |
|
30711 </file> |
|
30712 <file name="style.css"> |
|
30713 .strike { |
|
30714 text-decoration: line-through; |
|
30715 } |
|
30716 .bold { |
|
30717 font-weight: bold; |
|
30718 } |
|
30719 .red { |
|
30720 color: red; |
|
30721 } |
|
30722 </file> |
|
30723 <file name="protractor.js" type="protractor"> |
|
30724 var ps = element.all(by.css('p')); |
|
30725 |
|
30726 it('should let you toggle the class', function() { |
|
30727 |
|
30728 expect(ps.first().getAttribute('class')).not.toMatch(/bold/); |
|
30729 expect(ps.first().getAttribute('class')).not.toMatch(/red/); |
|
30730 |
|
30731 element(by.model('important')).click(); |
|
30732 expect(ps.first().getAttribute('class')).toMatch(/bold/); |
|
30733 |
|
30734 element(by.model('error')).click(); |
|
30735 expect(ps.first().getAttribute('class')).toMatch(/red/); |
|
30736 }); |
|
30737 |
|
30738 it('should let you toggle string example', function() { |
|
30739 expect(ps.get(1).getAttribute('class')).toBe(''); |
|
30740 element(by.model('style')).clear(); |
|
30741 element(by.model('style')).sendKeys('red'); |
|
30742 expect(ps.get(1).getAttribute('class')).toBe('red'); |
|
30743 }); |
|
30744 |
|
30745 it('array example should have 3 classes', function() { |
|
30746 expect(ps.last().getAttribute('class')).toBe(''); |
|
30747 element(by.model('style1')).sendKeys('bold'); |
|
30748 element(by.model('style2')).sendKeys('strike'); |
|
30749 element(by.model('style3')).sendKeys('red'); |
|
30750 expect(ps.last().getAttribute('class')).toBe('bold strike red'); |
|
30751 }); |
|
30752 </file> |
|
30753 </example> |
|
30754 |
|
30755 ## Animations |
|
30756 |
|
30757 The example below demonstrates how to perform animations using ngClass. |
|
30758 |
|
30759 <example module="ngAnimate" deps="angular-animate.js" animations="true"> |
|
30760 <file name="index.html"> |
|
30761 <input id="setbtn" type="button" value="set" ng-click="myVar='my-class'"> |
|
30762 <input id="clearbtn" type="button" value="clear" ng-click="myVar=''"> |
|
30763 <br> |
|
30764 <span class="base-class" ng-class="myVar">Sample Text</span> |
|
30765 </file> |
|
30766 <file name="style.css"> |
|
30767 .base-class { |
|
30768 -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; |
|
30769 transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; |
|
30770 } |
|
30771 |
|
30772 .base-class.my-class { |
|
30773 color: red; |
|
30774 font-size:3em; |
|
30775 } |
|
30776 </file> |
|
30777 <file name="protractor.js" type="protractor"> |
|
30778 it('should check ng-class', function() { |
|
30779 expect(element(by.css('.base-class')).getAttribute('class')).not. |
|
30780 toMatch(/my-class/); |
|
30781 |
|
30782 element(by.id('setbtn')).click(); |
|
30783 |
|
30784 expect(element(by.css('.base-class')).getAttribute('class')). |
|
30785 toMatch(/my-class/); |
|
30786 |
|
30787 element(by.id('clearbtn')).click(); |
|
30788 |
|
30789 expect(element(by.css('.base-class')).getAttribute('class')).not. |
|
30790 toMatch(/my-class/); |
|
30791 }); |
|
30792 </file> |
|
30793 </example> |
|
30794 |
|
30795 |
|
30796 ## ngClass and pre-existing CSS3 Transitions/Animations |
|
30797 The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure. |
|
30798 Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder |
|
30799 any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure |
|
30800 to view the step by step details of {@link ngAnimate.$animate#addclass $animate.addClass} and |
|
30801 {@link ngAnimate.$animate#removeclass $animate.removeClass}. |
|
30802 */ |
|
30803 var ngClassDirective = classDirective('', true); |
|
30804 |
|
30805 /** |
|
30806 * @ngdoc directive |
|
30807 * @name ngClassOdd |
|
30808 * @restrict AC |
|
30809 * |
|
30810 * @description |
|
30811 * The `ngClassOdd` and `ngClassEven` directives work exactly as |
|
30812 * {@link ng.directive:ngClass ngClass}, except they work in |
|
30813 * conjunction with `ngRepeat` and take effect only on odd (even) rows. |
|
30814 * |
|
30815 * This directive can be applied only within the scope of an |
|
30816 * {@link ng.directive:ngRepeat ngRepeat}. |
|
30817 * |
|
30818 * @element ANY |
|
30819 * @param {expression} ngClassOdd {@link guide/expression Expression} to eval. The result |
|
30820 * of the evaluation can be a string representing space delimited class names or an array. |
|
30821 * |
|
30822 * @example |
|
30823 <example> |
|
30824 <file name="index.html"> |
|
30825 <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']"> |
|
30826 <li ng-repeat="name in names"> |
|
30827 <span ng-class-odd="'odd'" ng-class-even="'even'"> |
|
30828 {{name}} |
|
30829 </span> |
|
30830 </li> |
|
30831 </ol> |
|
30832 </file> |
|
30833 <file name="style.css"> |
|
30834 .odd { |
|
30835 color: red; |
|
30836 } |
|
30837 .even { |
|
30838 color: blue; |
|
30839 } |
|
30840 </file> |
|
30841 <file name="protractor.js" type="protractor"> |
|
30842 it('should check ng-class-odd and ng-class-even', function() { |
|
30843 expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). |
|
30844 toMatch(/odd/); |
|
30845 expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). |
|
30846 toMatch(/even/); |
|
30847 }); |
|
30848 </file> |
|
30849 </example> |
|
30850 */ |
|
30851 var ngClassOddDirective = classDirective('Odd', 0); |
|
30852 |
|
30853 /** |
|
30854 * @ngdoc directive |
|
30855 * @name ngClassEven |
|
30856 * @restrict AC |
|
30857 * |
|
30858 * @description |
|
30859 * The `ngClassOdd` and `ngClassEven` directives work exactly as |
|
30860 * {@link ng.directive:ngClass ngClass}, except they work in |
|
30861 * conjunction with `ngRepeat` and take effect only on odd (even) rows. |
|
30862 * |
|
30863 * This directive can be applied only within the scope of an |
|
30864 * {@link ng.directive:ngRepeat ngRepeat}. |
|
30865 * |
|
30866 * @element ANY |
|
30867 * @param {expression} ngClassEven {@link guide/expression Expression} to eval. The |
|
30868 * result of the evaluation can be a string representing space delimited class names or an array. |
|
30869 * |
|
30870 * @example |
|
30871 <example> |
|
30872 <file name="index.html"> |
|
30873 <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']"> |
|
30874 <li ng-repeat="name in names"> |
|
30875 <span ng-class-odd="'odd'" ng-class-even="'even'"> |
|
30876 {{name}} |
|
30877 </span> |
|
30878 </li> |
|
30879 </ol> |
|
30880 </file> |
|
30881 <file name="style.css"> |
|
30882 .odd { |
|
30883 color: red; |
|
30884 } |
|
30885 .even { |
|
30886 color: blue; |
|
30887 } |
|
30888 </file> |
|
30889 <file name="protractor.js" type="protractor"> |
|
30890 it('should check ng-class-odd and ng-class-even', function() { |
|
30891 expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')). |
|
30892 toMatch(/odd/); |
|
30893 expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')). |
|
30894 toMatch(/even/); |
|
30895 }); |
|
30896 </file> |
|
30897 </example> |
|
30898 */ |
|
30899 var ngClassEvenDirective = classDirective('Even', 1); |
|
30900 |
|
30901 /** |
|
30902 * @ngdoc directive |
|
30903 * @name ngCloak |
|
30904 * @restrict AC |
|
30905 * |
|
30906 * @description |
|
30907 * The `ngCloak` directive is used to prevent the Angular html template from being briefly |
|
30908 * displayed by the browser in its raw (uncompiled) form while your application is loading. Use this |
|
30909 * directive to avoid the undesirable flicker effect caused by the html template display. |
|
30910 * |
|
30911 * The directive can be applied to the `<body>` element, but the preferred usage is to apply |
|
30912 * multiple `ngCloak` directives to small portions of the page to permit progressive rendering |
|
30913 * of the browser view. |
|
30914 * |
|
30915 * `ngCloak` works in cooperation with the following css rule embedded within `angular.js` and |
|
30916 * `angular.min.js`. |
|
30917 * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). |
|
30918 * |
|
30919 * ```css |
|
30920 * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { |
|
30921 * display: none !important; |
|
30922 * } |
|
30923 * ``` |
|
30924 * |
|
30925 * When this css rule is loaded by the browser, all html elements (including their children) that |
|
30926 * are tagged with the `ngCloak` directive are hidden. When Angular encounters this directive |
|
30927 * during the compilation of the template it deletes the `ngCloak` element attribute, making |
|
30928 * the compiled element visible. |
|
30929 * |
|
30930 * For the best result, the `angular.js` script must be loaded in the head section of the html |
|
30931 * document; alternatively, the css rule above must be included in the external stylesheet of the |
|
30932 * application. |
|
30933 * |
|
30934 * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they |
|
30935 * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css |
|
30936 * class `ng-cloak` in addition to the `ngCloak` directive as shown in the example below. |
|
30937 * |
|
30938 * @element ANY |
|
30939 * |
|
30940 * @example |
|
30941 <example> |
|
30942 <file name="index.html"> |
|
30943 <div id="template1" ng-cloak>{{ 'hello' }}</div> |
|
30944 <div id="template2" ng-cloak class="ng-cloak">{{ 'hello IE7' }}</div> |
|
30945 </file> |
|
30946 <file name="protractor.js" type="protractor"> |
|
30947 it('should remove the template directive and css class', function() { |
|
30948 expect($('#template1').getAttribute('ng-cloak')). |
|
30949 toBeNull(); |
|
30950 expect($('#template2').getAttribute('ng-cloak')). |
|
30951 toBeNull(); |
|
30952 }); |
|
30953 </file> |
|
30954 </example> |
|
30955 * |
|
30956 */ |
|
30957 var ngCloakDirective = ngDirective({ |
|
30958 compile: function(element, attr) { |
|
30959 attr.$set('ngCloak', undefined); |
|
30960 element.removeClass('ng-cloak'); |
|
30961 } |
|
30962 }); |
|
30963 |
|
30964 /** |
|
30965 * @ngdoc directive |
|
30966 * @name ngController |
|
30967 * |
|
30968 * @description |
|
30969 * The `ngController` directive attaches a controller class to the view. This is a key aspect of how angular |
|
30970 * supports the principles behind the Model-View-Controller design pattern. |
|
30971 * |
|
30972 * MVC components in angular: |
|
30973 * |
|
30974 * * Model — Models are the properties of a scope; scopes are attached to the DOM where scope properties |
|
30975 * are accessed through bindings. |
|
30976 * * View — The template (HTML with data bindings) that is rendered into the View. |
|
30977 * * Controller — The `ngController` directive specifies a Controller class; the class contains business |
|
30978 * logic behind the application to decorate the scope with functions and values |
|
30979 * |
|
30980 * Note that you can also attach controllers to the DOM by declaring it in a route definition |
|
30981 * via the {@link ngRoute.$route $route} service. A common mistake is to declare the controller |
|
30982 * again using `ng-controller` in the template itself. This will cause the controller to be attached |
|
30983 * and executed twice. |
|
30984 * |
|
30985 * @element ANY |
|
30986 * @scope |
|
30987 * @priority 500 |
|
30988 * @param {expression} ngController Name of a constructor function registered with the current |
|
30989 * {@link ng.$controllerProvider $controllerProvider} or an {@link guide/expression expression} |
|
30990 * that on the current scope evaluates to a constructor function. |
|
30991 * |
|
30992 * The controller instance can be published into a scope property by specifying |
|
30993 * `ng-controller="as propertyName"`. |
|
30994 * |
|
30995 * If the current `$controllerProvider` is configured to use globals (via |
|
30996 * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may |
|
30997 * also be the name of a globally accessible constructor function (not recommended). |
|
30998 * |
|
30999 * @example |
|
31000 * Here is a simple form for editing user contact information. Adding, removing, clearing, and |
|
31001 * greeting are methods declared on the controller (see source tab). These methods can |
|
31002 * easily be called from the angular markup. Any changes to the data are automatically reflected |
|
31003 * in the View without the need for a manual update. |
|
31004 * |
|
31005 * Two different declaration styles are included below: |
|
31006 * |
|
31007 * * one binds methods and properties directly onto the controller using `this`: |
|
31008 * `ng-controller="SettingsController1 as settings"` |
|
31009 * * one injects `$scope` into the controller: |
|
31010 * `ng-controller="SettingsController2"` |
|
31011 * |
|
31012 * The second option is more common in the Angular community, and is generally used in boilerplates |
|
31013 * and in this guide. However, there are advantages to binding properties directly to the controller |
|
31014 * and avoiding scope. |
|
31015 * |
|
31016 * * Using `controller as` makes it obvious which controller you are accessing in the template when |
|
31017 * multiple controllers apply to an element. |
|
31018 * * If you are writing your controllers as classes you have easier access to the properties and |
|
31019 * methods, which will appear on the scope, from inside the controller code. |
|
31020 * * Since there is always a `.` in the bindings, you don't have to worry about prototypal |
|
31021 * inheritance masking primitives. |
|
31022 * |
|
31023 * This example demonstrates the `controller as` syntax. |
|
31024 * |
|
31025 * <example name="ngControllerAs" module="controllerAsExample"> |
|
31026 * <file name="index.html"> |
|
31027 * <div id="ctrl-as-exmpl" ng-controller="SettingsController1 as settings"> |
|
31028 * Name: <input type="text" ng-model="settings.name"/> |
|
31029 * [ <a href="" ng-click="settings.greet()">greet</a> ]<br/> |
|
31030 * Contact: |
|
31031 * <ul> |
|
31032 * <li ng-repeat="contact in settings.contacts"> |
|
31033 * <select ng-model="contact.type"> |
|
31034 * <option>phone</option> |
|
31035 * <option>email</option> |
|
31036 * </select> |
|
31037 * <input type="text" ng-model="contact.value"/> |
|
31038 * [ <a href="" ng-click="settings.clearContact(contact)">clear</a> |
|
31039 * | <a href="" ng-click="settings.removeContact(contact)">X</a> ] |
|
31040 * </li> |
|
31041 * <li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li> |
|
31042 * </ul> |
|
31043 * </div> |
|
31044 * </file> |
|
31045 * <file name="app.js"> |
|
31046 * angular.module('controllerAsExample', []) |
|
31047 * .controller('SettingsController1', SettingsController1); |
|
31048 * |
|
31049 * function SettingsController1() { |
|
31050 * this.name = "John Smith"; |
|
31051 * this.contacts = [ |
|
31052 * {type: 'phone', value: '408 555 1212'}, |
|
31053 * {type: 'email', value: 'john.smith@example.org'} ]; |
|
31054 * } |
|
31055 * |
|
31056 * SettingsController1.prototype.greet = function() { |
|
31057 * alert(this.name); |
|
31058 * }; |
|
31059 * |
|
31060 * SettingsController1.prototype.addContact = function() { |
|
31061 * this.contacts.push({type: 'email', value: 'yourname@example.org'}); |
|
31062 * }; |
|
31063 * |
|
31064 * SettingsController1.prototype.removeContact = function(contactToRemove) { |
|
31065 * var index = this.contacts.indexOf(contactToRemove); |
|
31066 * this.contacts.splice(index, 1); |
|
31067 * }; |
|
31068 * |
|
31069 * SettingsController1.prototype.clearContact = function(contact) { |
|
31070 * contact.type = 'phone'; |
|
31071 * contact.value = ''; |
|
31072 * }; |
|
31073 * </file> |
|
31074 * <file name="protractor.js" type="protractor"> |
|
31075 * it('should check controller as', function() { |
|
31076 * var container = element(by.id('ctrl-as-exmpl')); |
|
31077 * expect(container.element(by.model('settings.name')) |
|
31078 * .getAttribute('value')).toBe('John Smith'); |
|
31079 * |
|
31080 * var firstRepeat = |
|
31081 * container.element(by.repeater('contact in settings.contacts').row(0)); |
|
31082 * var secondRepeat = |
|
31083 * container.element(by.repeater('contact in settings.contacts').row(1)); |
|
31084 * |
|
31085 * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) |
|
31086 * .toBe('408 555 1212'); |
|
31087 * |
|
31088 * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) |
|
31089 * .toBe('john.smith@example.org'); |
|
31090 * |
|
31091 * firstRepeat.element(by.linkText('clear')).click(); |
|
31092 * |
|
31093 * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) |
|
31094 * .toBe(''); |
|
31095 * |
|
31096 * container.element(by.linkText('add')).click(); |
|
31097 * |
|
31098 * expect(container.element(by.repeater('contact in settings.contacts').row(2)) |
|
31099 * .element(by.model('contact.value')) |
|
31100 * .getAttribute('value')) |
|
31101 * .toBe('yourname@example.org'); |
|
31102 * }); |
|
31103 * </file> |
|
31104 * </example> |
|
31105 * |
|
31106 * This example demonstrates the "attach to `$scope`" style of controller. |
|
31107 * |
|
31108 * <example name="ngController" module="controllerExample"> |
|
31109 * <file name="index.html"> |
|
31110 * <div id="ctrl-exmpl" ng-controller="SettingsController2"> |
|
31111 * Name: <input type="text" ng-model="name"/> |
|
31112 * [ <a href="" ng-click="greet()">greet</a> ]<br/> |
|
31113 * Contact: |
|
31114 * <ul> |
|
31115 * <li ng-repeat="contact in contacts"> |
|
31116 * <select ng-model="contact.type"> |
|
31117 * <option>phone</option> |
|
31118 * <option>email</option> |
|
31119 * </select> |
|
31120 * <input type="text" ng-model="contact.value"/> |
|
31121 * [ <a href="" ng-click="clearContact(contact)">clear</a> |
|
31122 * | <a href="" ng-click="removeContact(contact)">X</a> ] |
|
31123 * </li> |
|
31124 * <li>[ <a href="" ng-click="addContact()">add</a> ]</li> |
|
31125 * </ul> |
|
31126 * </div> |
|
31127 * </file> |
|
31128 * <file name="app.js"> |
|
31129 * angular.module('controllerExample', []) |
|
31130 * .controller('SettingsController2', ['$scope', SettingsController2]); |
|
31131 * |
|
31132 * function SettingsController2($scope) { |
|
31133 * $scope.name = "John Smith"; |
|
31134 * $scope.contacts = [ |
|
31135 * {type:'phone', value:'408 555 1212'}, |
|
31136 * {type:'email', value:'john.smith@example.org'} ]; |
|
31137 * |
|
31138 * $scope.greet = function() { |
|
31139 * alert($scope.name); |
|
31140 * }; |
|
31141 * |
|
31142 * $scope.addContact = function() { |
|
31143 * $scope.contacts.push({type:'email', value:'yourname@example.org'}); |
|
31144 * }; |
|
31145 * |
|
31146 * $scope.removeContact = function(contactToRemove) { |
|
31147 * var index = $scope.contacts.indexOf(contactToRemove); |
|
31148 * $scope.contacts.splice(index, 1); |
|
31149 * }; |
|
31150 * |
|
31151 * $scope.clearContact = function(contact) { |
|
31152 * contact.type = 'phone'; |
|
31153 * contact.value = ''; |
|
31154 * }; |
|
31155 * } |
|
31156 * </file> |
|
31157 * <file name="protractor.js" type="protractor"> |
|
31158 * it('should check controller', function() { |
|
31159 * var container = element(by.id('ctrl-exmpl')); |
|
31160 * |
|
31161 * expect(container.element(by.model('name')) |
|
31162 * .getAttribute('value')).toBe('John Smith'); |
|
31163 * |
|
31164 * var firstRepeat = |
|
31165 * container.element(by.repeater('contact in contacts').row(0)); |
|
31166 * var secondRepeat = |
|
31167 * container.element(by.repeater('contact in contacts').row(1)); |
|
31168 * |
|
31169 * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) |
|
31170 * .toBe('408 555 1212'); |
|
31171 * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) |
|
31172 * .toBe('john.smith@example.org'); |
|
31173 * |
|
31174 * firstRepeat.element(by.linkText('clear')).click(); |
|
31175 * |
|
31176 * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) |
|
31177 * .toBe(''); |
|
31178 * |
|
31179 * container.element(by.linkText('add')).click(); |
|
31180 * |
|
31181 * expect(container.element(by.repeater('contact in contacts').row(2)) |
|
31182 * .element(by.model('contact.value')) |
|
31183 * .getAttribute('value')) |
|
31184 * .toBe('yourname@example.org'); |
|
31185 * }); |
|
31186 * </file> |
|
31187 *</example> |
|
31188 |
|
31189 */ |
|
31190 var ngControllerDirective = [function() { |
|
31191 return { |
|
31192 restrict: 'A', |
|
31193 scope: true, |
|
31194 controller: '@', |
|
31195 priority: 500 |
|
31196 }; |
|
31197 }]; |
|
31198 |
|
31199 /** |
|
31200 * @ngdoc directive |
|
31201 * @name ngCsp |
|
31202 * |
|
31203 * @element html |
|
31204 * @description |
|
31205 * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. |
|
31206 * |
|
31207 * This is necessary when developing things like Google Chrome Extensions. |
|
31208 * |
|
31209 * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things). |
|
31210 * For Angular to be CSP compatible there are only two things that we need to do differently: |
|
31211 * |
|
31212 * - don't use `Function` constructor to generate optimized value getters |
|
31213 * - don't inject custom stylesheet into the document |
|
31214 * |
|
31215 * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` |
|
31216 * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will |
|
31217 * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will |
|
31218 * be raised. |
|
31219 * |
|
31220 * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically |
|
31221 * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}). |
|
31222 * To make those directives work in CSP mode, include the `angular-csp.css` manually. |
|
31223 * |
|
31224 * Angular tries to autodetect if CSP is active and automatically turn on the CSP-safe mode. This |
|
31225 * autodetection however triggers a CSP error to be logged in the console: |
|
31226 * |
|
31227 * ``` |
|
31228 * Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of |
|
31229 * script in the following Content Security Policy directive: "default-src 'self'". Note that |
|
31230 * 'script-src' was not explicitly set, so 'default-src' is used as a fallback. |
|
31231 * ``` |
|
31232 * |
|
31233 * This error is harmless but annoying. To prevent the error from showing up, put the `ngCsp` |
|
31234 * directive on the root element of the application or on the `angular.js` script tag, whichever |
|
31235 * appears first in the html document. |
|
31236 * |
|
31237 * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* |
|
31238 * |
|
31239 * @example |
|
31240 * This example shows how to apply the `ngCsp` directive to the `html` tag. |
|
31241 ```html |
|
31242 <!doctype html> |
|
31243 <html ng-app ng-csp> |
|
31244 ... |
|
31245 ... |
|
31246 </html> |
|
31247 ``` |
|
31248 * @example |
|
31249 // Note: the suffix `.csp` in the example name triggers |
|
31250 // csp mode in our http server! |
|
31251 <example name="example.csp" module="cspExample" ng-csp="true"> |
|
31252 <file name="index.html"> |
|
31253 <div ng-controller="MainController as ctrl"> |
|
31254 <div> |
|
31255 <button ng-click="ctrl.inc()" id="inc">Increment</button> |
|
31256 <span id="counter"> |
|
31257 {{ctrl.counter}} |
|
31258 </span> |
|
31259 </div> |
|
31260 |
|
31261 <div> |
|
31262 <button ng-click="ctrl.evil()" id="evil">Evil</button> |
|
31263 <span id="evilError"> |
|
31264 {{ctrl.evilError}} |
|
31265 </span> |
|
31266 </div> |
|
31267 </div> |
|
31268 </file> |
|
31269 <file name="script.js"> |
|
31270 angular.module('cspExample', []) |
|
31271 .controller('MainController', function() { |
|
31272 this.counter = 0; |
|
31273 this.inc = function() { |
|
31274 this.counter++; |
|
31275 }; |
|
31276 this.evil = function() { |
|
31277 // jshint evil:true |
|
31278 try { |
|
31279 eval('1+2'); |
|
31280 } catch (e) { |
|
31281 this.evilError = e.message; |
|
31282 } |
|
31283 }; |
|
31284 }); |
|
31285 </file> |
|
31286 <file name="protractor.js" type="protractor"> |
|
31287 var util, webdriver; |
|
31288 |
|
31289 var incBtn = element(by.id('inc')); |
|
31290 var counter = element(by.id('counter')); |
|
31291 var evilBtn = element(by.id('evil')); |
|
31292 var evilError = element(by.id('evilError')); |
|
31293 |
|
31294 function getAndClearSevereErrors() { |
|
31295 return browser.manage().logs().get('browser').then(function(browserLog) { |
|
31296 return browserLog.filter(function(logEntry) { |
|
31297 return logEntry.level.value > webdriver.logging.Level.WARNING.value; |
|
31298 }); |
|
31299 }); |
|
31300 } |
|
31301 |
|
31302 function clearErrors() { |
|
31303 getAndClearSevereErrors(); |
|
31304 } |
|
31305 |
|
31306 function expectNoErrors() { |
|
31307 getAndClearSevereErrors().then(function(filteredLog) { |
|
31308 expect(filteredLog.length).toEqual(0); |
|
31309 if (filteredLog.length) { |
|
31310 console.log('browser console errors: ' + util.inspect(filteredLog)); |
|
31311 } |
|
31312 }); |
|
31313 } |
|
31314 |
|
31315 function expectError(regex) { |
|
31316 getAndClearSevereErrors().then(function(filteredLog) { |
|
31317 var found = false; |
|
31318 filteredLog.forEach(function(log) { |
|
31319 if (log.message.match(regex)) { |
|
31320 found = true; |
|
31321 } |
|
31322 }); |
|
31323 if (!found) { |
|
31324 throw new Error('expected an error that matches ' + regex); |
|
31325 } |
|
31326 }); |
|
31327 } |
|
31328 |
|
31329 beforeEach(function() { |
|
31330 util = require('util'); |
|
31331 webdriver = require('protractor/node_modules/selenium-webdriver'); |
|
31332 }); |
|
31333 |
|
31334 // For now, we only test on Chrome, |
|
31335 // as Safari does not load the page with Protractor's injected scripts, |
|
31336 // and Firefox webdriver always disables content security policy (#6358) |
|
31337 if (browser.params.browser !== 'chrome') { |
|
31338 return; |
|
31339 } |
|
31340 |
|
31341 it('should not report errors when the page is loaded', function() { |
|
31342 // clear errors so we are not dependent on previous tests |
|
31343 clearErrors(); |
|
31344 // Need to reload the page as the page is already loaded when |
|
31345 // we come here |
|
31346 browser.driver.getCurrentUrl().then(function(url) { |
|
31347 browser.get(url); |
|
31348 }); |
|
31349 expectNoErrors(); |
|
31350 }); |
|
31351 |
|
31352 it('should evaluate expressions', function() { |
|
31353 expect(counter.getText()).toEqual('0'); |
|
31354 incBtn.click(); |
|
31355 expect(counter.getText()).toEqual('1'); |
|
31356 expectNoErrors(); |
|
31357 }); |
|
31358 |
|
31359 it('should throw and report an error when using "eval"', function() { |
|
31360 evilBtn.click(); |
|
31361 expect(evilError.getText()).toMatch(/Content Security Policy/); |
|
31362 expectError(/Content Security Policy/); |
|
31363 }); |
|
31364 </file> |
|
31365 </example> |
|
31366 */ |
|
31367 |
|
31368 // ngCsp is not implemented as a proper directive any more, because we need it be processed while we |
|
31369 // bootstrap the system (before $parse is instantiated), for this reason we just have |
|
31370 // the csp.isActive() fn that looks for ng-csp attribute anywhere in the current doc |
|
31371 |
|
31372 /** |
|
31373 * @ngdoc directive |
|
31374 * @name ngClick |
|
31375 * |
|
31376 * @description |
|
31377 * The ngClick directive allows you to specify custom behavior when |
|
31378 * an element is clicked. |
|
31379 * |
|
31380 * @element ANY |
|
31381 * @priority 0 |
|
31382 * @param {expression} ngClick {@link guide/expression Expression} to evaluate upon |
|
31383 * click. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31384 * |
|
31385 * @example |
|
31386 <example> |
|
31387 <file name="index.html"> |
|
31388 <button ng-click="count = count + 1" ng-init="count=0"> |
|
31389 Increment |
|
31390 </button> |
|
31391 <span> |
|
31392 count: {{count}} |
|
31393 </span> |
|
31394 </file> |
|
31395 <file name="protractor.js" type="protractor"> |
|
31396 it('should check ng-click', function() { |
|
31397 expect(element(by.binding('count')).getText()).toMatch('0'); |
|
31398 element(by.css('button')).click(); |
|
31399 expect(element(by.binding('count')).getText()).toMatch('1'); |
|
31400 }); |
|
31401 </file> |
|
31402 </example> |
|
31403 */ |
|
31404 /* |
|
31405 * A directive that allows creation of custom onclick handlers that are defined as angular |
|
31406 * expressions and are compiled and executed within the current scope. |
|
31407 * |
|
31408 * Events that are handled via these handler are always configured not to propagate further. |
|
31409 */ |
|
31410 var ngEventDirectives = {}; |
|
31411 |
|
31412 // For events that might fire synchronously during DOM manipulation |
|
31413 // we need to execute their event handlers asynchronously using $evalAsync, |
|
31414 // so that they are not executed in an inconsistent state. |
|
31415 var forceAsyncEvents = { |
|
31416 'blur': true, |
|
31417 'focus': true |
|
31418 }; |
|
31419 forEach( |
|
31420 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), |
|
31421 function(eventName) { |
|
31422 var directiveName = directiveNormalize('ng-' + eventName); |
|
31423 ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) { |
|
31424 return { |
|
31425 restrict: 'A', |
|
31426 compile: function($element, attr) { |
|
31427 var fn = $parse(attr[directiveName]); |
|
31428 return function ngEventHandler(scope, element) { |
|
31429 element.on(eventName, function(event) { |
|
31430 var callback = function() { |
|
31431 fn(scope, {$event:event}); |
|
31432 }; |
|
31433 if (forceAsyncEvents[eventName] && $rootScope.$$phase) { |
|
31434 scope.$evalAsync(callback); |
|
31435 } else { |
|
31436 scope.$apply(callback); |
|
31437 } |
|
31438 }); |
|
31439 }; |
|
31440 } |
|
31441 }; |
|
31442 }]; |
|
31443 } |
|
31444 ); |
|
31445 |
|
31446 /** |
|
31447 * @ngdoc directive |
|
31448 * @name ngDblclick |
|
31449 * |
|
31450 * @description |
|
31451 * The `ngDblclick` directive allows you to specify custom behavior on a dblclick event. |
|
31452 * |
|
31453 * @element ANY |
|
31454 * @priority 0 |
|
31455 * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon |
|
31456 * a dblclick. (The Event object is available as `$event`) |
|
31457 * |
|
31458 * @example |
|
31459 <example> |
|
31460 <file name="index.html"> |
|
31461 <button ng-dblclick="count = count + 1" ng-init="count=0"> |
|
31462 Increment (on double click) |
|
31463 </button> |
|
31464 count: {{count}} |
|
31465 </file> |
|
31466 </example> |
|
31467 */ |
|
31468 |
|
31469 |
|
31470 /** |
|
31471 * @ngdoc directive |
|
31472 * @name ngMousedown |
|
31473 * |
|
31474 * @description |
|
31475 * The ngMousedown directive allows you to specify custom behavior on mousedown event. |
|
31476 * |
|
31477 * @element ANY |
|
31478 * @priority 0 |
|
31479 * @param {expression} ngMousedown {@link guide/expression Expression} to evaluate upon |
|
31480 * mousedown. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31481 * |
|
31482 * @example |
|
31483 <example> |
|
31484 <file name="index.html"> |
|
31485 <button ng-mousedown="count = count + 1" ng-init="count=0"> |
|
31486 Increment (on mouse down) |
|
31487 </button> |
|
31488 count: {{count}} |
|
31489 </file> |
|
31490 </example> |
|
31491 */ |
|
31492 |
|
31493 |
|
31494 /** |
|
31495 * @ngdoc directive |
|
31496 * @name ngMouseup |
|
31497 * |
|
31498 * @description |
|
31499 * Specify custom behavior on mouseup event. |
|
31500 * |
|
31501 * @element ANY |
|
31502 * @priority 0 |
|
31503 * @param {expression} ngMouseup {@link guide/expression Expression} to evaluate upon |
|
31504 * mouseup. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31505 * |
|
31506 * @example |
|
31507 <example> |
|
31508 <file name="index.html"> |
|
31509 <button ng-mouseup="count = count + 1" ng-init="count=0"> |
|
31510 Increment (on mouse up) |
|
31511 </button> |
|
31512 count: {{count}} |
|
31513 </file> |
|
31514 </example> |
|
31515 */ |
|
31516 |
|
31517 /** |
|
31518 * @ngdoc directive |
|
31519 * @name ngMouseover |
|
31520 * |
|
31521 * @description |
|
31522 * Specify custom behavior on mouseover event. |
|
31523 * |
|
31524 * @element ANY |
|
31525 * @priority 0 |
|
31526 * @param {expression} ngMouseover {@link guide/expression Expression} to evaluate upon |
|
31527 * mouseover. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31528 * |
|
31529 * @example |
|
31530 <example> |
|
31531 <file name="index.html"> |
|
31532 <button ng-mouseover="count = count + 1" ng-init="count=0"> |
|
31533 Increment (when mouse is over) |
|
31534 </button> |
|
31535 count: {{count}} |
|
31536 </file> |
|
31537 </example> |
|
31538 */ |
|
31539 |
|
31540 |
|
31541 /** |
|
31542 * @ngdoc directive |
|
31543 * @name ngMouseenter |
|
31544 * |
|
31545 * @description |
|
31546 * Specify custom behavior on mouseenter event. |
|
31547 * |
|
31548 * @element ANY |
|
31549 * @priority 0 |
|
31550 * @param {expression} ngMouseenter {@link guide/expression Expression} to evaluate upon |
|
31551 * mouseenter. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31552 * |
|
31553 * @example |
|
31554 <example> |
|
31555 <file name="index.html"> |
|
31556 <button ng-mouseenter="count = count + 1" ng-init="count=0"> |
|
31557 Increment (when mouse enters) |
|
31558 </button> |
|
31559 count: {{count}} |
|
31560 </file> |
|
31561 </example> |
|
31562 */ |
|
31563 |
|
31564 |
|
31565 /** |
|
31566 * @ngdoc directive |
|
31567 * @name ngMouseleave |
|
31568 * |
|
31569 * @description |
|
31570 * Specify custom behavior on mouseleave event. |
|
31571 * |
|
31572 * @element ANY |
|
31573 * @priority 0 |
|
31574 * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon |
|
31575 * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31576 * |
|
31577 * @example |
|
31578 <example> |
|
31579 <file name="index.html"> |
|
31580 <button ng-mouseleave="count = count + 1" ng-init="count=0"> |
|
31581 Increment (when mouse leaves) |
|
31582 </button> |
|
31583 count: {{count}} |
|
31584 </file> |
|
31585 </example> |
|
31586 */ |
|
31587 |
|
31588 |
|
31589 /** |
|
31590 * @ngdoc directive |
|
31591 * @name ngMousemove |
|
31592 * |
|
31593 * @description |
|
31594 * Specify custom behavior on mousemove event. |
|
31595 * |
|
31596 * @element ANY |
|
31597 * @priority 0 |
|
31598 * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon |
|
31599 * mousemove. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31600 * |
|
31601 * @example |
|
31602 <example> |
|
31603 <file name="index.html"> |
|
31604 <button ng-mousemove="count = count + 1" ng-init="count=0"> |
|
31605 Increment (when mouse moves) |
|
31606 </button> |
|
31607 count: {{count}} |
|
31608 </file> |
|
31609 </example> |
|
31610 */ |
|
31611 |
|
31612 |
|
31613 /** |
|
31614 * @ngdoc directive |
|
31615 * @name ngKeydown |
|
31616 * |
|
31617 * @description |
|
31618 * Specify custom behavior on keydown event. |
|
31619 * |
|
31620 * @element ANY |
|
31621 * @priority 0 |
|
31622 * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon |
|
31623 * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) |
|
31624 * |
|
31625 * @example |
|
31626 <example> |
|
31627 <file name="index.html"> |
|
31628 <input ng-keydown="count = count + 1" ng-init="count=0"> |
|
31629 key down count: {{count}} |
|
31630 </file> |
|
31631 </example> |
|
31632 */ |
|
31633 |
|
31634 |
|
31635 /** |
|
31636 * @ngdoc directive |
|
31637 * @name ngKeyup |
|
31638 * |
|
31639 * @description |
|
31640 * Specify custom behavior on keyup event. |
|
31641 * |
|
31642 * @element ANY |
|
31643 * @priority 0 |
|
31644 * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon |
|
31645 * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) |
|
31646 * |
|
31647 * @example |
|
31648 <example> |
|
31649 <file name="index.html"> |
|
31650 <p>Typing in the input box below updates the key count</p> |
|
31651 <input ng-keyup="count = count + 1" ng-init="count=0"> key up count: {{count}} |
|
31652 |
|
31653 <p>Typing in the input box below updates the keycode</p> |
|
31654 <input ng-keyup="event=$event"> |
|
31655 <p>event keyCode: {{ event.keyCode }}</p> |
|
31656 <p>event altKey: {{ event.altKey }}</p> |
|
31657 </file> |
|
31658 </example> |
|
31659 */ |
|
31660 |
|
31661 |
|
31662 /** |
|
31663 * @ngdoc directive |
|
31664 * @name ngKeypress |
|
31665 * |
|
31666 * @description |
|
31667 * Specify custom behavior on keypress event. |
|
31668 * |
|
31669 * @element ANY |
|
31670 * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon |
|
31671 * keypress. ({@link guide/expression#-event- Event object is available as `$event`} |
|
31672 * and can be interrogated for keyCode, altKey, etc.) |
|
31673 * |
|
31674 * @example |
|
31675 <example> |
|
31676 <file name="index.html"> |
|
31677 <input ng-keypress="count = count + 1" ng-init="count=0"> |
|
31678 key press count: {{count}} |
|
31679 </file> |
|
31680 </example> |
|
31681 */ |
|
31682 |
|
31683 |
|
31684 /** |
|
31685 * @ngdoc directive |
|
31686 * @name ngSubmit |
|
31687 * |
|
31688 * @description |
|
31689 * Enables binding angular expressions to onsubmit events. |
|
31690 * |
|
31691 * Additionally it prevents the default action (which for form means sending the request to the |
|
31692 * server and reloading the current page), but only if the form does not contain `action`, |
|
31693 * `data-action`, or `x-action` attributes. |
|
31694 * |
|
31695 * <div class="alert alert-warning"> |
|
31696 * **Warning:** Be careful not to cause "double-submission" by using both the `ngClick` and |
|
31697 * `ngSubmit` handlers together. See the |
|
31698 * {@link form#submitting-a-form-and-preventing-the-default-action `form` directive documentation} |
|
31699 * for a detailed discussion of when `ngSubmit` may be triggered. |
|
31700 * </div> |
|
31701 * |
|
31702 * @element form |
|
31703 * @priority 0 |
|
31704 * @param {expression} ngSubmit {@link guide/expression Expression} to eval. |
|
31705 * ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31706 * |
|
31707 * @example |
|
31708 <example module="submitExample"> |
|
31709 <file name="index.html"> |
|
31710 <script> |
|
31711 angular.module('submitExample', []) |
|
31712 .controller('ExampleController', ['$scope', function($scope) { |
|
31713 $scope.list = []; |
|
31714 $scope.text = 'hello'; |
|
31715 $scope.submit = function() { |
|
31716 if ($scope.text) { |
|
31717 $scope.list.push(this.text); |
|
31718 $scope.text = ''; |
|
31719 } |
|
31720 }; |
|
31721 }]); |
|
31722 </script> |
|
31723 <form ng-submit="submit()" ng-controller="ExampleController"> |
|
31724 Enter text and hit enter: |
|
31725 <input type="text" ng-model="text" name="text" /> |
|
31726 <input type="submit" id="submit" value="Submit" /> |
|
31727 <pre>list={{list}}</pre> |
|
31728 </form> |
|
31729 </file> |
|
31730 <file name="protractor.js" type="protractor"> |
|
31731 it('should check ng-submit', function() { |
|
31732 expect(element(by.binding('list')).getText()).toBe('list=[]'); |
|
31733 element(by.css('#submit')).click(); |
|
31734 expect(element(by.binding('list')).getText()).toContain('hello'); |
|
31735 expect(element(by.model('text')).getAttribute('value')).toBe(''); |
|
31736 }); |
|
31737 it('should ignore empty strings', function() { |
|
31738 expect(element(by.binding('list')).getText()).toBe('list=[]'); |
|
31739 element(by.css('#submit')).click(); |
|
31740 element(by.css('#submit')).click(); |
|
31741 expect(element(by.binding('list')).getText()).toContain('hello'); |
|
31742 }); |
|
31743 </file> |
|
31744 </example> |
|
31745 */ |
|
31746 |
|
31747 /** |
|
31748 * @ngdoc directive |
|
31749 * @name ngFocus |
|
31750 * |
|
31751 * @description |
|
31752 * Specify custom behavior on focus event. |
|
31753 * |
|
31754 * Note: As the `focus` event is executed synchronously when calling `input.focus()` |
|
31755 * AngularJS executes the expression using `scope.$evalAsync` if the event is fired |
|
31756 * during an `$apply` to ensure a consistent state. |
|
31757 * |
|
31758 * @element window, input, select, textarea, a |
|
31759 * @priority 0 |
|
31760 * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon |
|
31761 * focus. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31762 * |
|
31763 * @example |
|
31764 * See {@link ng.directive:ngClick ngClick} |
|
31765 */ |
|
31766 |
|
31767 /** |
|
31768 * @ngdoc directive |
|
31769 * @name ngBlur |
|
31770 * |
|
31771 * @description |
|
31772 * Specify custom behavior on blur event. |
|
31773 * |
|
31774 * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when |
|
31775 * an element has lost focus. |
|
31776 * |
|
31777 * Note: As the `blur` event is executed synchronously also during DOM manipulations |
|
31778 * (e.g. removing a focussed input), |
|
31779 * AngularJS executes the expression using `scope.$evalAsync` if the event is fired |
|
31780 * during an `$apply` to ensure a consistent state. |
|
31781 * |
|
31782 * @element window, input, select, textarea, a |
|
31783 * @priority 0 |
|
31784 * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon |
|
31785 * blur. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31786 * |
|
31787 * @example |
|
31788 * See {@link ng.directive:ngClick ngClick} |
|
31789 */ |
|
31790 |
|
31791 /** |
|
31792 * @ngdoc directive |
|
31793 * @name ngCopy |
|
31794 * |
|
31795 * @description |
|
31796 * Specify custom behavior on copy event. |
|
31797 * |
|
31798 * @element window, input, select, textarea, a |
|
31799 * @priority 0 |
|
31800 * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon |
|
31801 * copy. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31802 * |
|
31803 * @example |
|
31804 <example> |
|
31805 <file name="index.html"> |
|
31806 <input ng-copy="copied=true" ng-init="copied=false; value='copy me'" ng-model="value"> |
|
31807 copied: {{copied}} |
|
31808 </file> |
|
31809 </example> |
|
31810 */ |
|
31811 |
|
31812 /** |
|
31813 * @ngdoc directive |
|
31814 * @name ngCut |
|
31815 * |
|
31816 * @description |
|
31817 * Specify custom behavior on cut event. |
|
31818 * |
|
31819 * @element window, input, select, textarea, a |
|
31820 * @priority 0 |
|
31821 * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon |
|
31822 * cut. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31823 * |
|
31824 * @example |
|
31825 <example> |
|
31826 <file name="index.html"> |
|
31827 <input ng-cut="cut=true" ng-init="cut=false; value='cut me'" ng-model="value"> |
|
31828 cut: {{cut}} |
|
31829 </file> |
|
31830 </example> |
|
31831 */ |
|
31832 |
|
31833 /** |
|
31834 * @ngdoc directive |
|
31835 * @name ngPaste |
|
31836 * |
|
31837 * @description |
|
31838 * Specify custom behavior on paste event. |
|
31839 * |
|
31840 * @element window, input, select, textarea, a |
|
31841 * @priority 0 |
|
31842 * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon |
|
31843 * paste. ({@link guide/expression#-event- Event object is available as `$event`}) |
|
31844 * |
|
31845 * @example |
|
31846 <example> |
|
31847 <file name="index.html"> |
|
31848 <input ng-paste="paste=true" ng-init="paste=false" placeholder='paste here'> |
|
31849 pasted: {{paste}} |
|
31850 </file> |
|
31851 </example> |
|
31852 */ |
|
31853 |
|
31854 /** |
|
31855 * @ngdoc directive |
|
31856 * @name ngIf |
|
31857 * @restrict A |
|
31858 * |
|
31859 * @description |
|
31860 * The `ngIf` directive removes or recreates a portion of the DOM tree based on an |
|
31861 * {expression}. If the expression assigned to `ngIf` evaluates to a false |
|
31862 * value then the element is removed from the DOM, otherwise a clone of the |
|
31863 * element is reinserted into the DOM. |
|
31864 * |
|
31865 * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the |
|
31866 * element in the DOM rather than changing its visibility via the `display` css property. A common |
|
31867 * case when this difference is significant is when using css selectors that rely on an element's |
|
31868 * position within the DOM, such as the `:first-child` or `:last-child` pseudo-classes. |
|
31869 * |
|
31870 * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope |
|
31871 * is created when the element is restored. The scope created within `ngIf` inherits from |
|
31872 * its parent scope using |
|
31873 * [prototypal inheritance](https://github.com/angular/angular.js/wiki/Understanding-Scopes#javascript-prototypal-inheritance). |
|
31874 * An important implication of this is if `ngModel` is used within `ngIf` to bind to |
|
31875 * a javascript primitive defined in the parent scope. In this case any modifications made to the |
|
31876 * variable within the child scope will override (hide) the value in the parent scope. |
|
31877 * |
|
31878 * Also, `ngIf` recreates elements using their compiled state. An example of this behavior |
|
31879 * is if an element's class attribute is directly modified after it's compiled, using something like |
|
31880 * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element |
|
31881 * the added class will be lost because the original compiled state is used to regenerate the element. |
|
31882 * |
|
31883 * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter` |
|
31884 * and `leave` effects. |
|
31885 * |
|
31886 * @animations |
|
31887 * enter - happens just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container |
|
31888 * leave - happens just before the `ngIf` contents are removed from the DOM |
|
31889 * |
|
31890 * @element ANY |
|
31891 * @scope |
|
31892 * @priority 600 |
|
31893 * @param {expression} ngIf If the {@link guide/expression expression} is falsy then |
|
31894 * the element is removed from the DOM tree. If it is truthy a copy of the compiled |
|
31895 * element is added to the DOM tree. |
|
31896 * |
|
31897 * @example |
|
31898 <example module="ngAnimate" deps="angular-animate.js" animations="true"> |
|
31899 <file name="index.html"> |
|
31900 Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/> |
|
31901 Show when checked: |
|
31902 <span ng-if="checked" class="animate-if"> |
|
31903 I'm removed when the checkbox is unchecked. |
|
31904 </span> |
|
31905 </file> |
|
31906 <file name="animations.css"> |
|
31907 .animate-if { |
|
31908 background:white; |
|
31909 border:1px solid black; |
|
31910 padding:10px; |
|
31911 } |
|
31912 |
|
31913 .animate-if.ng-enter, .animate-if.ng-leave { |
|
31914 -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; |
|
31915 transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; |
|
31916 } |
|
31917 |
|
31918 .animate-if.ng-enter, |
|
31919 .animate-if.ng-leave.ng-leave-active { |
|
31920 opacity:0; |
|
31921 } |
|
31922 |
|
31923 .animate-if.ng-leave, |
|
31924 .animate-if.ng-enter.ng-enter-active { |
|
31925 opacity:1; |
|
31926 } |
|
31927 </file> |
|
31928 </example> |
|
31929 */ |
|
31930 var ngIfDirective = ['$animate', function($animate) { |
|
31931 return { |
|
31932 multiElement: true, |
|
31933 transclude: 'element', |
|
31934 priority: 600, |
|
31935 terminal: true, |
|
31936 restrict: 'A', |
|
31937 $$tlb: true, |
|
31938 link: function ($scope, $element, $attr, ctrl, $transclude) { |
|
31939 var block, childScope, previousElements; |
|
31940 $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { |
|
31941 |
|
31942 if (value) { |
|
31943 if (!childScope) { |
|
31944 $transclude(function (clone, newScope) { |
|
31945 childScope = newScope; |
|
31946 clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); |
|
31947 // Note: We only need the first/last node of the cloned nodes. |
|
31948 // However, we need to keep the reference to the jqlite wrapper as it might be changed later |
|
31949 // by a directive with templateUrl when its template arrives. |
|
31950 block = { |
|
31951 clone: clone |
|
31952 }; |
|
31953 $animate.enter(clone, $element.parent(), $element); |
|
31954 }); |
|
31955 } |
|
31956 } else { |
|
31957 if(previousElements) { |
|
31958 previousElements.remove(); |
|
31959 previousElements = null; |
|
31960 } |
|
31961 if(childScope) { |
|
31962 childScope.$destroy(); |
|
31963 childScope = null; |
|
31964 } |
|
31965 if(block) { |
|
31966 previousElements = getBlockNodes(block.clone); |
|
31967 $animate.leave(previousElements).then(function() { |
|
31968 previousElements = null; |
|
31969 }); |
|
31970 block = null; |
|
31971 } |
|
31972 } |
|
31973 }); |
|
31974 } |
|
31975 }; |
|
31976 }]; |
|
31977 |
|
31978 /** |
|
31979 * @ngdoc directive |
|
31980 * @name ngInclude |
|
31981 * @restrict ECA |
|
31982 * |
|
31983 * @description |
|
31984 * Fetches, compiles and includes an external HTML fragment. |
|
31985 * |
|
31986 * By default, the template URL is restricted to the same domain and protocol as the |
|
31987 * application document. This is done by calling {@link ng.$sce#getTrustedResourceUrl |
|
31988 * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols |
|
31989 * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or |
|
31990 * [wrap them](ng.$sce#trustAsResourceUrl) as trusted values. Refer to Angular's {@link |
|
31991 * ng.$sce Strict Contextual Escaping}. |
|
31992 * |
|
31993 * In addition, the browser's |
|
31994 * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest) |
|
31995 * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) |
|
31996 * policy may further restrict whether the template is successfully loaded. |
|
31997 * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://` |
|
31998 * access on some browsers. |
|
31999 * |
|
32000 * @animations |
|
32001 * enter - animation is used to bring new content into the browser. |
|
32002 * leave - animation is used to animate existing content away. |
|
32003 * |
|
32004 * The enter and leave animation occur concurrently. |
|
32005 * |
|
32006 * @scope |
|
32007 * @priority 400 |
|
32008 * |
|
32009 * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant, |
|
32010 * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`. |
|
32011 * @param {string=} onload Expression to evaluate when a new partial is loaded. |
|
32012 * |
|
32013 * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll |
|
32014 * $anchorScroll} to scroll the viewport after the content is loaded. |
|
32015 * |
|
32016 * - If the attribute is not set, disable scrolling. |
|
32017 * - If the attribute is set without value, enable scrolling. |
|
32018 * - Otherwise enable scrolling only if the expression evaluates to truthy value. |
|
32019 * |
|
32020 * @example |
|
32021 <example module="includeExample" deps="angular-animate.js" animations="true"> |
|
32022 <file name="index.html"> |
|
32023 <div ng-controller="ExampleController"> |
|
32024 <select ng-model="template" ng-options="t.name for t in templates"> |
|
32025 <option value="">(blank)</option> |
|
32026 </select> |
|
32027 url of the template: <tt>{{template.url}}</tt> |
|
32028 <hr/> |
|
32029 <div class="slide-animate-container"> |
|
32030 <div class="slide-animate" ng-include="template.url"></div> |
|
32031 </div> |
|
32032 </div> |
|
32033 </file> |
|
32034 <file name="script.js"> |
|
32035 angular.module('includeExample', ['ngAnimate']) |
|
32036 .controller('ExampleController', ['$scope', function($scope) { |
|
32037 $scope.templates = |
|
32038 [ { name: 'template1.html', url: 'template1.html'}, |
|
32039 { name: 'template2.html', url: 'template2.html'} ]; |
|
32040 $scope.template = $scope.templates[0]; |
|
32041 }]); |
|
32042 </file> |
|
32043 <file name="template1.html"> |
|
32044 Content of template1.html |
|
32045 </file> |
|
32046 <file name="template2.html"> |
|
32047 Content of template2.html |
|
32048 </file> |
|
32049 <file name="animations.css"> |
|
32050 .slide-animate-container { |
|
32051 position:relative; |
|
32052 background:white; |
|
32053 border:1px solid black; |
|
32054 height:40px; |
|
32055 overflow:hidden; |
|
32056 } |
|
32057 |
|
32058 .slide-animate { |
|
32059 padding:10px; |
|
32060 } |
|
32061 |
|
32062 .slide-animate.ng-enter, .slide-animate.ng-leave { |
|
32063 -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; |
|
32064 transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; |
|
32065 |
|
32066 position:absolute; |
|
32067 top:0; |
|
32068 left:0; |
|
32069 right:0; |
|
32070 bottom:0; |
|
32071 display:block; |
|
32072 padding:10px; |
|
32073 } |
|
32074 |
|
32075 .slide-animate.ng-enter { |
|
32076 top:-50px; |
|
32077 } |
|
32078 .slide-animate.ng-enter.ng-enter-active { |
|
32079 top:0; |
|
32080 } |
|
32081 |
|
32082 .slide-animate.ng-leave { |
|
32083 top:0; |
|
32084 } |
|
32085 .slide-animate.ng-leave.ng-leave-active { |
|
32086 top:50px; |
|
32087 } |
|
32088 </file> |
|
32089 <file name="protractor.js" type="protractor"> |
|
32090 var templateSelect = element(by.model('template')); |
|
32091 var includeElem = element(by.css('[ng-include]')); |
|
32092 |
|
32093 it('should load template1.html', function() { |
|
32094 expect(includeElem.getText()).toMatch(/Content of template1.html/); |
|
32095 }); |
|
32096 |
|
32097 it('should load template2.html', function() { |
|
32098 if (browser.params.browser == 'firefox') { |
|
32099 // Firefox can't handle using selects |
|
32100 // See https://github.com/angular/protractor/issues/480 |
|
32101 return; |
|
32102 } |
|
32103 templateSelect.click(); |
|
32104 templateSelect.all(by.css('option')).get(2).click(); |
|
32105 expect(includeElem.getText()).toMatch(/Content of template2.html/); |
|
32106 }); |
|
32107 |
|
32108 it('should change to blank', function() { |
|
32109 if (browser.params.browser == 'firefox') { |
|
32110 // Firefox can't handle using selects |
|
32111 return; |
|
32112 } |
|
32113 templateSelect.click(); |
|
32114 templateSelect.all(by.css('option')).get(0).click(); |
|
32115 expect(includeElem.isPresent()).toBe(false); |
|
32116 }); |
|
32117 </file> |
|
32118 </example> |
|
32119 */ |
|
32120 |
|
32121 |
|
32122 /** |
|
32123 * @ngdoc event |
|
32124 * @name ngInclude#$includeContentRequested |
|
32125 * @eventType emit on the scope ngInclude was declared in |
|
32126 * @description |
|
32127 * Emitted every time the ngInclude content is requested. |
|
32128 * |
|
32129 * @param {Object} angularEvent Synthetic event object. |
|
32130 * @param {String} src URL of content to load. |
|
32131 */ |
|
32132 |
|
32133 |
|
32134 /** |
|
32135 * @ngdoc event |
|
32136 * @name ngInclude#$includeContentLoaded |
|
32137 * @eventType emit on the current ngInclude scope |
|
32138 * @description |
|
32139 * Emitted every time the ngInclude content is reloaded. |
|
32140 * |
|
32141 * @param {Object} angularEvent Synthetic event object. |
|
32142 * @param {String} src URL of content to load. |
|
32143 */ |
|
32144 |
|
32145 |
|
32146 /** |
|
32147 * @ngdoc event |
|
32148 * @name ngInclude#$includeContentError |
|
32149 * @eventType emit on the scope ngInclude was declared in |
|
32150 * @description |
|
32151 * Emitted when a template HTTP request yields an erronous response (status < 200 || status > 299) |
|
32152 * |
|
32153 * @param {Object} angularEvent Synthetic event object. |
|
32154 * @param {String} src URL of content to load. |
|
32155 */ |
|
32156 var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', '$sce', |
|
32157 function($templateRequest, $anchorScroll, $animate, $sce) { |
|
32158 return { |
|
32159 restrict: 'ECA', |
|
32160 priority: 400, |
|
32161 terminal: true, |
|
32162 transclude: 'element', |
|
32163 controller: angular.noop, |
|
32164 compile: function(element, attr) { |
|
32165 var srcExp = attr.ngInclude || attr.src, |
|
32166 onloadExp = attr.onload || '', |
|
32167 autoScrollExp = attr.autoscroll; |
|
32168 |
|
32169 return function(scope, $element, $attr, ctrl, $transclude) { |
|
32170 var changeCounter = 0, |
|
32171 currentScope, |
|
32172 previousElement, |
|
32173 currentElement; |
|
32174 |
|
32175 var cleanupLastIncludeContent = function() { |
|
32176 if(previousElement) { |
|
32177 previousElement.remove(); |
|
32178 previousElement = null; |
|
32179 } |
|
32180 if(currentScope) { |
|
32181 currentScope.$destroy(); |
|
32182 currentScope = null; |
|
32183 } |
|
32184 if(currentElement) { |
|
32185 $animate.leave(currentElement).then(function() { |
|
32186 previousElement = null; |
|
32187 }); |
|
32188 previousElement = currentElement; |
|
32189 currentElement = null; |
|
32190 } |
|
32191 }; |
|
32192 |
|
32193 scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) { |
|
32194 var afterAnimation = function() { |
|
32195 if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { |
|
32196 $anchorScroll(); |
|
32197 } |
|
32198 }; |
|
32199 var thisChangeId = ++changeCounter; |
|
32200 |
|
32201 if (src) { |
|
32202 //set the 2nd param to true to ignore the template request error so that the inner |
|
32203 //contents and scope can be cleaned up. |
|
32204 $templateRequest(src, true).then(function(response) { |
|
32205 if (thisChangeId !== changeCounter) return; |
|
32206 var newScope = scope.$new(); |
|
32207 ctrl.template = response; |
|
32208 |
|
32209 // Note: This will also link all children of ng-include that were contained in the original |
|
32210 // html. If that content contains controllers, ... they could pollute/change the scope. |
|
32211 // However, using ng-include on an element with additional content does not make sense... |
|
32212 // Note: We can't remove them in the cloneAttchFn of $transclude as that |
|
32213 // function is called before linking the content, which would apply child |
|
32214 // directives to non existing elements. |
|
32215 var clone = $transclude(newScope, function(clone) { |
|
32216 cleanupLastIncludeContent(); |
|
32217 $animate.enter(clone, null, $element).then(afterAnimation); |
|
32218 }); |
|
32219 |
|
32220 currentScope = newScope; |
|
32221 currentElement = clone; |
|
32222 |
|
32223 currentScope.$emit('$includeContentLoaded', src); |
|
32224 scope.$eval(onloadExp); |
|
32225 }, function() { |
|
32226 if (thisChangeId === changeCounter) { |
|
32227 cleanupLastIncludeContent(); |
|
32228 scope.$emit('$includeContentError', src); |
|
32229 } |
|
32230 }); |
|
32231 scope.$emit('$includeContentRequested', src); |
|
32232 } else { |
|
32233 cleanupLastIncludeContent(); |
|
32234 ctrl.template = null; |
|
32235 } |
|
32236 }); |
|
32237 }; |
|
32238 } |
|
32239 }; |
|
32240 }]; |
|
32241 |
|
32242 // This directive is called during the $transclude call of the first `ngInclude` directive. |
|
32243 // It will replace and compile the content of the element with the loaded template. |
|
32244 // We need this directive so that the element content is already filled when |
|
32245 // the link function of another directive on the same element as ngInclude |
|
32246 // is called. |
|
32247 var ngIncludeFillContentDirective = ['$compile', |
|
32248 function($compile) { |
|
32249 return { |
|
32250 restrict: 'ECA', |
|
32251 priority: -400, |
|
32252 require: 'ngInclude', |
|
32253 link: function(scope, $element, $attr, ctrl) { |
|
32254 if (/SVG/.test($element[0].toString())) { |
|
32255 // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not |
|
32256 // support innerHTML, so detect this here and try to generate the contents |
|
32257 // specially. |
|
32258 $element.empty(); |
|
32259 $compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope, |
|
32260 function namespaceAdaptedClone(clone) { |
|
32261 $element.append(clone); |
|
32262 }, undefined, undefined, $element); |
|
32263 return; |
|
32264 } |
|
32265 |
|
32266 $element.html(ctrl.template); |
|
32267 $compile($element.contents())(scope); |
|
32268 } |
|
32269 }; |
|
32270 }]; |
|
32271 |
|
32272 /** |
|
32273 * @ngdoc directive |
|
32274 * @name ngInit |
|
32275 * @restrict AC |
|
32276 * |
|
32277 * @description |
|
32278 * The `ngInit` directive allows you to evaluate an expression in the |
|
32279 * current scope. |
|
32280 * |
|
32281 * <div class="alert alert-error"> |
|
32282 * The only appropriate use of `ngInit` is for aliasing special properties of |
|
32283 * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you |
|
32284 * should use {@link guide/controller controllers} rather than `ngInit` |
|
32285 * to initialize values on a scope. |
|
32286 * </div> |
|
32287 * <div class="alert alert-warning"> |
|
32288 * **Note**: If you have assignment in `ngInit` along with {@link ng.$filter `$filter`}, make |
|
32289 * sure you have parenthesis for correct precedence: |
|
32290 * <pre class="prettyprint"> |
|
32291 * <div ng-init="test1 = (data | orderBy:'name')"></div> |
|
32292 * </pre> |
|
32293 * </div> |
|
32294 * |
|
32295 * @priority 450 |
|
32296 * |
|
32297 * @element ANY |
|
32298 * @param {expression} ngInit {@link guide/expression Expression} to eval. |
|
32299 * |
|
32300 * @example |
|
32301 <example module="initExample"> |
|
32302 <file name="index.html"> |
|
32303 <script> |
|
32304 angular.module('initExample', []) |
|
32305 .controller('ExampleController', ['$scope', function($scope) { |
|
32306 $scope.list = [['a', 'b'], ['c', 'd']]; |
|
32307 }]); |
|
32308 </script> |
|
32309 <div ng-controller="ExampleController"> |
|
32310 <div ng-repeat="innerList in list" ng-init="outerIndex = $index"> |
|
32311 <div ng-repeat="value in innerList" ng-init="innerIndex = $index"> |
|
32312 <span class="example-init">list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};</span> |
|
32313 </div> |
|
32314 </div> |
|
32315 </div> |
|
32316 </file> |
|
32317 <file name="protractor.js" type="protractor"> |
|
32318 it('should alias index positions', function() { |
|
32319 var elements = element.all(by.css('.example-init')); |
|
32320 expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;'); |
|
32321 expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;'); |
|
32322 expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;'); |
|
32323 expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;'); |
|
32324 }); |
|
32325 </file> |
|
32326 </example> |
|
32327 */ |
|
32328 var ngInitDirective = ngDirective({ |
|
32329 priority: 450, |
|
32330 compile: function() { |
|
32331 return { |
|
32332 pre: function(scope, element, attrs) { |
|
32333 scope.$eval(attrs.ngInit); |
|
32334 } |
|
32335 }; |
|
32336 } |
|
32337 }); |
|
32338 |
|
32339 /** |
|
32340 * @ngdoc directive |
|
32341 * @name ngNonBindable |
|
32342 * @restrict AC |
|
32343 * @priority 1000 |
|
32344 * |
|
32345 * @description |
|
32346 * The `ngNonBindable` directive tells Angular not to compile or bind the contents of the current |
|
32347 * DOM element. This is useful if the element contains what appears to be Angular directives and |
|
32348 * bindings but which should be ignored by Angular. This could be the case if you have a site that |
|
32349 * displays snippets of code, for instance. |
|
32350 * |
|
32351 * @element ANY |
|
32352 * |
|
32353 * @example |
|
32354 * In this example there are two locations where a simple interpolation binding (`{{}}`) is present, |
|
32355 * but the one wrapped in `ngNonBindable` is left alone. |
|
32356 * |
|
32357 * @example |
|
32358 <example> |
|
32359 <file name="index.html"> |
|
32360 <div>Normal: {{1 + 2}}</div> |
|
32361 <div ng-non-bindable>Ignored: {{1 + 2}}</div> |
|
32362 </file> |
|
32363 <file name="protractor.js" type="protractor"> |
|
32364 it('should check ng-non-bindable', function() { |
|
32365 expect(element(by.binding('1 + 2')).getText()).toContain('3'); |
|
32366 expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/); |
|
32367 }); |
|
32368 </file> |
|
32369 </example> |
|
32370 */ |
|
32371 var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); |
|
32372 |
|
32373 /** |
|
32374 * @ngdoc directive |
|
32375 * @name ngPluralize |
|
32376 * @restrict EA |
|
32377 * |
|
32378 * @description |
|
32379 * `ngPluralize` is a directive that displays messages according to en-US localization rules. |
|
32380 * These rules are bundled with angular.js, but can be overridden |
|
32381 * (see {@link guide/i18n Angular i18n} dev guide). You configure ngPluralize directive |
|
32382 * by specifying the mappings between |
|
32383 * [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html) |
|
32384 * and the strings to be displayed. |
|
32385 * |
|
32386 * # Plural categories and explicit number rules |
|
32387 * There are two |
|
32388 * [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html) |
|
32389 * in Angular's default en-US locale: "one" and "other". |
|
32390 * |
|
32391 * While a plural category may match many numbers (for example, in en-US locale, "other" can match |
|
32392 * any number that is not 1), an explicit number rule can only match one number. For example, the |
|
32393 * explicit number rule for "3" matches the number 3. There are examples of plural categories |
|
32394 * and explicit number rules throughout the rest of this documentation. |
|
32395 * |
|
32396 * # Configuring ngPluralize |
|
32397 * You configure ngPluralize by providing 2 attributes: `count` and `when`. |
|
32398 * You can also provide an optional attribute, `offset`. |
|
32399 * |
|
32400 * The value of the `count` attribute can be either a string or an {@link guide/expression |
|
32401 * Angular expression}; these are evaluated on the current scope for its bound value. |
|
32402 * |
|
32403 * The `when` attribute specifies the mappings between plural categories and the actual |
|
32404 * string to be displayed. The value of the attribute should be a JSON object. |
|
32405 * |
|
32406 * The following example shows how to configure ngPluralize: |
|
32407 * |
|
32408 * ```html |
|
32409 * <ng-pluralize count="personCount" |
|
32410 when="{'0': 'Nobody is viewing.', |
|
32411 * 'one': '1 person is viewing.', |
|
32412 * 'other': '{} people are viewing.'}"> |
|
32413 * </ng-pluralize> |
|
32414 *``` |
|
32415 * |
|
32416 * In the example, `"0: Nobody is viewing."` is an explicit number rule. If you did not |
|
32417 * specify this rule, 0 would be matched to the "other" category and "0 people are viewing" |
|
32418 * would be shown instead of "Nobody is viewing". You can specify an explicit number rule for |
|
32419 * other numbers, for example 12, so that instead of showing "12 people are viewing", you can |
|
32420 * show "a dozen people are viewing". |
|
32421 * |
|
32422 * You can use a set of closed braces (`{}`) as a placeholder for the number that you want substituted |
|
32423 * into pluralized strings. In the previous example, Angular will replace `{}` with |
|
32424 * <span ng-non-bindable>`{{personCount}}`</span>. The closed braces `{}` is a placeholder |
|
32425 * for <span ng-non-bindable>{{numberExpression}}</span>. |
|
32426 * |
|
32427 * # Configuring ngPluralize with offset |
|
32428 * The `offset` attribute allows further customization of pluralized text, which can result in |
|
32429 * a better user experience. For example, instead of the message "4 people are viewing this document", |
|
32430 * you might display "John, Kate and 2 others are viewing this document". |
|
32431 * The offset attribute allows you to offset a number by any desired value. |
|
32432 * Let's take a look at an example: |
|
32433 * |
|
32434 * ```html |
|
32435 * <ng-pluralize count="personCount" offset=2 |
|
32436 * when="{'0': 'Nobody is viewing.', |
|
32437 * '1': '{{person1}} is viewing.', |
|
32438 * '2': '{{person1}} and {{person2}} are viewing.', |
|
32439 * 'one': '{{person1}}, {{person2}} and one other person are viewing.', |
|
32440 * 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}"> |
|
32441 * </ng-pluralize> |
|
32442 * ``` |
|
32443 * |
|
32444 * Notice that we are still using two plural categories(one, other), but we added |
|
32445 * three explicit number rules 0, 1 and 2. |
|
32446 * When one person, perhaps John, views the document, "John is viewing" will be shown. |
|
32447 * When three people view the document, no explicit number rule is found, so |
|
32448 * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category. |
|
32449 * In this case, plural category 'one' is matched and "John, Mary and one other person are viewing" |
|
32450 * is shown. |
|
32451 * |
|
32452 * Note that when you specify offsets, you must provide explicit number rules for |
|
32453 * numbers from 0 up to and including the offset. If you use an offset of 3, for example, |
|
32454 * you must provide explicit number rules for 0, 1, 2 and 3. You must also provide plural strings for |
|
32455 * plural categories "one" and "other". |
|
32456 * |
|
32457 * @param {string|expression} count The variable to be bound to. |
|
32458 * @param {string} when The mapping between plural category to its corresponding strings. |
|
32459 * @param {number=} offset Offset to deduct from the total number. |
|
32460 * |
|
32461 * @example |
|
32462 <example module="pluralizeExample"> |
|
32463 <file name="index.html"> |
|
32464 <script> |
|
32465 angular.module('pluralizeExample', []) |
|
32466 .controller('ExampleController', ['$scope', function($scope) { |
|
32467 $scope.person1 = 'Igor'; |
|
32468 $scope.person2 = 'Misko'; |
|
32469 $scope.personCount = 1; |
|
32470 }]); |
|
32471 </script> |
|
32472 <div ng-controller="ExampleController"> |
|
32473 Person 1:<input type="text" ng-model="person1" value="Igor" /><br/> |
|
32474 Person 2:<input type="text" ng-model="person2" value="Misko" /><br/> |
|
32475 Number of People:<input type="text" ng-model="personCount" value="1" /><br/> |
|
32476 |
|
32477 <!--- Example with simple pluralization rules for en locale ---> |
|
32478 Without Offset: |
|
32479 <ng-pluralize count="personCount" |
|
32480 when="{'0': 'Nobody is viewing.', |
|
32481 'one': '1 person is viewing.', |
|
32482 'other': '{} people are viewing.'}"> |
|
32483 </ng-pluralize><br> |
|
32484 |
|
32485 <!--- Example with offset ---> |
|
32486 With Offset(2): |
|
32487 <ng-pluralize count="personCount" offset=2 |
|
32488 when="{'0': 'Nobody is viewing.', |
|
32489 '1': '{{person1}} is viewing.', |
|
32490 '2': '{{person1}} and {{person2}} are viewing.', |
|
32491 'one': '{{person1}}, {{person2}} and one other person are viewing.', |
|
32492 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}"> |
|
32493 </ng-pluralize> |
|
32494 </div> |
|
32495 </file> |
|
32496 <file name="protractor.js" type="protractor"> |
|
32497 it('should show correct pluralized string', function() { |
|
32498 var withoutOffset = element.all(by.css('ng-pluralize')).get(0); |
|
32499 var withOffset = element.all(by.css('ng-pluralize')).get(1); |
|
32500 var countInput = element(by.model('personCount')); |
|
32501 |
|
32502 expect(withoutOffset.getText()).toEqual('1 person is viewing.'); |
|
32503 expect(withOffset.getText()).toEqual('Igor is viewing.'); |
|
32504 |
|
32505 countInput.clear(); |
|
32506 countInput.sendKeys('0'); |
|
32507 |
|
32508 expect(withoutOffset.getText()).toEqual('Nobody is viewing.'); |
|
32509 expect(withOffset.getText()).toEqual('Nobody is viewing.'); |
|
32510 |
|
32511 countInput.clear(); |
|
32512 countInput.sendKeys('2'); |
|
32513 |
|
32514 expect(withoutOffset.getText()).toEqual('2 people are viewing.'); |
|
32515 expect(withOffset.getText()).toEqual('Igor and Misko are viewing.'); |
|
32516 |
|
32517 countInput.clear(); |
|
32518 countInput.sendKeys('3'); |
|
32519 |
|
32520 expect(withoutOffset.getText()).toEqual('3 people are viewing.'); |
|
32521 expect(withOffset.getText()).toEqual('Igor, Misko and one other person are viewing.'); |
|
32522 |
|
32523 countInput.clear(); |
|
32524 countInput.sendKeys('4'); |
|
32525 |
|
32526 expect(withoutOffset.getText()).toEqual('4 people are viewing.'); |
|
32527 expect(withOffset.getText()).toEqual('Igor, Misko and 2 other people are viewing.'); |
|
32528 }); |
|
32529 it('should show data-bound names', function() { |
|
32530 var withOffset = element.all(by.css('ng-pluralize')).get(1); |
|
32531 var personCount = element(by.model('personCount')); |
|
32532 var person1 = element(by.model('person1')); |
|
32533 var person2 = element(by.model('person2')); |
|
32534 personCount.clear(); |
|
32535 personCount.sendKeys('4'); |
|
32536 person1.clear(); |
|
32537 person1.sendKeys('Di'); |
|
32538 person2.clear(); |
|
32539 person2.sendKeys('Vojta'); |
|
32540 expect(withOffset.getText()).toEqual('Di, Vojta and 2 other people are viewing.'); |
|
32541 }); |
|
32542 </file> |
|
32543 </example> |
|
32544 */ |
|
32545 var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) { |
|
32546 var BRACE = /{}/g; |
|
32547 return { |
|
32548 restrict: 'EA', |
|
32549 link: function(scope, element, attr) { |
|
32550 var numberExp = attr.count, |
|
32551 whenExp = attr.$attr.when && element.attr(attr.$attr.when), // we have {{}} in attrs |
|
32552 offset = attr.offset || 0, |
|
32553 whens = scope.$eval(whenExp) || {}, |
|
32554 whensExpFns = {}, |
|
32555 startSymbol = $interpolate.startSymbol(), |
|
32556 endSymbol = $interpolate.endSymbol(), |
|
32557 isWhen = /^when(Minus)?(.+)$/; |
|
32558 |
|
32559 forEach(attr, function(expression, attributeName) { |
|
32560 if (isWhen.test(attributeName)) { |
|
32561 whens[lowercase(attributeName.replace('when', '').replace('Minus', '-'))] = |
|
32562 element.attr(attr.$attr[attributeName]); |
|
32563 } |
|
32564 }); |
|
32565 forEach(whens, function(expression, key) { |
|
32566 whensExpFns[key] = |
|
32567 $interpolate(expression.replace(BRACE, startSymbol + numberExp + '-' + |
|
32568 offset + endSymbol)); |
|
32569 }); |
|
32570 |
|
32571 scope.$watch(function ngPluralizeWatch() { |
|
32572 var value = parseFloat(scope.$eval(numberExp)); |
|
32573 |
|
32574 if (!isNaN(value)) { |
|
32575 //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise, |
|
32576 //check it against pluralization rules in $locale service |
|
32577 if (!(value in whens)) value = $locale.pluralCat(value - offset); |
|
32578 return whensExpFns[value](scope); |
|
32579 } else { |
|
32580 return ''; |
|
32581 } |
|
32582 }, function ngPluralizeWatchAction(newVal) { |
|
32583 element.text(newVal); |
|
32584 }); |
|
32585 } |
|
32586 }; |
|
32587 }]; |
|
32588 |
|
32589 /** |
|
32590 * @ngdoc directive |
|
32591 * @name ngRepeat |
|
32592 * |
|
32593 * @description |
|
32594 * The `ngRepeat` directive instantiates a template once per item from a collection. Each template |
|
32595 * instance gets its own scope, where the given loop variable is set to the current collection item, |
|
32596 * and `$index` is set to the item index or key. |
|
32597 * |
|
32598 * Special properties are exposed on the local scope of each template instance, including: |
|
32599 * |
|
32600 * | Variable | Type | Details | |
|
32601 * |-----------|-----------------|-----------------------------------------------------------------------------| |
|
32602 * | `$index` | {@type number} | iterator offset of the repeated element (0..length-1) | |
|
32603 * | `$first` | {@type boolean} | true if the repeated element is first in the iterator. | |
|
32604 * | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. | |
|
32605 * | `$last` | {@type boolean} | true if the repeated element is last in the iterator. | |
|
32606 * | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). | |
|
32607 * | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). | |
|
32608 * |
|
32609 * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}. |
|
32610 * This may be useful when, for instance, nesting ngRepeats. |
|
32611 * |
|
32612 * # Special repeat start and end points |
|
32613 * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending |
|
32614 * the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively. |
|
32615 * The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on) |
|
32616 * up to and including the ending HTML tag where **ng-repeat-end** is placed. |
|
32617 * |
|
32618 * The example below makes use of this feature: |
|
32619 * ```html |
|
32620 * <header ng-repeat-start="item in items"> |
|
32621 * Header {{ item }} |
|
32622 * </header> |
|
32623 * <div class="body"> |
|
32624 * Body {{ item }} |
|
32625 * </div> |
|
32626 * <footer ng-repeat-end> |
|
32627 * Footer {{ item }} |
|
32628 * </footer> |
|
32629 * ``` |
|
32630 * |
|
32631 * And with an input of {@type ['A','B']} for the items variable in the example above, the output will evaluate to: |
|
32632 * ```html |
|
32633 * <header> |
|
32634 * Header A |
|
32635 * </header> |
|
32636 * <div class="body"> |
|
32637 * Body A |
|
32638 * </div> |
|
32639 * <footer> |
|
32640 * Footer A |
|
32641 * </footer> |
|
32642 * <header> |
|
32643 * Header B |
|
32644 * </header> |
|
32645 * <div class="body"> |
|
32646 * Body B |
|
32647 * </div> |
|
32648 * <footer> |
|
32649 * Footer B |
|
32650 * </footer> |
|
32651 * ``` |
|
32652 * |
|
32653 * The custom start and end points for ngRepeat also support all other HTML directive syntax flavors provided in AngularJS (such |
|
32654 * as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**). |
|
32655 * |
|
32656 * @animations |
|
32657 * **.enter** - when a new item is added to the list or when an item is revealed after a filter |
|
32658 * |
|
32659 * **.leave** - when an item is removed from the list or when an item is filtered out |
|
32660 * |
|
32661 * **.move** - when an adjacent item is filtered out causing a reorder or when the item contents are reordered |
|
32662 * |
|
32663 * @element ANY |
|
32664 * @scope |
|
32665 * @priority 1000 |
|
32666 * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These |
|
32667 * formats are currently supported: |
|
32668 * |
|
32669 * * `variable in expression` – where variable is the user defined loop variable and `expression` |
|
32670 * is a scope expression giving the collection to enumerate. |
|
32671 * |
|
32672 * For example: `album in artist.albums`. |
|
32673 * |
|
32674 * * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers, |
|
32675 * and `expression` is the scope expression giving the collection to enumerate. |
|
32676 * |
|
32677 * For example: `(name, age) in {'adam':10, 'amalie':12}`. |
|
32678 * |
|
32679 * * `variable in expression track by tracking_expression` – You can also provide an optional tracking function |
|
32680 * which can be used to associate the objects in the collection with the DOM elements. If no tracking function |
|
32681 * is specified the ng-repeat associates elements by identity in the collection. It is an error to have |
|
32682 * more than one tracking function to resolve to the same key. (This would mean that two distinct objects are |
|
32683 * mapped to the same DOM element, which is not possible.) Filters should be applied to the expression, |
|
32684 * before specifying a tracking expression. |
|
32685 * |
|
32686 * For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements |
|
32687 * will be associated by item identity in the array. |
|
32688 * |
|
32689 * * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the |
|
32690 * intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message |
|
32691 * when a filter is active on the repeater, but the filtered result set is empty. |
|
32692 * |
|
32693 * For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after |
|
32694 * the items have been processed through the filter. |
|
32695 * |
|
32696 * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique |
|
32697 * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements |
|
32698 * with the corresponding item in the array by identity. Moving the same object in array would move the DOM |
|
32699 * element in the same way in the DOM. |
|
32700 * |
|
32701 * For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this |
|
32702 * case the object identity does not matter. Two objects are considered equivalent as long as their `id` |
|
32703 * property is same. |
|
32704 * |
|
32705 * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter |
|
32706 * to items in conjunction with a tracking expression. |
|
32707 * |
|
32708 * @example |
|
32709 * This example initializes the scope to a list of names and |
|
32710 * then uses `ngRepeat` to display every person: |
|
32711 <example module="ngAnimate" deps="angular-animate.js" animations="true"> |
|
32712 <file name="index.html"> |
|
32713 <div ng-init="friends = [ |
|
32714 {name:'John', age:25, gender:'boy'}, |
|
32715 {name:'Jessie', age:30, gender:'girl'}, |
|
32716 {name:'Johanna', age:28, gender:'girl'}, |
|
32717 {name:'Joy', age:15, gender:'girl'}, |
|
32718 {name:'Mary', age:28, gender:'girl'}, |
|
32719 {name:'Peter', age:95, gender:'boy'}, |
|
32720 {name:'Sebastian', age:50, gender:'boy'}, |
|
32721 {name:'Erika', age:27, gender:'girl'}, |
|
32722 {name:'Patrick', age:40, gender:'boy'}, |
|
32723 {name:'Samantha', age:60, gender:'girl'} |
|
32724 ]"> |
|
32725 I have {{friends.length}} friends. They are: |
|
32726 <input type="search" ng-model="q" placeholder="filter friends..." /> |
|
32727 <ul class="example-animate-container"> |
|
32728 <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results"> |
|
32729 [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. |
|
32730 </li> |
|
32731 <li class="animate-repeat" ng-if="results.length == 0"> |
|
32732 <strong>No results found...</strong> |
|
32733 </li> |
|
32734 </ul> |
|
32735 </div> |
|
32736 </file> |
|
32737 <file name="animations.css"> |
|
32738 .example-animate-container { |
|
32739 background:white; |
|
32740 border:1px solid black; |
|
32741 list-style:none; |
|
32742 margin:0; |
|
32743 padding:0 10px; |
|
32744 } |
|
32745 |
|
32746 .animate-repeat { |
|
32747 line-height:40px; |
|
32748 list-style:none; |
|
32749 box-sizing:border-box; |
|
32750 } |
|
32751 |
|
32752 .animate-repeat.ng-move, |
|
32753 .animate-repeat.ng-enter, |
|
32754 .animate-repeat.ng-leave { |
|
32755 -webkit-transition:all linear 0.5s; |
|
32756 transition:all linear 0.5s; |
|
32757 } |
|
32758 |
|
32759 .animate-repeat.ng-leave.ng-leave-active, |
|
32760 .animate-repeat.ng-move, |
|
32761 .animate-repeat.ng-enter { |
|
32762 opacity:0; |
|
32763 max-height:0; |
|
32764 } |
|
32765 |
|
32766 .animate-repeat.ng-leave, |
|
32767 .animate-repeat.ng-move.ng-move-active, |
|
32768 .animate-repeat.ng-enter.ng-enter-active { |
|
32769 opacity:1; |
|
32770 max-height:40px; |
|
32771 } |
|
32772 </file> |
|
32773 <file name="protractor.js" type="protractor"> |
|
32774 var friends = element.all(by.repeater('friend in friends')); |
|
32775 |
|
32776 it('should render initial data set', function() { |
|
32777 expect(friends.count()).toBe(10); |
|
32778 expect(friends.get(0).getText()).toEqual('[1] John who is 25 years old.'); |
|
32779 expect(friends.get(1).getText()).toEqual('[2] Jessie who is 30 years old.'); |
|
32780 expect(friends.last().getText()).toEqual('[10] Samantha who is 60 years old.'); |
|
32781 expect(element(by.binding('friends.length')).getText()) |
|
32782 .toMatch("I have 10 friends. They are:"); |
|
32783 }); |
|
32784 |
|
32785 it('should update repeater when filter predicate changes', function() { |
|
32786 expect(friends.count()).toBe(10); |
|
32787 |
|
32788 element(by.model('q')).sendKeys('ma'); |
|
32789 |
|
32790 expect(friends.count()).toBe(2); |
|
32791 expect(friends.get(0).getText()).toEqual('[1] Mary who is 28 years old.'); |
|
32792 expect(friends.last().getText()).toEqual('[2] Samantha who is 60 years old.'); |
|
32793 }); |
|
32794 </file> |
|
32795 </example> |
|
32796 */ |
|
32797 var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { |
|
32798 var NG_REMOVED = '$$NG_REMOVED'; |
|
32799 var ngRepeatMinErr = minErr('ngRepeat'); |
|
32800 |
|
32801 var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) { |
|
32802 // TODO(perf): generate setters to shave off ~40ms or 1-1.5% |
|
32803 scope[valueIdentifier] = value; |
|
32804 if (keyIdentifier) scope[keyIdentifier] = key; |
|
32805 scope.$index = index; |
|
32806 scope.$first = (index === 0); |
|
32807 scope.$last = (index === (arrayLength - 1)); |
|
32808 scope.$middle = !(scope.$first || scope.$last); |
|
32809 // jshint bitwise: false |
|
32810 scope.$odd = !(scope.$even = (index&1) === 0); |
|
32811 // jshint bitwise: true |
|
32812 }; |
|
32813 |
|
32814 var getBlockStart = function(block) { |
|
32815 return block.clone[0]; |
|
32816 }; |
|
32817 |
|
32818 var getBlockEnd = function(block) { |
|
32819 return block.clone[block.clone.length - 1]; |
|
32820 }; |
|
32821 |
|
32822 |
|
32823 return { |
|
32824 restrict: 'A', |
|
32825 multiElement: true, |
|
32826 transclude: 'element', |
|
32827 priority: 1000, |
|
32828 terminal: true, |
|
32829 $$tlb: true, |
|
32830 compile: function ngRepeatCompile($element, $attr) { |
|
32831 var expression = $attr.ngRepeat; |
|
32832 var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); |
|
32833 |
|
32834 var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); |
|
32835 |
|
32836 if (!match) { |
|
32837 throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", |
|
32838 expression); |
|
32839 } |
|
32840 |
|
32841 var lhs = match[1]; |
|
32842 var rhs = match[2]; |
|
32843 var aliasAs = match[3]; |
|
32844 var trackByExp = match[4]; |
|
32845 |
|
32846 match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); |
|
32847 |
|
32848 if (!match) { |
|
32849 throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", |
|
32850 lhs); |
|
32851 } |
|
32852 var valueIdentifier = match[3] || match[1]; |
|
32853 var keyIdentifier = match[2]; |
|
32854 |
|
32855 if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) || |
|
32856 /^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent)$/.test(aliasAs))) { |
|
32857 throw ngRepeatMinErr('badident', "alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.", |
|
32858 aliasAs); |
|
32859 } |
|
32860 |
|
32861 var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn; |
|
32862 var hashFnLocals = {$id: hashKey}; |
|
32863 |
|
32864 if (trackByExp) { |
|
32865 trackByExpGetter = $parse(trackByExp); |
|
32866 } else { |
|
32867 trackByIdArrayFn = function (key, value) { |
|
32868 return hashKey(value); |
|
32869 }; |
|
32870 trackByIdObjFn = function (key) { |
|
32871 return key; |
|
32872 }; |
|
32873 } |
|
32874 |
|
32875 return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) { |
|
32876 |
|
32877 if (trackByExpGetter) { |
|
32878 trackByIdExpFn = function(key, value, index) { |
|
32879 // assign key, value, and $index to the locals so that they can be used in hash functions |
|
32880 if (keyIdentifier) hashFnLocals[keyIdentifier] = key; |
|
32881 hashFnLocals[valueIdentifier] = value; |
|
32882 hashFnLocals.$index = index; |
|
32883 return trackByExpGetter($scope, hashFnLocals); |
|
32884 }; |
|
32885 } |
|
32886 |
|
32887 // Store a list of elements from previous run. This is a hash where key is the item from the |
|
32888 // iterator, and the value is objects with following properties. |
|
32889 // - scope: bound scope |
|
32890 // - element: previous element. |
|
32891 // - index: position |
|
32892 // |
|
32893 // We are using no-proto object so that we don't need to guard against inherited props via |
|
32894 // hasOwnProperty. |
|
32895 var lastBlockMap = createMap(); |
|
32896 |
|
32897 //watch props |
|
32898 $scope.$watchCollection(rhs, function ngRepeatAction(collection) { |
|
32899 var index, length, |
|
32900 previousNode = $element[0], // node that cloned nodes should be inserted after |
|
32901 // initialized to the comment node anchor |
|
32902 nextNode, |
|
32903 // Same as lastBlockMap but it has the current state. It will become the |
|
32904 // lastBlockMap on the next iteration. |
|
32905 nextBlockMap = createMap(), |
|
32906 collectionLength, |
|
32907 key, value, // key/value of iteration |
|
32908 trackById, |
|
32909 trackByIdFn, |
|
32910 collectionKeys, |
|
32911 block, // last object information {scope, element, id} |
|
32912 nextBlockOrder, |
|
32913 elementsToRemove; |
|
32914 |
|
32915 if (aliasAs) { |
|
32916 $scope[aliasAs] = collection; |
|
32917 } |
|
32918 |
|
32919 if (isArrayLike(collection)) { |
|
32920 collectionKeys = collection; |
|
32921 trackByIdFn = trackByIdExpFn || trackByIdArrayFn; |
|
32922 } else { |
|
32923 trackByIdFn = trackByIdExpFn || trackByIdObjFn; |
|
32924 // if object, extract keys, sort them and use to determine order of iteration over obj props |
|
32925 collectionKeys = []; |
|
32926 for (var itemKey in collection) { |
|
32927 if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) != '$') { |
|
32928 collectionKeys.push(itemKey); |
|
32929 } |
|
32930 } |
|
32931 collectionKeys.sort(); |
|
32932 } |
|
32933 |
|
32934 collectionLength = collectionKeys.length; |
|
32935 nextBlockOrder = new Array(collectionLength); |
|
32936 |
|
32937 // locate existing items |
|
32938 for (index = 0; index < collectionLength; index++) { |
|
32939 key = (collection === collectionKeys) ? index : collectionKeys[index]; |
|
32940 value = collection[key]; |
|
32941 trackById = trackByIdFn(key, value, index); |
|
32942 if (lastBlockMap[trackById]) { |
|
32943 // found previously seen block |
|
32944 block = lastBlockMap[trackById]; |
|
32945 delete lastBlockMap[trackById]; |
|
32946 nextBlockMap[trackById] = block; |
|
32947 nextBlockOrder[index] = block; |
|
32948 } else if (nextBlockMap[trackById]) { |
|
32949 // if collision detected. restore lastBlockMap and throw an error |
|
32950 forEach(nextBlockOrder, function (block) { |
|
32951 if (block && block.scope) lastBlockMap[block.id] = block; |
|
32952 }); |
|
32953 throw ngRepeatMinErr('dupes', |
|
32954 "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}", |
|
32955 expression, trackById, toJson(value)); |
|
32956 } else { |
|
32957 // new never before seen block |
|
32958 nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined}; |
|
32959 nextBlockMap[trackById] = true; |
|
32960 } |
|
32961 } |
|
32962 |
|
32963 // remove leftover items |
|
32964 for (var blockKey in lastBlockMap) { |
|
32965 block = lastBlockMap[blockKey]; |
|
32966 elementsToRemove = getBlockNodes(block.clone); |
|
32967 $animate.leave(elementsToRemove); |
|
32968 if (elementsToRemove[0].parentNode) { |
|
32969 // if the element was not removed yet because of pending animation, mark it as deleted |
|
32970 // so that we can ignore it later |
|
32971 for (index = 0, length = elementsToRemove.length; index < length; index++) { |
|
32972 elementsToRemove[index][NG_REMOVED] = true; |
|
32973 } |
|
32974 } |
|
32975 block.scope.$destroy(); |
|
32976 } |
|
32977 |
|
32978 // we are not using forEach for perf reasons (trying to avoid #call) |
|
32979 for (index = 0; index < collectionLength; index++) { |
|
32980 key = (collection === collectionKeys) ? index : collectionKeys[index]; |
|
32981 value = collection[key]; |
|
32982 block = nextBlockOrder[index]; |
|
32983 |
|
32984 if (block.scope) { |
|
32985 // if we have already seen this object, then we need to reuse the |
|
32986 // associated scope/element |
|
32987 |
|
32988 nextNode = previousNode; |
|
32989 |
|
32990 // skip nodes that are already pending removal via leave animation |
|
32991 do { |
|
32992 nextNode = nextNode.nextSibling; |
|
32993 } while (nextNode && nextNode[NG_REMOVED]); |
|
32994 |
|
32995 if (getBlockStart(block) != nextNode) { |
|
32996 // existing item which got moved |
|
32997 $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode)); |
|
32998 } |
|
32999 previousNode = getBlockEnd(block); |
|
33000 updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); |
|
33001 } else { |
|
33002 // new item which we don't know about |
|
33003 $transclude(function ngRepeatTransclude(clone, scope) { |
|
33004 block.scope = scope; |
|
33005 // http://jsperf.com/clone-vs-createcomment |
|
33006 var endNode = ngRepeatEndComment.cloneNode(false); |
|
33007 clone[clone.length++] = endNode; |
|
33008 |
|
33009 // TODO(perf): support naked previousNode in `enter` to avoid creation of jqLite wrapper? |
|
33010 $animate.enter(clone, null, jqLite(previousNode)); |
|
33011 previousNode = endNode; |
|
33012 // Note: We only need the first/last node of the cloned nodes. |
|
33013 // However, we need to keep the reference to the jqlite wrapper as it might be changed later |
|
33014 // by a directive with templateUrl when its template arrives. |
|
33015 block.clone = clone; |
|
33016 nextBlockMap[block.id] = block; |
|
33017 updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); |
|
33018 }); |
|
33019 } |
|
33020 } |
|
33021 lastBlockMap = nextBlockMap; |
|
33022 }); |
|
33023 }; |
|
33024 } |
|
33025 }; |
|
33026 }]; |
|
33027 |
|
33028 var NG_HIDE_CLASS = 'ng-hide'; |
|
33029 var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; |
|
33030 /** |
|
33031 * @ngdoc directive |
|
33032 * @name ngShow |
|
33033 * |
|
33034 * @description |
|
33035 * The `ngShow` directive shows or hides the given HTML element based on the expression |
|
33036 * provided to the `ngShow` attribute. The element is shown or hidden by removing or adding |
|
33037 * the `.ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined |
|
33038 * in AngularJS and sets the display style to none (using an !important flag). |
|
33039 * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). |
|
33040 * |
|
33041 * ```html |
|
33042 * <!-- when $scope.myValue is truthy (element is visible) --> |
|
33043 * <div ng-show="myValue"></div> |
|
33044 * |
|
33045 * <!-- when $scope.myValue is falsy (element is hidden) --> |
|
33046 * <div ng-show="myValue" class="ng-hide"></div> |
|
33047 * ``` |
|
33048 * |
|
33049 * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added to the class |
|
33050 * attribute on the element causing it to become hidden. When truthy, the `.ng-hide` CSS class is removed |
|
33051 * from the element causing the element not to appear hidden. |
|
33052 * |
|
33053 * ## Why is !important used? |
|
33054 * |
|
33055 * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector |
|
33056 * can be easily overridden by heavier selectors. For example, something as simple |
|
33057 * as changing the display style on a HTML list item would make hidden elements appear visible. |
|
33058 * This also becomes a bigger issue when dealing with CSS frameworks. |
|
33059 * |
|
33060 * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector |
|
33061 * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the |
|
33062 * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. |
|
33063 * |
|
33064 * ### Overriding `.ng-hide` |
|
33065 * |
|
33066 * By default, the `.ng-hide` class will style the element with `display:none!important`. If you wish to change |
|
33067 * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` |
|
33068 * class in CSS: |
|
33069 * |
|
33070 * ```css |
|
33071 * .ng-hide { |
|
33072 * /* this is just another form of hiding an element */ |
|
33073 * display:block!important; |
|
33074 * position:absolute; |
|
33075 * top:-9999px; |
|
33076 * left:-9999px; |
|
33077 * } |
|
33078 * ``` |
|
33079 * |
|
33080 * By default you don't need to override in CSS anything and the animations will work around the display style. |
|
33081 * |
|
33082 * ## A note about animations with `ngShow` |
|
33083 * |
|
33084 * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression |
|
33085 * is true and false. This system works like the animation system present with ngClass except that |
|
33086 * you must also include the !important flag to override the display property |
|
33087 * so that you can perform an animation when the element is hidden during the time of the animation. |
|
33088 * |
|
33089 * ```css |
|
33090 * // |
|
33091 * //a working example can be found at the bottom of this page |
|
33092 * // |
|
33093 * .my-element.ng-hide-add, .my-element.ng-hide-remove { |
|
33094 * /* this is required as of 1.3x to properly |
|
33095 * apply all styling in a show/hide animation */ |
|
33096 * transition:0s linear all; |
|
33097 * } |
|
33098 * |
|
33099 * .my-element.ng-hide-add-active, |
|
33100 * .my-element.ng-hide-remove-active { |
|
33101 * /* the transition is defined in the active class */ |
|
33102 * transition:1s linear all; |
|
33103 * } |
|
33104 * |
|
33105 * .my-element.ng-hide-add { ... } |
|
33106 * .my-element.ng-hide-add.ng-hide-add-active { ... } |
|
33107 * .my-element.ng-hide-remove { ... } |
|
33108 * .my-element.ng-hide-remove.ng-hide-remove-active { ... } |
|
33109 * ``` |
|
33110 * |
|
33111 * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display |
|
33112 * property to block during animation states--ngAnimate will handle the style toggling automatically for you. |
|
33113 * |
|
33114 * @animations |
|
33115 * addClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a truthy value and the just before contents are set to visible |
|
33116 * removeClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden |
|
33117 * |
|
33118 * @element ANY |
|
33119 * @param {expression} ngShow If the {@link guide/expression expression} is truthy |
|
33120 * then the element is shown or hidden respectively. |
|
33121 * |
|
33122 * @example |
|
33123 <example module="ngAnimate" deps="angular-animate.js" animations="true"> |
|
33124 <file name="index.html"> |
|
33125 Click me: <input type="checkbox" ng-model="checked"><br/> |
|
33126 <div> |
|
33127 Show: |
|
33128 <div class="check-element animate-show" ng-show="checked"> |
|
33129 <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked. |
|
33130 </div> |
|
33131 </div> |
|
33132 <div> |
|
33133 Hide: |
|
33134 <div class="check-element animate-show" ng-hide="checked"> |
|
33135 <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked. |
|
33136 </div> |
|
33137 </div> |
|
33138 </file> |
|
33139 <file name="glyphicons.css"> |
|
33140 @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css); |
|
33141 </file> |
|
33142 <file name="animations.css"> |
|
33143 .animate-show { |
|
33144 line-height:20px; |
|
33145 opacity:1; |
|
33146 padding:10px; |
|
33147 border:1px solid black; |
|
33148 background:white; |
|
33149 } |
|
33150 |
|
33151 .animate-show.ng-hide-add.ng-hide-add-active, |
|
33152 .animate-show.ng-hide-remove.ng-hide-remove-active { |
|
33153 -webkit-transition:all linear 0.5s; |
|
33154 transition:all linear 0.5s; |
|
33155 } |
|
33156 |
|
33157 .animate-show.ng-hide { |
|
33158 line-height:0; |
|
33159 opacity:0; |
|
33160 padding:0 10px; |
|
33161 } |
|
33162 |
|
33163 .check-element { |
|
33164 padding:10px; |
|
33165 border:1px solid black; |
|
33166 background:white; |
|
33167 } |
|
33168 </file> |
|
33169 <file name="protractor.js" type="protractor"> |
|
33170 var thumbsUp = element(by.css('span.glyphicon-thumbs-up')); |
|
33171 var thumbsDown = element(by.css('span.glyphicon-thumbs-down')); |
|
33172 |
|
33173 it('should check ng-show / ng-hide', function() { |
|
33174 expect(thumbsUp.isDisplayed()).toBeFalsy(); |
|
33175 expect(thumbsDown.isDisplayed()).toBeTruthy(); |
|
33176 |
|
33177 element(by.model('checked')).click(); |
|
33178 |
|
33179 expect(thumbsUp.isDisplayed()).toBeTruthy(); |
|
33180 expect(thumbsDown.isDisplayed()).toBeFalsy(); |
|
33181 }); |
|
33182 </file> |
|
33183 </example> |
|
33184 */ |
|
33185 var ngShowDirective = ['$animate', function($animate) { |
|
33186 return { |
|
33187 restrict: 'A', |
|
33188 multiElement: true, |
|
33189 link: function(scope, element, attr) { |
|
33190 scope.$watch(attr.ngShow, function ngShowWatchAction(value){ |
|
33191 // we're adding a temporary, animation-specific class for ng-hide since this way |
|
33192 // we can control when the element is actually displayed on screen without having |
|
33193 // to have a global/greedy CSS selector that breaks when other animations are run. |
|
33194 // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845 |
|
33195 $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, NG_HIDE_IN_PROGRESS_CLASS); |
|
33196 }); |
|
33197 } |
|
33198 }; |
|
33199 }]; |
|
33200 |
|
33201 |
|
33202 /** |
|
33203 * @ngdoc directive |
|
33204 * @name ngHide |
|
33205 * |
|
33206 * @description |
|
33207 * The `ngHide` directive shows or hides the given HTML element based on the expression |
|
33208 * provided to the `ngHide` attribute. The element is shown or hidden by removing or adding |
|
33209 * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined |
|
33210 * in AngularJS and sets the display style to none (using an !important flag). |
|
33211 * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). |
|
33212 * |
|
33213 * ```html |
|
33214 * <!-- when $scope.myValue is truthy (element is hidden) --> |
|
33215 * <div ng-hide="myValue" class="ng-hide"></div> |
|
33216 * |
|
33217 * <!-- when $scope.myValue is falsy (element is visible) --> |
|
33218 * <div ng-hide="myValue"></div> |
|
33219 * ``` |
|
33220 * |
|
33221 * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added to the class |
|
33222 * attribute on the element causing it to become hidden. When falsy, the `.ng-hide` CSS class is removed |
|
33223 * from the element causing the element not to appear hidden. |
|
33224 * |
|
33225 * ## Why is !important used? |
|
33226 * |
|
33227 * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector |
|
33228 * can be easily overridden by heavier selectors. For example, something as simple |
|
33229 * as changing the display style on a HTML list item would make hidden elements appear visible. |
|
33230 * This also becomes a bigger issue when dealing with CSS frameworks. |
|
33231 * |
|
33232 * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector |
|
33233 * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the |
|
33234 * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. |
|
33235 * |
|
33236 * ### Overriding `.ng-hide` |
|
33237 * |
|
33238 * By default, the `.ng-hide` class will style the element with `display:none!important`. If you wish to change |
|
33239 * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` |
|
33240 * class in CSS: |
|
33241 * |
|
33242 * ```css |
|
33243 * .ng-hide { |
|
33244 * /* this is just another form of hiding an element */ |
|
33245 * display:block!important; |
|
33246 * position:absolute; |
|
33247 * top:-9999px; |
|
33248 * left:-9999px; |
|
33249 * } |
|
33250 * ``` |
|
33251 * |
|
33252 * By default you don't need to override in CSS anything and the animations will work around the display style. |
|
33253 * |
|
33254 * ## A note about animations with `ngHide` |
|
33255 * |
|
33256 * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression |
|
33257 * is true and false. This system works like the animation system present with ngClass, except that the `.ng-hide` |
|
33258 * CSS class is added and removed for you instead of your own CSS class. |
|
33259 * |
|
33260 * ```css |
|
33261 * // |
|
33262 * //a working example can be found at the bottom of this page |
|
33263 * // |
|
33264 * .my-element.ng-hide-add, .my-element.ng-hide-remove { |
|
33265 * transition:0.5s linear all; |
|
33266 * } |
|
33267 * |
|
33268 * .my-element.ng-hide-add { ... } |
|
33269 * .my-element.ng-hide-add.ng-hide-add-active { ... } |
|
33270 * .my-element.ng-hide-remove { ... } |
|
33271 * .my-element.ng-hide-remove.ng-hide-remove-active { ... } |
|
33272 * ``` |
|
33273 * |
|
33274 * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display |
|
33275 * property to block during animation states--ngAnimate will handle the style toggling automatically for you. |
|
33276 * |
|
33277 * @animations |
|
33278 * removeClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden |
|
33279 * addClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a non truthy value and just before the contents are set to visible |
|
33280 * |
|
33281 * @element ANY |
|
33282 * @param {expression} ngHide If the {@link guide/expression expression} is truthy then |
|
33283 * the element is shown or hidden respectively. |
|
33284 * |
|
33285 * @example |
|
33286 <example module="ngAnimate" deps="angular-animate.js" animations="true"> |
|
33287 <file name="index.html"> |
|
33288 Click me: <input type="checkbox" ng-model="checked"><br/> |
|
33289 <div> |
|
33290 Show: |
|
33291 <div class="check-element animate-hide" ng-show="checked"> |
|
33292 <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked. |
|
33293 </div> |
|
33294 </div> |
|
33295 <div> |
|
33296 Hide: |
|
33297 <div class="check-element animate-hide" ng-hide="checked"> |
|
33298 <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked. |
|
33299 </div> |
|
33300 </div> |
|
33301 </file> |
|
33302 <file name="glyphicons.css"> |
|
33303 @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css); |
|
33304 </file> |
|
33305 <file name="animations.css"> |
|
33306 .animate-hide { |
|
33307 -webkit-transition:all linear 0.5s; |
|
33308 transition:all linear 0.5s; |
|
33309 line-height:20px; |
|
33310 opacity:1; |
|
33311 padding:10px; |
|
33312 border:1px solid black; |
|
33313 background:white; |
|
33314 } |
|
33315 |
|
33316 .animate-hide.ng-hide { |
|
33317 line-height:0; |
|
33318 opacity:0; |
|
33319 padding:0 10px; |
|
33320 } |
|
33321 |
|
33322 .check-element { |
|
33323 padding:10px; |
|
33324 border:1px solid black; |
|
33325 background:white; |
|
33326 } |
|
33327 </file> |
|
33328 <file name="protractor.js" type="protractor"> |
|
33329 var thumbsUp = element(by.css('span.glyphicon-thumbs-up')); |
|
33330 var thumbsDown = element(by.css('span.glyphicon-thumbs-down')); |
|
33331 |
|
33332 it('should check ng-show / ng-hide', function() { |
|
33333 expect(thumbsUp.isDisplayed()).toBeFalsy(); |
|
33334 expect(thumbsDown.isDisplayed()).toBeTruthy(); |
|
33335 |
|
33336 element(by.model('checked')).click(); |
|
33337 |
|
33338 expect(thumbsUp.isDisplayed()).toBeTruthy(); |
|
33339 expect(thumbsDown.isDisplayed()).toBeFalsy(); |
|
33340 }); |
|
33341 </file> |
|
33342 </example> |
|
33343 */ |
|
33344 var ngHideDirective = ['$animate', function($animate) { |
|
33345 return { |
|
33346 restrict: 'A', |
|
33347 multiElement: true, |
|
33348 link: function(scope, element, attr) { |
|
33349 scope.$watch(attr.ngHide, function ngHideWatchAction(value){ |
|
33350 // The comment inside of the ngShowDirective explains why we add and |
|
33351 // remove a temporary class for the show/hide animation |
|
33352 $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, NG_HIDE_IN_PROGRESS_CLASS); |
|
33353 }); |
|
33354 } |
|
33355 }; |
|
33356 }]; |
|
33357 |
|
33358 /** |
|
33359 * @ngdoc directive |
|
33360 * @name ngStyle |
|
33361 * @restrict AC |
|
33362 * |
|
33363 * @description |
|
33364 * The `ngStyle` directive allows you to set CSS style on an HTML element conditionally. |
|
33365 * |
|
33366 * @element ANY |
|
33367 * @param {expression} ngStyle |
|
33368 * |
|
33369 * {@link guide/expression Expression} which evals to an |
|
33370 * object whose keys are CSS style names and values are corresponding values for those CSS |
|
33371 * keys. |
|
33372 * |
|
33373 * Since some CSS style names are not valid keys for an object, they must be quoted. |
|
33374 * See the 'background-color' style in the example below. |
|
33375 * |
|
33376 * @example |
|
33377 <example> |
|
33378 <file name="index.html"> |
|
33379 <input type="button" value="set color" ng-click="myStyle={color:'red'}"> |
|
33380 <input type="button" value="set background" ng-click="myStyle={'background-color':'blue'}"> |
|
33381 <input type="button" value="clear" ng-click="myStyle={}"> |
|
33382 <br/> |
|
33383 <span ng-style="myStyle">Sample Text</span> |
|
33384 <pre>myStyle={{myStyle}}</pre> |
|
33385 </file> |
|
33386 <file name="style.css"> |
|
33387 span { |
|
33388 color: black; |
|
33389 } |
|
33390 </file> |
|
33391 <file name="protractor.js" type="protractor"> |
|
33392 var colorSpan = element(by.css('span')); |
|
33393 |
|
33394 it('should check ng-style', function() { |
|
33395 expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); |
|
33396 element(by.css('input[value=\'set color\']')).click(); |
|
33397 expect(colorSpan.getCssValue('color')).toBe('rgba(255, 0, 0, 1)'); |
|
33398 element(by.css('input[value=clear]')).click(); |
|
33399 expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); |
|
33400 }); |
|
33401 </file> |
|
33402 </example> |
|
33403 */ |
|
33404 var ngStyleDirective = ngDirective(function(scope, element, attr) { |
|
33405 scope.$watch(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) { |
|
33406 if (oldStyles && (newStyles !== oldStyles)) { |
|
33407 forEach(oldStyles, function(val, style) { element.css(style, '');}); |
|
33408 } |
|
33409 if (newStyles) element.css(newStyles); |
|
33410 }, true); |
|
33411 }); |
|
33412 |
|
33413 /** |
|
33414 * @ngdoc directive |
|
33415 * @name ngSwitch |
|
33416 * @restrict EA |
|
33417 * |
|
33418 * @description |
|
33419 * The `ngSwitch` directive is used to conditionally swap DOM structure on your template based on a scope expression. |
|
33420 * Elements within `ngSwitch` but without `ngSwitchWhen` or `ngSwitchDefault` directives will be preserved at the location |
|
33421 * as specified in the template. |
|
33422 * |
|
33423 * The directive itself works similar to ngInclude, however, instead of downloading template code (or loading it |
|
33424 * from the template cache), `ngSwitch` simply chooses one of the nested elements and makes it visible based on which element |
|
33425 * matches the value obtained from the evaluated expression. In other words, you define a container element |
|
33426 * (where you place the directive), place an expression on the **`on="..."` attribute** |
|
33427 * (or the **`ng-switch="..."` attribute**), define any inner elements inside of the directive and place |
|
33428 * a when attribute per element. The when attribute is used to inform ngSwitch which element to display when the on |
|
33429 * expression is evaluated. If a matching expression is not found via a when attribute then an element with the default |
|
33430 * attribute is displayed. |
|
33431 * |
|
33432 * <div class="alert alert-info"> |
|
33433 * Be aware that the attribute values to match against cannot be expressions. They are interpreted |
|
33434 * as literal string values to match against. |
|
33435 * For example, **`ng-switch-when="someVal"`** will match against the string `"someVal"` not against the |
|
33436 * value of the expression `$scope.someVal`. |
|
33437 * </div> |
|
33438 |
|
33439 * @animations |
|
33440 * enter - happens after the ngSwitch contents change and the matched child element is placed inside the container |
|
33441 * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM |
|
33442 * |
|
33443 * @usage |
|
33444 * |
|
33445 * ``` |
|
33446 * <ANY ng-switch="expression"> |
|
33447 * <ANY ng-switch-when="matchValue1">...</ANY> |
|
33448 * <ANY ng-switch-when="matchValue2">...</ANY> |
|
33449 * <ANY ng-switch-default>...</ANY> |
|
33450 * </ANY> |
|
33451 * ``` |
|
33452 * |
|
33453 * |
|
33454 * @scope |
|
33455 * @priority 1200 |
|
33456 * @param {*} ngSwitch|on expression to match against <tt>ng-switch-when</tt>. |
|
33457 * On child elements add: |
|
33458 * |
|
33459 * * `ngSwitchWhen`: the case statement to match against. If match then this |
|
33460 * case will be displayed. If the same match appears multiple times, all the |
|
33461 * elements will be displayed. |
|
33462 * * `ngSwitchDefault`: the default case when no other case match. If there |
|
33463 * are multiple default cases, all of them will be displayed when no other |
|
33464 * case match. |
|
33465 * |
|
33466 * |
|
33467 * @example |
|
33468 <example module="switchExample" deps="angular-animate.js" animations="true"> |
|
33469 <file name="index.html"> |
|
33470 <div ng-controller="ExampleController"> |
|
33471 <select ng-model="selection" ng-options="item for item in items"> |
|
33472 </select> |
|
33473 <tt>selection={{selection}}</tt> |
|
33474 <hr/> |
|
33475 <div class="animate-switch-container" |
|
33476 ng-switch on="selection"> |
|
33477 <div class="animate-switch" ng-switch-when="settings">Settings Div</div> |
|
33478 <div class="animate-switch" ng-switch-when="home">Home Span</div> |
|
33479 <div class="animate-switch" ng-switch-default>default</div> |
|
33480 </div> |
|
33481 </div> |
|
33482 </file> |
|
33483 <file name="script.js"> |
|
33484 angular.module('switchExample', ['ngAnimate']) |
|
33485 .controller('ExampleController', ['$scope', function($scope) { |
|
33486 $scope.items = ['settings', 'home', 'other']; |
|
33487 $scope.selection = $scope.items[0]; |
|
33488 }]); |
|
33489 </file> |
|
33490 <file name="animations.css"> |
|
33491 .animate-switch-container { |
|
33492 position:relative; |
|
33493 background:white; |
|
33494 border:1px solid black; |
|
33495 height:40px; |
|
33496 overflow:hidden; |
|
33497 } |
|
33498 |
|
33499 .animate-switch { |
|
33500 padding:10px; |
|
33501 } |
|
33502 |
|
33503 .animate-switch.ng-animate { |
|
33504 -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; |
|
33505 transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; |
|
33506 |
|
33507 position:absolute; |
|
33508 top:0; |
|
33509 left:0; |
|
33510 right:0; |
|
33511 bottom:0; |
|
33512 } |
|
33513 |
|
33514 .animate-switch.ng-leave.ng-leave-active, |
|
33515 .animate-switch.ng-enter { |
|
33516 top:-50px; |
|
33517 } |
|
33518 .animate-switch.ng-leave, |
|
33519 .animate-switch.ng-enter.ng-enter-active { |
|
33520 top:0; |
|
33521 } |
|
33522 </file> |
|
33523 <file name="protractor.js" type="protractor"> |
|
33524 var switchElem = element(by.css('[ng-switch]')); |
|
33525 var select = element(by.model('selection')); |
|
33526 |
|
33527 it('should start in settings', function() { |
|
33528 expect(switchElem.getText()).toMatch(/Settings Div/); |
|
33529 }); |
|
33530 it('should change to home', function() { |
|
33531 select.all(by.css('option')).get(1).click(); |
|
33532 expect(switchElem.getText()).toMatch(/Home Span/); |
|
33533 }); |
|
33534 it('should select default', function() { |
|
33535 select.all(by.css('option')).get(2).click(); |
|
33536 expect(switchElem.getText()).toMatch(/default/); |
|
33537 }); |
|
33538 </file> |
|
33539 </example> |
|
33540 */ |
|
33541 var ngSwitchDirective = ['$animate', function($animate) { |
|
33542 return { |
|
33543 restrict: 'EA', |
|
33544 require: 'ngSwitch', |
|
33545 |
|
33546 // asks for $scope to fool the BC controller module |
|
33547 controller: ['$scope', function ngSwitchController() { |
|
33548 this.cases = {}; |
|
33549 }], |
|
33550 link: function(scope, element, attr, ngSwitchController) { |
|
33551 var watchExpr = attr.ngSwitch || attr.on, |
|
33552 selectedTranscludes = [], |
|
33553 selectedElements = [], |
|
33554 previousLeaveAnimations = [], |
|
33555 selectedScopes = []; |
|
33556 |
|
33557 var spliceFactory = function(array, index) { |
|
33558 return function() { array.splice(index, 1); }; |
|
33559 }; |
|
33560 |
|
33561 scope.$watch(watchExpr, function ngSwitchWatchAction(value) { |
|
33562 var i, ii; |
|
33563 for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) { |
|
33564 $animate.cancel(previousLeaveAnimations[i]); |
|
33565 } |
|
33566 previousLeaveAnimations.length = 0; |
|
33567 |
|
33568 for (i = 0, ii = selectedScopes.length; i < ii; ++i) { |
|
33569 var selected = getBlockNodes(selectedElements[i].clone); |
|
33570 selectedScopes[i].$destroy(); |
|
33571 var promise = previousLeaveAnimations[i] = $animate.leave(selected); |
|
33572 promise.then(spliceFactory(previousLeaveAnimations, i)); |
|
33573 } |
|
33574 |
|
33575 selectedElements.length = 0; |
|
33576 selectedScopes.length = 0; |
|
33577 |
|
33578 if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) { |
|
33579 forEach(selectedTranscludes, function(selectedTransclude) { |
|
33580 selectedTransclude.transclude(function(caseElement, selectedScope) { |
|
33581 selectedScopes.push(selectedScope); |
|
33582 var anchor = selectedTransclude.element; |
|
33583 caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: '); |
|
33584 var block = { clone: caseElement }; |
|
33585 |
|
33586 selectedElements.push(block); |
|
33587 $animate.enter(caseElement, anchor.parent(), anchor); |
|
33588 }); |
|
33589 }); |
|
33590 } |
|
33591 }); |
|
33592 } |
|
33593 }; |
|
33594 }]; |
|
33595 |
|
33596 var ngSwitchWhenDirective = ngDirective({ |
|
33597 transclude: 'element', |
|
33598 priority: 1200, |
|
33599 require: '^ngSwitch', |
|
33600 multiElement: true, |
|
33601 link: function(scope, element, attrs, ctrl, $transclude) { |
|
33602 ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []); |
|
33603 ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element }); |
|
33604 } |
|
33605 }); |
|
33606 |
|
33607 var ngSwitchDefaultDirective = ngDirective({ |
|
33608 transclude: 'element', |
|
33609 priority: 1200, |
|
33610 require: '^ngSwitch', |
|
33611 multiElement: true, |
|
33612 link: function(scope, element, attr, ctrl, $transclude) { |
|
33613 ctrl.cases['?'] = (ctrl.cases['?'] || []); |
|
33614 ctrl.cases['?'].push({ transclude: $transclude, element: element }); |
|
33615 } |
|
33616 }); |
|
33617 |
|
33618 /** |
|
33619 * @ngdoc directive |
|
33620 * @name ngTransclude |
|
33621 * @restrict EAC |
|
33622 * |
|
33623 * @description |
|
33624 * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. |
|
33625 * |
|
33626 * Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted. |
|
33627 * |
|
33628 * @element ANY |
|
33629 * |
|
33630 * @example |
|
33631 <example module="transcludeExample"> |
|
33632 <file name="index.html"> |
|
33633 <script> |
|
33634 angular.module('transcludeExample', []) |
|
33635 .directive('pane', function(){ |
|
33636 return { |
|
33637 restrict: 'E', |
|
33638 transclude: true, |
|
33639 scope: { title:'@' }, |
|
33640 template: '<div style="border: 1px solid black;">' + |
|
33641 '<div style="background-color: gray">{{title}}</div>' + |
|
33642 '<ng-transclude></ng-transclude>' + |
|
33643 '</div>' |
|
33644 }; |
|
33645 }) |
|
33646 .controller('ExampleController', ['$scope', function($scope) { |
|
33647 $scope.title = 'Lorem Ipsum'; |
|
33648 $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; |
|
33649 }]); |
|
33650 </script> |
|
33651 <div ng-controller="ExampleController"> |
|
33652 <input ng-model="title"><br> |
|
33653 <textarea ng-model="text"></textarea> <br/> |
|
33654 <pane title="{{title}}">{{text}}</pane> |
|
33655 </div> |
|
33656 </file> |
|
33657 <file name="protractor.js" type="protractor"> |
|
33658 it('should have transcluded', function() { |
|
33659 var titleElement = element(by.model('title')); |
|
33660 titleElement.clear(); |
|
33661 titleElement.sendKeys('TITLE'); |
|
33662 var textElement = element(by.model('text')); |
|
33663 textElement.clear(); |
|
33664 textElement.sendKeys('TEXT'); |
|
33665 expect(element(by.binding('title')).getText()).toEqual('TITLE'); |
|
33666 expect(element(by.binding('text')).getText()).toEqual('TEXT'); |
|
33667 }); |
|
33668 </file> |
|
33669 </example> |
|
33670 * |
|
33671 */ |
|
33672 var ngTranscludeDirective = ngDirective({ |
|
33673 restrict: 'EAC', |
|
33674 link: function($scope, $element, $attrs, controller, $transclude) { |
|
33675 if (!$transclude) { |
|
33676 throw minErr('ngTransclude')('orphan', |
|
33677 'Illegal use of ngTransclude directive in the template! ' + |
|
33678 'No parent directive that requires a transclusion found. ' + |
|
33679 'Element: {0}', |
|
33680 startingTag($element)); |
|
33681 } |
|
33682 |
|
33683 $transclude(function(clone) { |
|
33684 $element.empty(); |
|
33685 $element.append(clone); |
|
33686 }); |
|
33687 } |
|
33688 }); |
|
33689 |
|
33690 /** |
|
33691 * @ngdoc directive |
|
33692 * @name script |
|
33693 * @restrict E |
|
33694 * |
|
33695 * @description |
|
33696 * Load the content of a `<script>` element into {@link ng.$templateCache `$templateCache`}, so that the |
|
33697 * template can be used by {@link ng.directive:ngInclude `ngInclude`}, |
|
33698 * {@link ngRoute.directive:ngView `ngView`}, or {@link guide/directive directives}. The type of the |
|
33699 * `<script>` element must be specified as `text/ng-template`, and a cache name for the template must be |
|
33700 * assigned through the element's `id`, which can then be used as a directive's `templateUrl`. |
|
33701 * |
|
33702 * @param {string} type Must be set to `'text/ng-template'`. |
|
33703 * @param {string} id Cache name of the template. |
|
33704 * |
|
33705 * @example |
|
33706 <example> |
|
33707 <file name="index.html"> |
|
33708 <script type="text/ng-template" id="/tpl.html"> |
|
33709 Content of the template. |
|
33710 </script> |
|
33711 |
|
33712 <a ng-click="currentTpl='/tpl.html'" id="tpl-link">Load inlined template</a> |
|
33713 <div id="tpl-content" ng-include src="currentTpl"></div> |
|
33714 </file> |
|
33715 <file name="protractor.js" type="protractor"> |
|
33716 it('should load template defined inside script tag', function() { |
|
33717 element(by.css('#tpl-link')).click(); |
|
33718 expect(element(by.css('#tpl-content')).getText()).toMatch(/Content of the template/); |
|
33719 }); |
|
33720 </file> |
|
33721 </example> |
|
33722 */ |
|
33723 var scriptDirective = ['$templateCache', function($templateCache) { |
|
33724 return { |
|
33725 restrict: 'E', |
|
33726 terminal: true, |
|
33727 compile: function(element, attr) { |
|
33728 if (attr.type == 'text/ng-template') { |
|
33729 var templateUrl = attr.id, |
|
33730 // IE is not consistent, in scripts we have to read .text but in other nodes we have to read .textContent |
|
33731 text = element[0].text; |
|
33732 |
|
33733 $templateCache.put(templateUrl, text); |
|
33734 } |
|
33735 } |
|
33736 }; |
|
33737 }]; |
|
33738 |
|
33739 var ngOptionsMinErr = minErr('ngOptions'); |
|
33740 /** |
|
33741 * @ngdoc directive |
|
33742 * @name select |
|
33743 * @restrict E |
|
33744 * |
|
33745 * @description |
|
33746 * HTML `SELECT` element with angular data-binding. |
|
33747 * |
|
33748 * # `ngOptions` |
|
33749 * |
|
33750 * The `ngOptions` attribute can be used to dynamically generate a list of `<option>` |
|
33751 * elements for the `<select>` element using the array or object obtained by evaluating the |
|
33752 * `ngOptions` comprehension_expression. |
|
33753 * |
|
33754 * When an item in the `<select>` menu is selected, the array element or object property |
|
33755 * represented by the selected option will be bound to the model identified by the `ngModel` |
|
33756 * directive. |
|
33757 * |
|
33758 * <div class="alert alert-warning"> |
|
33759 * **Note:** `ngModel` compares by reference, not value. This is important when binding to an |
|
33760 * array of objects. See an example [in this jsfiddle](http://jsfiddle.net/qWzTb/). |
|
33761 * </div> |
|
33762 * |
|
33763 * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can |
|
33764 * be nested into the `<select>` element. This element will then represent the `null` or "not selected" |
|
33765 * option. See example below for demonstration. |
|
33766 * |
|
33767 * <div class="alert alert-warning"> |
|
33768 * **Note:** `ngOptions` provides an iterator facility for the `<option>` element which should be used instead |
|
33769 * of {@link ng.directive:ngRepeat ngRepeat} when you want the |
|
33770 * `select` model to be bound to a non-string value. This is because an option element can only |
|
33771 * be bound to string values at present. |
|
33772 * </div> |
|
33773 * |
|
33774 * <div class="alert alert-info"> |
|
33775 * **Note:** Using `select as` will bind the result of the `select as` expression to the model, but |
|
33776 * the value of the `<select>` and `<option>` html elements will be either the index (for array data sources) |
|
33777 * or property name (for object data sources) of the value within the collection. |
|
33778 * </div> |
|
33779 * |
|
33780 * @param {string} ngModel Assignable angular expression to data-bind to. |
|
33781 * @param {string=} name Property name of the form under which the control is published. |
|
33782 * @param {string=} required The control is considered valid only if value is entered. |
|
33783 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to |
|
33784 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of |
|
33785 * `required` when you want to data-bind to the `required` attribute. |
|
33786 * @param {comprehension_expression=} ngOptions in one of the following forms: |
|
33787 * |
|
33788 * * for array data sources: |
|
33789 * * `label` **`for`** `value` **`in`** `array` |
|
33790 * * `select` **`as`** `label` **`for`** `value` **`in`** `array` |
|
33791 * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` |
|
33792 * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr` |
|
33793 * * for object data sources: |
|
33794 * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object` |
|
33795 * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object` |
|
33796 * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object` |
|
33797 * * `select` **`as`** `label` **`group by`** `group` |
|
33798 * **`for` `(`**`key`**`,`** `value`**`) in`** `object` |
|
33799 * |
|
33800 * Where: |
|
33801 * |
|
33802 * * `array` / `object`: an expression which evaluates to an array / object to iterate over. |
|
33803 * * `value`: local variable which will refer to each item in the `array` or each property value |
|
33804 * of `object` during iteration. |
|
33805 * * `key`: local variable which will refer to a property name in `object` during iteration. |
|
33806 * * `label`: The result of this expression will be the label for `<option>` element. The |
|
33807 * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`). |
|
33808 * * `select`: The result of this expression will be bound to the model of the parent `<select>` |
|
33809 * element. If not specified, `select` expression will default to `value`. |
|
33810 * * `group`: The result of this expression will be used to group options using the `<optgroup>` |
|
33811 * DOM element. |
|
33812 * * `trackexpr`: Used when working with an array of objects. The result of this expression will be |
|
33813 * used to identify the objects in the array. The `trackexpr` will most likely refer to the |
|
33814 * `value` variable (e.g. `value.propertyName`). With this the selection is preserved |
|
33815 * even when the options are recreated (e.g. reloaded from the server). |
|
33816 |
|
33817 * <div class="alert alert-info"> |
|
33818 * **Note:** Using `select as` together with `trackexpr` is not possible (and will throw). |
|
33819 * Reasoning: |
|
33820 * - Example: <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected"> |
|
33821 * values: [{id: 1, label: 'aLabel', subItem: {name: 'aSubItem'}}, {id: 2, label: 'bLabel', subItem: {name: 'bSubItemß'}}], |
|
33822 * $scope.selected = {name: 'aSubItem'}; |
|
33823 * - track by is always applied to `value`, with purpose to preserve the selection, |
|
33824 * (to `item` in this case) |
|
33825 * - to calculate whether an item is selected we do the following: |
|
33826 * 1. apply `track by` to the values in the array, e.g. |
|
33827 * In the example: [1,2] |
|
33828 * 2. apply `track by` to the already selected value in `ngModel`: |
|
33829 * In the example: this is not possible, as `track by` refers to `item.id`, but the selected |
|
33830 * value from `ngModel` is `{name: aSubItem}`. |
|
33831 * |
|
33832 * </div> |
|
33833 * |
|
33834 * @example |
|
33835 <example module="selectExample"> |
|
33836 <file name="index.html"> |
|
33837 <script> |
|
33838 angular.module('selectExample', []) |
|
33839 .controller('ExampleController', ['$scope', function($scope) { |
|
33840 $scope.colors = [ |
|
33841 {name:'black', shade:'dark'}, |
|
33842 {name:'white', shade:'light'}, |
|
33843 {name:'red', shade:'dark'}, |
|
33844 {name:'blue', shade:'dark'}, |
|
33845 {name:'yellow', shade:'light'} |
|
33846 ]; |
|
33847 $scope.myColor = $scope.colors[2]; // red |
|
33848 }]); |
|
33849 </script> |
|
33850 <div ng-controller="ExampleController"> |
|
33851 <ul> |
|
33852 <li ng-repeat="color in colors"> |
|
33853 Name: <input ng-model="color.name"> |
|
33854 [<a href ng-click="colors.splice($index, 1)">X</a>] |
|
33855 </li> |
|
33856 <li> |
|
33857 [<a href ng-click="colors.push({})">add</a>] |
|
33858 </li> |
|
33859 </ul> |
|
33860 <hr/> |
|
33861 Color (null not allowed): |
|
33862 <select ng-model="myColor" ng-options="color.name for color in colors"></select><br> |
|
33863 |
|
33864 Color (null allowed): |
|
33865 <span class="nullable"> |
|
33866 <select ng-model="myColor" ng-options="color.name for color in colors"> |
|
33867 <option value="">-- choose color --</option> |
|
33868 </select> |
|
33869 </span><br/> |
|
33870 |
|
33871 Color grouped by shade: |
|
33872 <select ng-model="myColor" ng-options="color.name group by color.shade for color in colors"> |
|
33873 </select><br/> |
|
33874 |
|
33875 |
|
33876 Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br> |
|
33877 <hr/> |
|
33878 Currently selected: {{ {selected_color:myColor} }} |
|
33879 <div style="border:solid 1px black; height:20px" |
|
33880 ng-style="{'background-color':myColor.name}"> |
|
33881 </div> |
|
33882 </div> |
|
33883 </file> |
|
33884 <file name="protractor.js" type="protractor"> |
|
33885 it('should check ng-options', function() { |
|
33886 expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red'); |
|
33887 element.all(by.model('myColor')).first().click(); |
|
33888 element.all(by.css('select[ng-model="myColor"] option')).first().click(); |
|
33889 expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black'); |
|
33890 element(by.css('.nullable select[ng-model="myColor"]')).click(); |
|
33891 element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click(); |
|
33892 expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null'); |
|
33893 }); |
|
33894 </file> |
|
33895 </example> |
|
33896 */ |
|
33897 |
|
33898 var ngOptionsDirective = valueFn({ |
|
33899 restrict: 'A', |
|
33900 terminal: true |
|
33901 }); |
|
33902 |
|
33903 // jshint maxlen: false |
|
33904 var selectDirective = ['$compile', '$parse', function($compile, $parse) { |
|
33905 //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888 |
|
33906 var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/, |
|
33907 nullModelCtrl = {$setViewValue: noop}; |
|
33908 // jshint maxlen: 100 |
|
33909 |
|
33910 return { |
|
33911 restrict: 'E', |
|
33912 require: ['select', '?ngModel'], |
|
33913 controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { |
|
33914 var self = this, |
|
33915 optionsMap = {}, |
|
33916 ngModelCtrl = nullModelCtrl, |
|
33917 nullOption, |
|
33918 unknownOption; |
|
33919 |
|
33920 |
|
33921 self.databound = $attrs.ngModel; |
|
33922 |
|
33923 |
|
33924 self.init = function(ngModelCtrl_, nullOption_, unknownOption_) { |
|
33925 ngModelCtrl = ngModelCtrl_; |
|
33926 nullOption = nullOption_; |
|
33927 unknownOption = unknownOption_; |
|
33928 }; |
|
33929 |
|
33930 |
|
33931 self.addOption = function(value, element) { |
|
33932 assertNotHasOwnProperty(value, '"option value"'); |
|
33933 optionsMap[value] = true; |
|
33934 |
|
33935 if (ngModelCtrl.$viewValue == value) { |
|
33936 $element.val(value); |
|
33937 if (unknownOption.parent()) unknownOption.remove(); |
|
33938 } |
|
33939 // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459 |
|
33940 // Adding an <option selected="selected"> element to a <select required="required"> should |
|
33941 // automatically select the new element |
|
33942 if (element[0].hasAttribute('selected')) { |
|
33943 element[0].selected = true; |
|
33944 } |
|
33945 }; |
|
33946 |
|
33947 |
|
33948 self.removeOption = function(value) { |
|
33949 if (this.hasOption(value)) { |
|
33950 delete optionsMap[value]; |
|
33951 if (ngModelCtrl.$viewValue == value) { |
|
33952 this.renderUnknownOption(value); |
|
33953 } |
|
33954 } |
|
33955 }; |
|
33956 |
|
33957 |
|
33958 self.renderUnknownOption = function(val) { |
|
33959 var unknownVal = '? ' + hashKey(val) + ' ?'; |
|
33960 unknownOption.val(unknownVal); |
|
33961 $element.prepend(unknownOption); |
|
33962 $element.val(unknownVal); |
|
33963 unknownOption.prop('selected', true); // needed for IE |
|
33964 }; |
|
33965 |
|
33966 |
|
33967 self.hasOption = function(value) { |
|
33968 return optionsMap.hasOwnProperty(value); |
|
33969 }; |
|
33970 |
|
33971 $scope.$on('$destroy', function() { |
|
33972 // disable unknown option so that we don't do work when the whole select is being destroyed |
|
33973 self.renderUnknownOption = noop; |
|
33974 }); |
|
33975 }], |
|
33976 |
|
33977 link: function(scope, element, attr, ctrls) { |
|
33978 // if ngModel is not defined, we don't need to do anything |
|
33979 if (!ctrls[1]) return; |
|
33980 |
|
33981 var selectCtrl = ctrls[0], |
|
33982 ngModelCtrl = ctrls[1], |
|
33983 multiple = attr.multiple, |
|
33984 optionsExp = attr.ngOptions, |
|
33985 nullOption = false, // if false, user will not be able to select it (used by ngOptions) |
|
33986 emptyOption, |
|
33987 renderScheduled = false, |
|
33988 // we can't just jqLite('<option>') since jqLite is not smart enough |
|
33989 // to create it in <select> and IE barfs otherwise. |
|
33990 optionTemplate = jqLite(document.createElement('option')), |
|
33991 optGroupTemplate =jqLite(document.createElement('optgroup')), |
|
33992 unknownOption = optionTemplate.clone(); |
|
33993 |
|
33994 // find "null" option |
|
33995 for(var i = 0, children = element.children(), ii = children.length; i < ii; i++) { |
|
33996 if (children[i].value === '') { |
|
33997 emptyOption = nullOption = children.eq(i); |
|
33998 break; |
|
33999 } |
|
34000 } |
|
34001 |
|
34002 selectCtrl.init(ngModelCtrl, nullOption, unknownOption); |
|
34003 |
|
34004 // required validator |
|
34005 if (multiple) { |
|
34006 ngModelCtrl.$isEmpty = function(value) { |
|
34007 return !value || value.length === 0; |
|
34008 }; |
|
34009 } |
|
34010 |
|
34011 if (optionsExp) setupAsOptions(scope, element, ngModelCtrl); |
|
34012 else if (multiple) setupAsMultiple(scope, element, ngModelCtrl); |
|
34013 else setupAsSingle(scope, element, ngModelCtrl, selectCtrl); |
|
34014 |
|
34015 |
|
34016 //////////////////////////// |
|
34017 |
|
34018 |
|
34019 |
|
34020 function setupAsSingle(scope, selectElement, ngModelCtrl, selectCtrl) { |
|
34021 ngModelCtrl.$render = function() { |
|
34022 var viewValue = ngModelCtrl.$viewValue; |
|
34023 |
|
34024 if (selectCtrl.hasOption(viewValue)) { |
|
34025 if (unknownOption.parent()) unknownOption.remove(); |
|
34026 selectElement.val(viewValue); |
|
34027 if (viewValue === '') emptyOption.prop('selected', true); // to make IE9 happy |
|
34028 } else { |
|
34029 if (isUndefined(viewValue) && emptyOption) { |
|
34030 selectElement.val(''); |
|
34031 } else { |
|
34032 selectCtrl.renderUnknownOption(viewValue); |
|
34033 } |
|
34034 } |
|
34035 }; |
|
34036 |
|
34037 selectElement.on('change', function() { |
|
34038 scope.$apply(function() { |
|
34039 if (unknownOption.parent()) unknownOption.remove(); |
|
34040 ngModelCtrl.$setViewValue(selectElement.val()); |
|
34041 }); |
|
34042 }); |
|
34043 } |
|
34044 |
|
34045 function setupAsMultiple(scope, selectElement, ctrl) { |
|
34046 var lastView; |
|
34047 ctrl.$render = function() { |
|
34048 var items = new HashMap(ctrl.$viewValue); |
|
34049 forEach(selectElement.find('option'), function(option) { |
|
34050 option.selected = isDefined(items.get(option.value)); |
|
34051 }); |
|
34052 }; |
|
34053 |
|
34054 // we have to do it on each watch since ngModel watches reference, but |
|
34055 // we need to work of an array, so we need to see if anything was inserted/removed |
|
34056 scope.$watch(function selectMultipleWatch() { |
|
34057 if (!equals(lastView, ctrl.$viewValue)) { |
|
34058 lastView = shallowCopy(ctrl.$viewValue); |
|
34059 ctrl.$render(); |
|
34060 } |
|
34061 }); |
|
34062 |
|
34063 selectElement.on('change', function() { |
|
34064 scope.$apply(function() { |
|
34065 var array = []; |
|
34066 forEach(selectElement.find('option'), function(option) { |
|
34067 if (option.selected) { |
|
34068 array.push(option.value); |
|
34069 } |
|
34070 }); |
|
34071 ctrl.$setViewValue(array); |
|
34072 }); |
|
34073 }); |
|
34074 } |
|
34075 |
|
34076 function setupAsOptions(scope, selectElement, ctrl) { |
|
34077 var match; |
|
34078 |
|
34079 if (!(match = optionsExp.match(NG_OPTIONS_REGEXP))) { |
|
34080 throw ngOptionsMinErr('iexp', |
|
34081 "Expected expression in form of " + |
|
34082 "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + |
|
34083 " but got '{0}'. Element: {1}", |
|
34084 optionsExp, startingTag(selectElement)); |
|
34085 } |
|
34086 |
|
34087 var displayFn = $parse(match[2] || match[1]), |
|
34088 valueName = match[4] || match[6], |
|
34089 selectAs = / as /.test(match[0]) && match[1], |
|
34090 selectAsFn = selectAs ? $parse(selectAs) : null, |
|
34091 keyName = match[5], |
|
34092 groupByFn = $parse(match[3] || ''), |
|
34093 valueFn = $parse(match[2] ? match[1] : valueName), |
|
34094 valuesFn = $parse(match[7]), |
|
34095 track = match[8], |
|
34096 trackFn = track ? $parse(match[8]) : null, |
|
34097 // This is an array of array of existing option groups in DOM. |
|
34098 // We try to reuse these if possible |
|
34099 // - optionGroupsCache[0] is the options with no option group |
|
34100 // - optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element |
|
34101 optionGroupsCache = [[{element: selectElement, label:''}]], |
|
34102 //re-usable object to represent option's locals |
|
34103 locals = {}; |
|
34104 |
|
34105 if (trackFn && selectAsFn) { |
|
34106 throw ngOptionsMinErr('trkslct', |
|
34107 "Comprehension expression cannot contain both selectAs '{0}' " + |
|
34108 "and trackBy '{1}' expressions.", |
|
34109 selectAs, track); |
|
34110 } |
|
34111 |
|
34112 if (nullOption) { |
|
34113 // compile the element since there might be bindings in it |
|
34114 $compile(nullOption)(scope); |
|
34115 |
|
34116 // remove the class, which is added automatically because we recompile the element and it |
|
34117 // becomes the compilation root |
|
34118 nullOption.removeClass('ng-scope'); |
|
34119 |
|
34120 // we need to remove it before calling selectElement.empty() because otherwise IE will |
|
34121 // remove the label from the element. wtf? |
|
34122 nullOption.remove(); |
|
34123 } |
|
34124 |
|
34125 // clear contents, we'll add what's needed based on the model |
|
34126 selectElement.empty(); |
|
34127 |
|
34128 selectElement.on('change', selectionChanged); |
|
34129 |
|
34130 ctrl.$render = render; |
|
34131 |
|
34132 scope.$watchCollection(valuesFn, scheduleRendering); |
|
34133 scope.$watchCollection(getLabels, scheduleRendering); |
|
34134 |
|
34135 if (multiple) { |
|
34136 scope.$watchCollection(function() { return ctrl.$modelValue; }, scheduleRendering); |
|
34137 } |
|
34138 |
|
34139 // ------------------------------------------------------------------ // |
|
34140 |
|
34141 function callExpression(exprFn, key, value) { |
|
34142 locals[valueName] = value; |
|
34143 if (keyName) locals[keyName] = key; |
|
34144 return exprFn(scope, locals); |
|
34145 } |
|
34146 |
|
34147 function selectionChanged() { |
|
34148 scope.$apply(function() { |
|
34149 var optionGroup, |
|
34150 collection = valuesFn(scope) || [], |
|
34151 key, value, optionElement, index, groupIndex, length, groupLength, trackIndex; |
|
34152 var viewValue; |
|
34153 if (multiple) { |
|
34154 viewValue = []; |
|
34155 forEach(selectElement.val(), function(selectedKey) { |
|
34156 viewValue.push(getViewValue(selectedKey, collection[selectedKey])); |
|
34157 }); |
|
34158 } else { |
|
34159 var selectedKey = selectElement.val(); |
|
34160 viewValue = getViewValue(selectedKey, collection[selectedKey]); |
|
34161 } |
|
34162 ctrl.$setViewValue(viewValue); |
|
34163 render(); |
|
34164 }); |
|
34165 } |
|
34166 |
|
34167 function getViewValue(key, value) { |
|
34168 if (key === '?') { |
|
34169 return undefined; |
|
34170 } else if (key === '') { |
|
34171 return null; |
|
34172 } else { |
|
34173 var viewValueFn = selectAsFn ? selectAsFn : valueFn; |
|
34174 return callExpression(viewValueFn, key, value); |
|
34175 } |
|
34176 } |
|
34177 |
|
34178 function getLabels() { |
|
34179 var values = valuesFn(scope); |
|
34180 var toDisplay; |
|
34181 if (values && isArray(values)) { |
|
34182 toDisplay = new Array(values.length); |
|
34183 for (var i = 0, ii = values.length; i < ii; i++) { |
|
34184 toDisplay[i] = callExpression(displayFn, i, values[i]); |
|
34185 } |
|
34186 return toDisplay; |
|
34187 } else if (values) { |
|
34188 // TODO: Add a test for this case |
|
34189 toDisplay = {}; |
|
34190 for (var prop in values) { |
|
34191 if (values.hasOwnProperty(prop)) { |
|
34192 toDisplay[prop] = callExpression(displayFn, prop, values[prop]); |
|
34193 } |
|
34194 } |
|
34195 } |
|
34196 return toDisplay; |
|
34197 } |
|
34198 |
|
34199 function createIsSelectedFn(viewValue) { |
|
34200 var selectedSet; |
|
34201 if (multiple) { |
|
34202 if (!selectAs && trackFn && isArray(viewValue)) { |
|
34203 |
|
34204 selectedSet = new HashMap([]); |
|
34205 for (var trackIndex = 0; trackIndex < viewValue.length; trackIndex++) { |
|
34206 // tracking by key |
|
34207 selectedSet.put(callExpression(trackFn, null, viewValue[trackIndex]), true); |
|
34208 } |
|
34209 } else { |
|
34210 selectedSet = new HashMap(viewValue); |
|
34211 } |
|
34212 } else if (!selectAsFn && trackFn) { |
|
34213 viewValue = callExpression(trackFn, null, viewValue); |
|
34214 } |
|
34215 return function isSelected(key, value) { |
|
34216 var compareValueFn; |
|
34217 if (selectAsFn) { |
|
34218 compareValueFn = selectAsFn; |
|
34219 } else if (trackFn) { |
|
34220 compareValueFn = trackFn; |
|
34221 } else { |
|
34222 compareValueFn = valueFn; |
|
34223 } |
|
34224 |
|
34225 if (multiple) { |
|
34226 return isDefined(selectedSet.remove(callExpression(compareValueFn, key, value))); |
|
34227 } else { |
|
34228 return viewValue == callExpression(compareValueFn, key, value); |
|
34229 } |
|
34230 }; |
|
34231 } |
|
34232 |
|
34233 function scheduleRendering() { |
|
34234 if (!renderScheduled) { |
|
34235 scope.$$postDigest(render); |
|
34236 renderScheduled = true; |
|
34237 } |
|
34238 } |
|
34239 |
|
34240 function render() { |
|
34241 renderScheduled = false; |
|
34242 |
|
34243 // Temporary location for the option groups before we render them |
|
34244 var optionGroups = {'':[]}, |
|
34245 optionGroupNames = [''], |
|
34246 optionGroupName, |
|
34247 optionGroup, |
|
34248 option, |
|
34249 existingParent, existingOptions, existingOption, |
|
34250 viewValue = ctrl.$viewValue, |
|
34251 values = valuesFn(scope) || [], |
|
34252 keys = keyName ? sortedKeys(values) : values, |
|
34253 key, |
|
34254 value, |
|
34255 groupLength, length, |
|
34256 groupIndex, index, |
|
34257 selected, |
|
34258 isSelected = createIsSelectedFn(viewValue), |
|
34259 anySelected = false, |
|
34260 lastElement, |
|
34261 element, |
|
34262 label; |
|
34263 |
|
34264 // We now build up the list of options we need (we merge later) |
|
34265 for (index = 0; length = keys.length, index < length; index++) { |
|
34266 key = index; |
|
34267 if (keyName) { |
|
34268 key = keys[index]; |
|
34269 if ( key.charAt(0) === '$' ) continue; |
|
34270 } |
|
34271 value = values[key]; |
|
34272 |
|
34273 optionGroupName = callExpression(groupByFn, key, value) || ''; |
|
34274 if (!(optionGroup = optionGroups[optionGroupName])) { |
|
34275 optionGroup = optionGroups[optionGroupName] = []; |
|
34276 optionGroupNames.push(optionGroupName); |
|
34277 } |
|
34278 |
|
34279 selected = isSelected(key, value); |
|
34280 anySelected = anySelected || selected; |
|
34281 |
|
34282 label = callExpression(displayFn, key, value); // what will be seen by the user |
|
34283 |
|
34284 // doing displayFn(scope, locals) || '' overwrites zero values |
|
34285 label = isDefined(label) ? label : ''; |
|
34286 optionGroup.push({ |
|
34287 // either the index into array or key from object |
|
34288 id: (keyName ? keys[index] : index), |
|
34289 label: label, |
|
34290 selected: selected // determine if we should be selected |
|
34291 }); |
|
34292 } |
|
34293 if (!multiple) { |
|
34294 if (nullOption || viewValue === null) { |
|
34295 // insert null option if we have a placeholder, or the model is null |
|
34296 optionGroups[''].unshift({id:'', label:'', selected:!anySelected}); |
|
34297 } else if (!anySelected) { |
|
34298 // option could not be found, we have to insert the undefined item |
|
34299 optionGroups[''].unshift({id:'?', label:'', selected:true}); |
|
34300 } |
|
34301 } |
|
34302 |
|
34303 // Now we need to update the list of DOM nodes to match the optionGroups we computed above |
|
34304 for (groupIndex = 0, groupLength = optionGroupNames.length; |
|
34305 groupIndex < groupLength; |
|
34306 groupIndex++) { |
|
34307 // current option group name or '' if no group |
|
34308 optionGroupName = optionGroupNames[groupIndex]; |
|
34309 |
|
34310 // list of options for that group. (first item has the parent) |
|
34311 optionGroup = optionGroups[optionGroupName]; |
|
34312 |
|
34313 if (optionGroupsCache.length <= groupIndex) { |
|
34314 // we need to grow the optionGroups |
|
34315 existingParent = { |
|
34316 element: optGroupTemplate.clone().attr('label', optionGroupName), |
|
34317 label: optionGroup.label |
|
34318 }; |
|
34319 existingOptions = [existingParent]; |
|
34320 optionGroupsCache.push(existingOptions); |
|
34321 selectElement.append(existingParent.element); |
|
34322 } else { |
|
34323 existingOptions = optionGroupsCache[groupIndex]; |
|
34324 existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element |
|
34325 |
|
34326 // update the OPTGROUP label if not the same. |
|
34327 if (existingParent.label != optionGroupName) { |
|
34328 existingParent.element.attr('label', existingParent.label = optionGroupName); |
|
34329 } |
|
34330 } |
|
34331 |
|
34332 lastElement = null; // start at the beginning |
|
34333 for(index = 0, length = optionGroup.length; index < length; index++) { |
|
34334 option = optionGroup[index]; |
|
34335 if ((existingOption = existingOptions[index+1])) { |
|
34336 // reuse elements |
|
34337 lastElement = existingOption.element; |
|
34338 if (existingOption.label !== option.label) { |
|
34339 lastElement.text(existingOption.label = option.label); |
|
34340 } |
|
34341 if (existingOption.id !== option.id) { |
|
34342 lastElement.val(existingOption.id = option.id); |
|
34343 } |
|
34344 // lastElement.prop('selected') provided by jQuery has side-effects |
|
34345 if (lastElement[0].selected !== option.selected) { |
|
34346 lastElement.prop('selected', (existingOption.selected = option.selected)); |
|
34347 if (msie) { |
|
34348 // See #7692 |
|
34349 // The selected item wouldn't visually update on IE without this. |
|
34350 // Tested on Win7: IE9, IE10 and IE11. Future IEs should be tested as well |
|
34351 lastElement.prop('selected', existingOption.selected); |
|
34352 } |
|
34353 } |
|
34354 } else { |
|
34355 // grow elements |
|
34356 |
|
34357 // if it's a null option |
|
34358 if (option.id === '' && nullOption) { |
|
34359 // put back the pre-compiled element |
|
34360 element = nullOption; |
|
34361 } else { |
|
34362 // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but |
|
34363 // in this version of jQuery on some browser the .text() returns a string |
|
34364 // rather then the element. |
|
34365 (element = optionTemplate.clone()) |
|
34366 .val(option.id) |
|
34367 .prop('selected', option.selected) |
|
34368 .attr('selected', option.selected) |
|
34369 .text(option.label); |
|
34370 } |
|
34371 |
|
34372 existingOptions.push(existingOption = { |
|
34373 element: element, |
|
34374 label: option.label, |
|
34375 id: option.id, |
|
34376 selected: option.selected |
|
34377 }); |
|
34378 selectCtrl.addOption(option.label, element); |
|
34379 if (lastElement) { |
|
34380 lastElement.after(element); |
|
34381 } else { |
|
34382 existingParent.element.append(element); |
|
34383 } |
|
34384 lastElement = element; |
|
34385 } |
|
34386 } |
|
34387 // remove any excessive OPTIONs in a group |
|
34388 index++; // increment since the existingOptions[0] is parent element not OPTION |
|
34389 while(existingOptions.length > index) { |
|
34390 option = existingOptions.pop(); |
|
34391 selectCtrl.removeOption(option.label); |
|
34392 option.element.remove(); |
|
34393 } |
|
34394 } |
|
34395 // remove any excessive OPTGROUPs from select |
|
34396 while(optionGroupsCache.length > groupIndex) { |
|
34397 optionGroupsCache.pop()[0].element.remove(); |
|
34398 } |
|
34399 } |
|
34400 } |
|
34401 } |
|
34402 }; |
|
34403 }]; |
|
34404 |
|
34405 var optionDirective = ['$interpolate', function($interpolate) { |
|
34406 var nullSelectCtrl = { |
|
34407 addOption: noop, |
|
34408 removeOption: noop |
|
34409 }; |
|
34410 |
|
34411 return { |
|
34412 restrict: 'E', |
|
34413 priority: 100, |
|
34414 compile: function(element, attr) { |
|
34415 if (isUndefined(attr.value)) { |
|
34416 var interpolateFn = $interpolate(element.text(), true); |
|
34417 if (!interpolateFn) { |
|
34418 attr.$set('value', element.text()); |
|
34419 } |
|
34420 } |
|
34421 |
|
34422 return function (scope, element, attr) { |
|
34423 var selectCtrlName = '$selectController', |
|
34424 parent = element.parent(), |
|
34425 selectCtrl = parent.data(selectCtrlName) || |
|
34426 parent.parent().data(selectCtrlName); // in case we are in optgroup |
|
34427 |
|
34428 if (!selectCtrl || !selectCtrl.databound) { |
|
34429 selectCtrl = nullSelectCtrl; |
|
34430 } |
|
34431 |
|
34432 if (interpolateFn) { |
|
34433 scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) { |
|
34434 attr.$set('value', newVal); |
|
34435 if (oldVal !== newVal) { |
|
34436 selectCtrl.removeOption(oldVal); |
|
34437 } |
|
34438 selectCtrl.addOption(newVal, element); |
|
34439 }); |
|
34440 } else { |
|
34441 selectCtrl.addOption(attr.value, element); |
|
34442 } |
|
34443 |
|
34444 element.on('$destroy', function() { |
|
34445 selectCtrl.removeOption(attr.value); |
|
34446 }); |
|
34447 }; |
|
34448 } |
|
34449 }; |
|
34450 }]; |
|
34451 |
|
34452 var styleDirective = valueFn({ |
|
34453 restrict: 'E', |
|
34454 terminal: false |
|
34455 }); |
|
34456 |
|
34457 if (window.angular.bootstrap) { |
|
34458 //AngularJS is already loaded, so we can return here... |
|
34459 console.log('WARNING: Tried to load angular more than once.'); |
|
34460 return; |
|
34461 } |
|
34462 |
|
34463 //try to bind to jquery now so that one can write jqLite(document).ready() |
|
34464 //but we will rebind on bootstrap again. |
|
34465 bindJQuery(); |
|
34466 |
|
34467 publishExternalAPI(angular); |
|
34468 |
|
34469 jqLite(document).ready(function() { |
|
34470 angularInit(document, bootstrap); |
|
34471 }); |
|
34472 |
|
34473 })(window, document); |
|
34474 |
|
34475 !window.angular.$$csp() && window.angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}</style>'); |
|
34476 /* |
|
34477 * angular-ui-bootstrap |
|
34478 * http://angular-ui.github.io/bootstrap/ |
|
34479 |
|
34480 * Version: 0.11.2 - 2014-09-26 |
|
34481 * License: MIT |
|
34482 */ |
|
34483 angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]); |
|
34484 angular.module("ui.bootstrap.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]); |
|
34485 angular.module('ui.bootstrap.transition', []) |
|
34486 |
|
34487 /** |
|
34488 * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. |
|
34489 * @param {DOMElement} element The DOMElement that will be animated. |
|
34490 * @param {string|object|function} trigger The thing that will cause the transition to start: |
|
34491 * - As a string, it represents the css class to be added to the element. |
|
34492 * - As an object, it represents a hash of style attributes to be applied to the element. |
|
34493 * - As a function, it represents a function to be called that will cause the transition to occur. |
|
34494 * @return {Promise} A promise that is resolved when the transition finishes. |
|
34495 */ |
|
34496 .factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) { |
|
34497 |
|
34498 var $transition = function(element, trigger, options) { |
|
34499 options = options || {}; |
|
34500 var deferred = $q.defer(); |
|
34501 var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; |
|
34502 |
|
34503 var transitionEndHandler = function(event) { |
|
34504 $rootScope.$apply(function() { |
|
34505 element.unbind(endEventName, transitionEndHandler); |
|
34506 deferred.resolve(element); |
|
34507 }); |
|
34508 }; |
|
34509 |
|
34510 if (endEventName) { |
|
34511 element.bind(endEventName, transitionEndHandler); |
|
34512 } |
|
34513 |
|
34514 // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur |
|
34515 $timeout(function() { |
|
34516 if ( angular.isString(trigger) ) { |
|
34517 element.addClass(trigger); |
|
34518 } else if ( angular.isFunction(trigger) ) { |
|
34519 trigger(element); |
|
34520 } else if ( angular.isObject(trigger) ) { |
|
34521 element.css(trigger); |
|
34522 } |
|
34523 //If browser does not support transitions, instantly resolve |
|
34524 if ( !endEventName ) { |
|
34525 deferred.resolve(element); |
|
34526 } |
|
34527 }); |
|
34528 |
|
34529 // Add our custom cancel function to the promise that is returned |
|
34530 // We can call this if we are about to run a new transition, which we know will prevent this transition from ending, |
|
34531 // i.e. it will therefore never raise a transitionEnd event for that transition |
|
34532 deferred.promise.cancel = function() { |
|
34533 if ( endEventName ) { |
|
34534 element.unbind(endEventName, transitionEndHandler); |
|
34535 } |
|
34536 deferred.reject('Transition cancelled'); |
|
34537 }; |
|
34538 |
|
34539 return deferred.promise; |
|
34540 }; |
|
34541 |
|
34542 // Work out the name of the transitionEnd event |
|
34543 var transElement = document.createElement('trans'); |
|
34544 var transitionEndEventNames = { |
|
34545 'WebkitTransition': 'webkitTransitionEnd', |
|
34546 'MozTransition': 'transitionend', |
|
34547 'OTransition': 'oTransitionEnd', |
|
34548 'transition': 'transitionend' |
|
34549 }; |
|
34550 var animationEndEventNames = { |
|
34551 'WebkitTransition': 'webkitAnimationEnd', |
|
34552 'MozTransition': 'animationend', |
|
34553 'OTransition': 'oAnimationEnd', |
|
34554 'transition': 'animationend' |
|
34555 }; |
|
34556 function findEndEventName(endEventNames) { |
|
34557 for (var name in endEventNames){ |
|
34558 if (transElement.style[name] !== undefined) { |
|
34559 return endEventNames[name]; |
|
34560 } |
|
34561 } |
|
34562 } |
|
34563 $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); |
|
34564 $transition.animationEndEventName = findEndEventName(animationEndEventNames); |
|
34565 return $transition; |
|
34566 }]); |
|
34567 |
|
34568 angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition']) |
|
34569 |
|
34570 .directive('collapse', ['$transition', function ($transition) { |
|
34571 |
|
34572 return { |
|
34573 link: function (scope, element, attrs) { |
|
34574 |
|
34575 var initialAnimSkip = true; |
|
34576 var currentTransition; |
|
34577 |
|
34578 function doTransition(change) { |
|
34579 var newTransition = $transition(element, change); |
|
34580 if (currentTransition) { |
|
34581 currentTransition.cancel(); |
|
34582 } |
|
34583 currentTransition = newTransition; |
|
34584 newTransition.then(newTransitionDone, newTransitionDone); |
|
34585 return newTransition; |
|
34586 |
|
34587 function newTransitionDone() { |
|
34588 // Make sure it's this transition, otherwise, leave it alone. |
|
34589 if (currentTransition === newTransition) { |
|
34590 currentTransition = undefined; |
|
34591 } |
|
34592 } |
|
34593 } |
|
34594 |
|
34595 function expand() { |
|
34596 if (initialAnimSkip) { |
|
34597 initialAnimSkip = false; |
|
34598 expandDone(); |
|
34599 } else { |
|
34600 element.removeClass('collapse').addClass('collapsing'); |
|
34601 doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone); |
|
34602 } |
|
34603 } |
|
34604 |
|
34605 function expandDone() { |
|
34606 element.removeClass('collapsing'); |
|
34607 element.addClass('collapse in'); |
|
34608 element.css({height: 'auto'}); |
|
34609 } |
|
34610 |
|
34611 function collapse() { |
|
34612 if (initialAnimSkip) { |
|
34613 initialAnimSkip = false; |
|
34614 collapseDone(); |
|
34615 element.css({height: 0}); |
|
34616 } else { |
|
34617 // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value |
|
34618 element.css({ height: element[0].scrollHeight + 'px' }); |
|
34619 //trigger reflow so a browser realizes that height was updated from auto to a specific value |
|
34620 var x = element[0].offsetWidth; |
|
34621 |
|
34622 element.removeClass('collapse in').addClass('collapsing'); |
|
34623 |
|
34624 doTransition({ height: 0 }).then(collapseDone); |
|
34625 } |
|
34626 } |
|
34627 |
|
34628 function collapseDone() { |
|
34629 element.removeClass('collapsing'); |
|
34630 element.addClass('collapse'); |
|
34631 } |
|
34632 |
|
34633 scope.$watch(attrs.collapse, function (shouldCollapse) { |
|
34634 if (shouldCollapse) { |
|
34635 collapse(); |
|
34636 } else { |
|
34637 expand(); |
|
34638 } |
|
34639 }); |
|
34640 } |
|
34641 }; |
|
34642 }]); |
|
34643 |
|
34644 angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) |
|
34645 |
|
34646 .constant('accordionConfig', { |
|
34647 closeOthers: true |
|
34648 }) |
|
34649 |
|
34650 .controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { |
|
34651 |
|
34652 // This array keeps track of the accordion groups |
|
34653 this.groups = []; |
|
34654 |
|
34655 // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to |
|
34656 this.closeOthers = function(openGroup) { |
|
34657 var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; |
|
34658 if ( closeOthers ) { |
|
34659 angular.forEach(this.groups, function (group) { |
|
34660 if ( group !== openGroup ) { |
|
34661 group.isOpen = false; |
|
34662 } |
|
34663 }); |
|
34664 } |
|
34665 }; |
|
34666 |
|
34667 // This is called from the accordion-group directive to add itself to the accordion |
|
34668 this.addGroup = function(groupScope) { |
|
34669 var that = this; |
|
34670 this.groups.push(groupScope); |
|
34671 |
|
34672 groupScope.$on('$destroy', function (event) { |
|
34673 that.removeGroup(groupScope); |
|
34674 }); |
|
34675 }; |
|
34676 |
|
34677 // This is called from the accordion-group directive when to remove itself |
|
34678 this.removeGroup = function(group) { |
|
34679 var index = this.groups.indexOf(group); |
|
34680 if ( index !== -1 ) { |
|
34681 this.groups.splice(index, 1); |
|
34682 } |
|
34683 }; |
|
34684 |
|
34685 }]) |
|
34686 |
|
34687 // The accordion directive simply sets up the directive controller |
|
34688 // and adds an accordion CSS class to itself element. |
|
34689 .directive('accordion', function () { |
|
34690 return { |
|
34691 restrict:'EA', |
|
34692 controller:'AccordionController', |
|
34693 transclude: true, |
|
34694 replace: false, |
|
34695 templateUrl: 'template/accordion/accordion.html' |
|
34696 }; |
|
34697 }) |
|
34698 |
|
34699 // The accordion-group directive indicates a block of html that will expand and collapse in an accordion |
|
34700 .directive('accordionGroup', function() { |
|
34701 return { |
|
34702 require:'^accordion', // We need this directive to be inside an accordion |
|
34703 restrict:'EA', |
|
34704 transclude:true, // It transcludes the contents of the directive into the template |
|
34705 replace: true, // The element containing the directive will be replaced with the template |
|
34706 templateUrl:'template/accordion/accordion-group.html', |
|
34707 scope: { |
|
34708 heading: '@', // Interpolate the heading attribute onto this scope |
|
34709 isOpen: '=?', |
|
34710 isDisabled: '=?' |
|
34711 }, |
|
34712 controller: function() { |
|
34713 this.setHeading = function(element) { |
|
34714 this.heading = element; |
|
34715 }; |
|
34716 }, |
|
34717 link: function(scope, element, attrs, accordionCtrl) { |
|
34718 accordionCtrl.addGroup(scope); |
|
34719 |
|
34720 scope.$watch('isOpen', function(value) { |
|
34721 if ( value ) { |
|
34722 accordionCtrl.closeOthers(scope); |
|
34723 } |
|
34724 }); |
|
34725 |
|
34726 scope.toggleOpen = function() { |
|
34727 if ( !scope.isDisabled ) { |
|
34728 scope.isOpen = !scope.isOpen; |
|
34729 } |
|
34730 }; |
|
34731 } |
|
34732 }; |
|
34733 }) |
|
34734 |
|
34735 // Use accordion-heading below an accordion-group to provide a heading containing HTML |
|
34736 // <accordion-group> |
|
34737 // <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading> |
|
34738 // </accordion-group> |
|
34739 .directive('accordionHeading', function() { |
|
34740 return { |
|
34741 restrict: 'EA', |
|
34742 transclude: true, // Grab the contents to be used as the heading |
|
34743 template: '', // In effect remove this element! |
|
34744 replace: true, |
|
34745 require: '^accordionGroup', |
|
34746 link: function(scope, element, attr, accordionGroupCtrl, transclude) { |
|
34747 // Pass the heading to the accordion-group controller |
|
34748 // so that it can be transcluded into the right place in the template |
|
34749 // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] |
|
34750 accordionGroupCtrl.setHeading(transclude(scope, function() {})); |
|
34751 } |
|
34752 }; |
|
34753 }) |
|
34754 |
|
34755 // Use in the accordion-group template to indicate where you want the heading to be transcluded |
|
34756 // You must provide the property on the accordion-group controller that will hold the transcluded element |
|
34757 // <div class="accordion-group"> |
|
34758 // <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div> |
|
34759 // ... |
|
34760 // </div> |
|
34761 .directive('accordionTransclude', function() { |
|
34762 return { |
|
34763 require: '^accordionGroup', |
|
34764 link: function(scope, element, attr, controller) { |
|
34765 scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { |
|
34766 if ( heading ) { |
|
34767 element.html(''); |
|
34768 element.append(heading); |
|
34769 } |
|
34770 }); |
|
34771 } |
|
34772 }; |
|
34773 }); |
|
34774 |
|
34775 angular.module('ui.bootstrap.alert', []) |
|
34776 |
|
34777 .controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) { |
|
34778 $scope.closeable = 'close' in $attrs; |
|
34779 }]) |
|
34780 |
|
34781 .directive('alert', function () { |
|
34782 return { |
|
34783 restrict:'EA', |
|
34784 controller:'AlertController', |
|
34785 templateUrl:'template/alert/alert.html', |
|
34786 transclude:true, |
|
34787 replace:true, |
|
34788 scope: { |
|
34789 type: '@', |
|
34790 close: '&' |
|
34791 } |
|
34792 }; |
|
34793 }); |
|
34794 |
|
34795 angular.module('ui.bootstrap.bindHtml', []) |
|
34796 |
|
34797 .directive('bindHtmlUnsafe', function () { |
|
34798 return function (scope, element, attr) { |
|
34799 element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); |
|
34800 scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { |
|
34801 element.html(value || ''); |
|
34802 }); |
|
34803 }; |
|
34804 }); |
|
34805 angular.module('ui.bootstrap.buttons', []) |
|
34806 |
|
34807 .constant('buttonConfig', { |
|
34808 activeClass: 'active', |
|
34809 toggleEvent: 'click' |
|
34810 }) |
|
34811 |
|
34812 .controller('ButtonsController', ['buttonConfig', function(buttonConfig) { |
|
34813 this.activeClass = buttonConfig.activeClass || 'active'; |
|
34814 this.toggleEvent = buttonConfig.toggleEvent || 'click'; |
|
34815 }]) |
|
34816 |
|
34817 .directive('btnRadio', function () { |
|
34818 return { |
|
34819 require: ['btnRadio', 'ngModel'], |
|
34820 controller: 'ButtonsController', |
|
34821 link: function (scope, element, attrs, ctrls) { |
|
34822 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; |
|
34823 |
|
34824 //model -> UI |
|
34825 ngModelCtrl.$render = function () { |
|
34826 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); |
|
34827 }; |
|
34828 |
|
34829 //ui->model |
|
34830 element.bind(buttonsCtrl.toggleEvent, function () { |
|
34831 var isActive = element.hasClass(buttonsCtrl.activeClass); |
|
34832 |
|
34833 if (!isActive || angular.isDefined(attrs.uncheckable)) { |
|
34834 scope.$apply(function () { |
|
34835 ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); |
|
34836 ngModelCtrl.$render(); |
|
34837 }); |
|
34838 } |
|
34839 }); |
|
34840 } |
|
34841 }; |
|
34842 }) |
|
34843 |
|
34844 .directive('btnCheckbox', function () { |
|
34845 return { |
|
34846 require: ['btnCheckbox', 'ngModel'], |
|
34847 controller: 'ButtonsController', |
|
34848 link: function (scope, element, attrs, ctrls) { |
|
34849 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; |
|
34850 |
|
34851 function getTrueValue() { |
|
34852 return getCheckboxValue(attrs.btnCheckboxTrue, true); |
|
34853 } |
|
34854 |
|
34855 function getFalseValue() { |
|
34856 return getCheckboxValue(attrs.btnCheckboxFalse, false); |
|
34857 } |
|
34858 |
|
34859 function getCheckboxValue(attributeValue, defaultValue) { |
|
34860 var val = scope.$eval(attributeValue); |
|
34861 return angular.isDefined(val) ? val : defaultValue; |
|
34862 } |
|
34863 |
|
34864 //model -> UI |
|
34865 ngModelCtrl.$render = function () { |
|
34866 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); |
|
34867 }; |
|
34868 |
|
34869 //ui->model |
|
34870 element.bind(buttonsCtrl.toggleEvent, function () { |
|
34871 scope.$apply(function () { |
|
34872 ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); |
|
34873 ngModelCtrl.$render(); |
|
34874 }); |
|
34875 }); |
|
34876 } |
|
34877 }; |
|
34878 }); |
|
34879 |
|
34880 /** |
|
34881 * @ngdoc overview |
|
34882 * @name ui.bootstrap.carousel |
|
34883 * |
|
34884 * @description |
|
34885 * AngularJS version of an image carousel. |
|
34886 * |
|
34887 */ |
|
34888 angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) |
|
34889 .controller('CarouselController', ['$scope', '$timeout', '$transition', function ($scope, $timeout, $transition) { |
|
34890 var self = this, |
|
34891 slides = self.slides = $scope.slides = [], |
|
34892 currentIndex = -1, |
|
34893 currentTimeout, isPlaying; |
|
34894 self.currentSlide = null; |
|
34895 |
|
34896 var destroyed = false; |
|
34897 /* direction: "prev" or "next" */ |
|
34898 self.select = $scope.select = function(nextSlide, direction) { |
|
34899 var nextIndex = slides.indexOf(nextSlide); |
|
34900 //Decide direction if it's not given |
|
34901 if (direction === undefined) { |
|
34902 direction = nextIndex > currentIndex ? 'next' : 'prev'; |
|
34903 } |
|
34904 if (nextSlide && nextSlide !== self.currentSlide) { |
|
34905 if ($scope.$currentTransition) { |
|
34906 $scope.$currentTransition.cancel(); |
|
34907 //Timeout so ng-class in template has time to fix classes for finished slide |
|
34908 $timeout(goNext); |
|
34909 } else { |
|
34910 goNext(); |
|
34911 } |
|
34912 } |
|
34913 function goNext() { |
|
34914 // Scope has been destroyed, stop here. |
|
34915 if (destroyed) { return; } |
|
34916 //If we have a slide to transition from and we have a transition type and we're allowed, go |
|
34917 if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { |
|
34918 //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime |
|
34919 nextSlide.$element.addClass(direction); |
|
34920 var reflow = nextSlide.$element[0].offsetWidth; //force reflow |
|
34921 |
|
34922 //Set all other slides to stop doing their stuff for the new transition |
|
34923 angular.forEach(slides, function(slide) { |
|
34924 angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); |
|
34925 }); |
|
34926 angular.extend(nextSlide, {direction: direction, active: true, entering: true}); |
|
34927 angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); |
|
34928 |
|
34929 $scope.$currentTransition = $transition(nextSlide.$element, {}); |
|
34930 //We have to create new pointers inside a closure since next & current will change |
|
34931 (function(next,current) { |
|
34932 $scope.$currentTransition.then( |
|
34933 function(){ transitionDone(next, current); }, |
|
34934 function(){ transitionDone(next, current); } |
|
34935 ); |
|
34936 }(nextSlide, self.currentSlide)); |
|
34937 } else { |
|
34938 transitionDone(nextSlide, self.currentSlide); |
|
34939 } |
|
34940 self.currentSlide = nextSlide; |
|
34941 currentIndex = nextIndex; |
|
34942 //every time you change slides, reset the timer |
|
34943 restartTimer(); |
|
34944 } |
|
34945 function transitionDone(next, current) { |
|
34946 angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); |
|
34947 angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); |
|
34948 $scope.$currentTransition = null; |
|
34949 } |
|
34950 }; |
|
34951 $scope.$on('$destroy', function () { |
|
34952 destroyed = true; |
|
34953 }); |
|
34954 |
|
34955 /* Allow outside people to call indexOf on slides array */ |
|
34956 self.indexOfSlide = function(slide) { |
|
34957 return slides.indexOf(slide); |
|
34958 }; |
|
34959 |
|
34960 $scope.next = function() { |
|
34961 var newIndex = (currentIndex + 1) % slides.length; |
|
34962 |
|
34963 //Prevent this user-triggered transition from occurring if there is already one in progress |
|
34964 if (!$scope.$currentTransition) { |
|
34965 return self.select(slides[newIndex], 'next'); |
|
34966 } |
|
34967 }; |
|
34968 |
|
34969 $scope.prev = function() { |
|
34970 var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; |
|
34971 |
|
34972 //Prevent this user-triggered transition from occurring if there is already one in progress |
|
34973 if (!$scope.$currentTransition) { |
|
34974 return self.select(slides[newIndex], 'prev'); |
|
34975 } |
|
34976 }; |
|
34977 |
|
34978 $scope.isActive = function(slide) { |
|
34979 return self.currentSlide === slide; |
|
34980 }; |
|
34981 |
|
34982 $scope.$watch('interval', restartTimer); |
|
34983 $scope.$on('$destroy', resetTimer); |
|
34984 |
|
34985 function restartTimer() { |
|
34986 resetTimer(); |
|
34987 var interval = +$scope.interval; |
|
34988 if (!isNaN(interval) && interval>=0) { |
|
34989 currentTimeout = $timeout(timerFn, interval); |
|
34990 } |
|
34991 } |
|
34992 |
|
34993 function resetTimer() { |
|
34994 if (currentTimeout) { |
|
34995 $timeout.cancel(currentTimeout); |
|
34996 currentTimeout = null; |
|
34997 } |
|
34998 } |
|
34999 |
|
35000 function timerFn() { |
|
35001 if (isPlaying) { |
|
35002 $scope.next(); |
|
35003 restartTimer(); |
|
35004 } else { |
|
35005 $scope.pause(); |
|
35006 } |
|
35007 } |
|
35008 |
|
35009 $scope.play = function() { |
|
35010 if (!isPlaying) { |
|
35011 isPlaying = true; |
|
35012 restartTimer(); |
|
35013 } |
|
35014 }; |
|
35015 $scope.pause = function() { |
|
35016 if (!$scope.noPause) { |
|
35017 isPlaying = false; |
|
35018 resetTimer(); |
|
35019 } |
|
35020 }; |
|
35021 |
|
35022 self.addSlide = function(slide, element) { |
|
35023 slide.$element = element; |
|
35024 slides.push(slide); |
|
35025 //if this is the first slide or the slide is set to active, select it |
|
35026 if(slides.length === 1 || slide.active) { |
|
35027 self.select(slides[slides.length-1]); |
|
35028 if (slides.length == 1) { |
|
35029 $scope.play(); |
|
35030 } |
|
35031 } else { |
|
35032 slide.active = false; |
|
35033 } |
|
35034 }; |
|
35035 |
|
35036 self.removeSlide = function(slide) { |
|
35037 //get the index of the slide inside the carousel |
|
35038 var index = slides.indexOf(slide); |
|
35039 slides.splice(index, 1); |
|
35040 if (slides.length > 0 && slide.active) { |
|
35041 if (index >= slides.length) { |
|
35042 self.select(slides[index-1]); |
|
35043 } else { |
|
35044 self.select(slides[index]); |
|
35045 } |
|
35046 } else if (currentIndex > index) { |
|
35047 currentIndex--; |
|
35048 } |
|
35049 }; |
|
35050 |
|
35051 }]) |
|
35052 |
|
35053 /** |
|
35054 * @ngdoc directive |
|
35055 * @name ui.bootstrap.carousel.directive:carousel |
|
35056 * @restrict EA |
|
35057 * |
|
35058 * @description |
|
35059 * Carousel is the outer container for a set of image 'slides' to showcase. |
|
35060 * |
|
35061 * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. |
|
35062 * @param {boolean=} noTransition Whether to disable transitions on the carousel. |
|
35063 * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). |
|
35064 * |
|
35065 * @example |
|
35066 <example module="ui.bootstrap"> |
|
35067 <file name="index.html"> |
|
35068 <carousel> |
|
35069 <slide> |
|
35070 <img src="http://placekitten.com/150/150" style="margin:auto;"> |
|
35071 <div class="carousel-caption"> |
|
35072 <p>Beautiful!</p> |
|
35073 </div> |
|
35074 </slide> |
|
35075 <slide> |
|
35076 <img src="http://placekitten.com/100/150" style="margin:auto;"> |
|
35077 <div class="carousel-caption"> |
|
35078 <p>D'aww!</p> |
|
35079 </div> |
|
35080 </slide> |
|
35081 </carousel> |
|
35082 </file> |
|
35083 <file name="demo.css"> |
|
35084 .carousel-indicators { |
|
35085 top: auto; |
|
35086 bottom: 15px; |
|
35087 } |
|
35088 </file> |
|
35089 </example> |
|
35090 */ |
|
35091 .directive('carousel', [function() { |
|
35092 return { |
|
35093 restrict: 'EA', |
|
35094 transclude: true, |
|
35095 replace: true, |
|
35096 controller: 'CarouselController', |
|
35097 require: 'carousel', |
|
35098 templateUrl: 'template/carousel/carousel.html', |
|
35099 scope: { |
|
35100 interval: '=', |
|
35101 noTransition: '=', |
|
35102 noPause: '=' |
|
35103 } |
|
35104 }; |
|
35105 }]) |
|
35106 |
|
35107 /** |
|
35108 * @ngdoc directive |
|
35109 * @name ui.bootstrap.carousel.directive:slide |
|
35110 * @restrict EA |
|
35111 * |
|
35112 * @description |
|
35113 * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. |
|
35114 * |
|
35115 * @param {boolean=} active Model binding, whether or not this slide is currently active. |
|
35116 * |
|
35117 * @example |
|
35118 <example module="ui.bootstrap"> |
|
35119 <file name="index.html"> |
|
35120 <div ng-controller="CarouselDemoCtrl"> |
|
35121 <carousel> |
|
35122 <slide ng-repeat="slide in slides" active="slide.active"> |
|
35123 <img ng-src="{{slide.image}}" style="margin:auto;"> |
|
35124 <div class="carousel-caption"> |
|
35125 <h4>Slide {{$index}}</h4> |
|
35126 <p>{{slide.text}}</p> |
|
35127 </div> |
|
35128 </slide> |
|
35129 </carousel> |
|
35130 Interval, in milliseconds: <input type="number" ng-model="myInterval"> |
|
35131 <br />Enter a negative number to stop the interval. |
|
35132 </div> |
|
35133 </file> |
|
35134 <file name="script.js"> |
|
35135 function CarouselDemoCtrl($scope) { |
|
35136 $scope.myInterval = 5000; |
|
35137 } |
|
35138 </file> |
|
35139 <file name="demo.css"> |
|
35140 .carousel-indicators { |
|
35141 top: auto; |
|
35142 bottom: 15px; |
|
35143 } |
|
35144 </file> |
|
35145 </example> |
|
35146 */ |
|
35147 |
|
35148 .directive('slide', function() { |
|
35149 return { |
|
35150 require: '^carousel', |
|
35151 restrict: 'EA', |
|
35152 transclude: true, |
|
35153 replace: true, |
|
35154 templateUrl: 'template/carousel/slide.html', |
|
35155 scope: { |
|
35156 active: '=?' |
|
35157 }, |
|
35158 link: function (scope, element, attrs, carouselCtrl) { |
|
35159 carouselCtrl.addSlide(scope, element); |
|
35160 //when the scope is destroyed then remove the slide from the current slides array |
|
35161 scope.$on('$destroy', function() { |
|
35162 carouselCtrl.removeSlide(scope); |
|
35163 }); |
|
35164 |
|
35165 scope.$watch('active', function(active) { |
|
35166 if (active) { |
|
35167 carouselCtrl.select(scope); |
|
35168 } |
|
35169 }); |
|
35170 } |
|
35171 }; |
|
35172 }); |
|
35173 |
|
35174 angular.module('ui.bootstrap.dateparser', []) |
|
35175 |
|
35176 .service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) { |
|
35177 |
|
35178 this.parsers = {}; |
|
35179 |
|
35180 var formatCodeToRegex = { |
|
35181 'yyyy': { |
|
35182 regex: '\\d{4}', |
|
35183 apply: function(value) { this.year = +value; } |
|
35184 }, |
|
35185 'yy': { |
|
35186 regex: '\\d{2}', |
|
35187 apply: function(value) { this.year = +value + 2000; } |
|
35188 }, |
|
35189 'y': { |
|
35190 regex: '\\d{1,4}', |
|
35191 apply: function(value) { this.year = +value; } |
|
35192 }, |
|
35193 'MMMM': { |
|
35194 regex: $locale.DATETIME_FORMATS.MONTH.join('|'), |
|
35195 apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } |
|
35196 }, |
|
35197 'MMM': { |
|
35198 regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), |
|
35199 apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } |
|
35200 }, |
|
35201 'MM': { |
|
35202 regex: '0[1-9]|1[0-2]', |
|
35203 apply: function(value) { this.month = value - 1; } |
|
35204 }, |
|
35205 'M': { |
|
35206 regex: '[1-9]|1[0-2]', |
|
35207 apply: function(value) { this.month = value - 1; } |
|
35208 }, |
|
35209 'dd': { |
|
35210 regex: '[0-2][0-9]{1}|3[0-1]{1}', |
|
35211 apply: function(value) { this.date = +value; } |
|
35212 }, |
|
35213 'd': { |
|
35214 regex: '[1-2]?[0-9]{1}|3[0-1]{1}', |
|
35215 apply: function(value) { this.date = +value; } |
|
35216 }, |
|
35217 'EEEE': { |
|
35218 regex: $locale.DATETIME_FORMATS.DAY.join('|') |
|
35219 }, |
|
35220 'EEE': { |
|
35221 regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') |
|
35222 } |
|
35223 }; |
|
35224 |
|
35225 function createParser(format) { |
|
35226 var map = [], regex = format.split(''); |
|
35227 |
|
35228 angular.forEach(formatCodeToRegex, function(data, code) { |
|
35229 var index = format.indexOf(code); |
|
35230 |
|
35231 if (index > -1) { |
|
35232 format = format.split(''); |
|
35233 |
|
35234 regex[index] = '(' + data.regex + ')'; |
|
35235 format[index] = '$'; // Custom symbol to define consumed part of format |
|
35236 for (var i = index + 1, n = index + code.length; i < n; i++) { |
|
35237 regex[i] = ''; |
|
35238 format[i] = '$'; |
|
35239 } |
|
35240 format = format.join(''); |
|
35241 |
|
35242 map.push({ index: index, apply: data.apply }); |
|
35243 } |
|
35244 }); |
|
35245 |
|
35246 return { |
|
35247 regex: new RegExp('^' + regex.join('') + '$'), |
|
35248 map: orderByFilter(map, 'index') |
|
35249 }; |
|
35250 } |
|
35251 |
|
35252 this.parse = function(input, format) { |
|
35253 if ( !angular.isString(input) || !format ) { |
|
35254 return input; |
|
35255 } |
|
35256 |
|
35257 format = $locale.DATETIME_FORMATS[format] || format; |
|
35258 |
|
35259 if ( !this.parsers[format] ) { |
|
35260 this.parsers[format] = createParser(format); |
|
35261 } |
|
35262 |
|
35263 var parser = this.parsers[format], |
|
35264 regex = parser.regex, |
|
35265 map = parser.map, |
|
35266 results = input.match(regex); |
|
35267 |
|
35268 if ( results && results.length ) { |
|
35269 var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt; |
|
35270 |
|
35271 for( var i = 1, n = results.length; i < n; i++ ) { |
|
35272 var mapper = map[i-1]; |
|
35273 if ( mapper.apply ) { |
|
35274 mapper.apply.call(fields, results[i]); |
|
35275 } |
|
35276 } |
|
35277 |
|
35278 if ( isValid(fields.year, fields.month, fields.date) ) { |
|
35279 dt = new Date( fields.year, fields.month, fields.date, fields.hours); |
|
35280 } |
|
35281 |
|
35282 return dt; |
|
35283 } |
|
35284 }; |
|
35285 |
|
35286 // Check if date is valid for specific month (and year for February). |
|
35287 // Month: 0 = Jan, 1 = Feb, etc |
|
35288 function isValid(year, month, date) { |
|
35289 if ( month === 1 && date > 28) { |
|
35290 return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); |
|
35291 } |
|
35292 |
|
35293 if ( month === 3 || month === 5 || month === 8 || month === 10) { |
|
35294 return date < 31; |
|
35295 } |
|
35296 |
|
35297 return true; |
|
35298 } |
|
35299 }]); |
|
35300 |
|
35301 angular.module('ui.bootstrap.position', []) |
|
35302 |
|
35303 /** |
|
35304 * A set of utility methods that can be use to retrieve position of DOM elements. |
|
35305 * It is meant to be used where we need to absolute-position DOM elements in |
|
35306 * relation to other, existing elements (this is the case for tooltips, popovers, |
|
35307 * typeahead suggestions etc.). |
|
35308 */ |
|
35309 .factory('$position', ['$document', '$window', function ($document, $window) { |
|
35310 |
|
35311 function getStyle(el, cssprop) { |
|
35312 if (el.currentStyle) { //IE |
|
35313 return el.currentStyle[cssprop]; |
|
35314 } else if ($window.getComputedStyle) { |
|
35315 return $window.getComputedStyle(el)[cssprop]; |
|
35316 } |
|
35317 // finally try and get inline style |
|
35318 return el.style[cssprop]; |
|
35319 } |
|
35320 |
|
35321 /** |
|
35322 * Checks if a given element is statically positioned |
|
35323 * @param element - raw DOM element |
|
35324 */ |
|
35325 function isStaticPositioned(element) { |
|
35326 return (getStyle(element, 'position') || 'static' ) === 'static'; |
|
35327 } |
|
35328 |
|
35329 /** |
|
35330 * returns the closest, non-statically positioned parentOffset of a given element |
|
35331 * @param element |
|
35332 */ |
|
35333 var parentOffsetEl = function (element) { |
|
35334 var docDomEl = $document[0]; |
|
35335 var offsetParent = element.offsetParent || docDomEl; |
|
35336 while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { |
|
35337 offsetParent = offsetParent.offsetParent; |
|
35338 } |
|
35339 return offsetParent || docDomEl; |
|
35340 }; |
|
35341 |
|
35342 return { |
|
35343 /** |
|
35344 * Provides read-only equivalent of jQuery's position function: |
|
35345 * http://api.jquery.com/position/ |
|
35346 */ |
|
35347 position: function (element) { |
|
35348 var elBCR = this.offset(element); |
|
35349 var offsetParentBCR = { top: 0, left: 0 }; |
|
35350 var offsetParentEl = parentOffsetEl(element[0]); |
|
35351 if (offsetParentEl != $document[0]) { |
|
35352 offsetParentBCR = this.offset(angular.element(offsetParentEl)); |
|
35353 offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; |
|
35354 offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; |
|
35355 } |
|
35356 |
|
35357 var boundingClientRect = element[0].getBoundingClientRect(); |
|
35358 return { |
|
35359 width: boundingClientRect.width || element.prop('offsetWidth'), |
|
35360 height: boundingClientRect.height || element.prop('offsetHeight'), |
|
35361 top: elBCR.top - offsetParentBCR.top, |
|
35362 left: elBCR.left - offsetParentBCR.left |
|
35363 }; |
|
35364 }, |
|
35365 |
|
35366 /** |
|
35367 * Provides read-only equivalent of jQuery's offset function: |
|
35368 * http://api.jquery.com/offset/ |
|
35369 */ |
|
35370 offset: function (element) { |
|
35371 var boundingClientRect = element[0].getBoundingClientRect(); |
|
35372 return { |
|
35373 width: boundingClientRect.width || element.prop('offsetWidth'), |
|
35374 height: boundingClientRect.height || element.prop('offsetHeight'), |
|
35375 top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), |
|
35376 left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) |
|
35377 }; |
|
35378 }, |
|
35379 |
|
35380 /** |
|
35381 * Provides coordinates for the targetEl in relation to hostEl |
|
35382 */ |
|
35383 positionElements: function (hostEl, targetEl, positionStr, appendToBody) { |
|
35384 |
|
35385 var positionStrParts = positionStr.split('-'); |
|
35386 var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; |
|
35387 |
|
35388 var hostElPos, |
|
35389 targetElWidth, |
|
35390 targetElHeight, |
|
35391 targetElPos; |
|
35392 |
|
35393 hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); |
|
35394 |
|
35395 targetElWidth = targetEl.prop('offsetWidth'); |
|
35396 targetElHeight = targetEl.prop('offsetHeight'); |
|
35397 |
|
35398 var shiftWidth = { |
|
35399 center: function () { |
|
35400 return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; |
|
35401 }, |
|
35402 left: function () { |
|
35403 return hostElPos.left; |
|
35404 }, |
|
35405 right: function () { |
|
35406 return hostElPos.left + hostElPos.width; |
|
35407 } |
|
35408 }; |
|
35409 |
|
35410 var shiftHeight = { |
|
35411 center: function () { |
|
35412 return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; |
|
35413 }, |
|
35414 top: function () { |
|
35415 return hostElPos.top; |
|
35416 }, |
|
35417 bottom: function () { |
|
35418 return hostElPos.top + hostElPos.height; |
|
35419 } |
|
35420 }; |
|
35421 |
|
35422 switch (pos0) { |
|
35423 case 'right': |
|
35424 targetElPos = { |
|
35425 top: shiftHeight[pos1](), |
|
35426 left: shiftWidth[pos0]() |
|
35427 }; |
|
35428 break; |
|
35429 case 'left': |
|
35430 targetElPos = { |
|
35431 top: shiftHeight[pos1](), |
|
35432 left: hostElPos.left - targetElWidth |
|
35433 }; |
|
35434 break; |
|
35435 case 'bottom': |
|
35436 targetElPos = { |
|
35437 top: shiftHeight[pos0](), |
|
35438 left: shiftWidth[pos1]() |
|
35439 }; |
|
35440 break; |
|
35441 default: |
|
35442 targetElPos = { |
|
35443 top: hostElPos.top - targetElHeight, |
|
35444 left: shiftWidth[pos1]() |
|
35445 }; |
|
35446 break; |
|
35447 } |
|
35448 |
|
35449 return targetElPos; |
|
35450 } |
|
35451 }; |
|
35452 }]); |
|
35453 |
|
35454 angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) |
|
35455 |
|
35456 .constant('datepickerConfig', { |
|
35457 formatDay: 'dd', |
|
35458 formatMonth: 'MMMM', |
|
35459 formatYear: 'yyyy', |
|
35460 formatDayHeader: 'EEE', |
|
35461 formatDayTitle: 'MMMM yyyy', |
|
35462 formatMonthTitle: 'yyyy', |
|
35463 datepickerMode: 'day', |
|
35464 minMode: 'day', |
|
35465 maxMode: 'year', |
|
35466 showWeeks: true, |
|
35467 startingDay: 0, |
|
35468 yearRange: 20, |
|
35469 minDate: null, |
|
35470 maxDate: null |
|
35471 }) |
|
35472 |
|
35473 .controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { |
|
35474 var self = this, |
|
35475 ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; |
|
35476 |
|
35477 // Modes chain |
|
35478 this.modes = ['day', 'month', 'year']; |
|
35479 |
|
35480 // Configuration attributes |
|
35481 angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', |
|
35482 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { |
|
35483 self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; |
|
35484 }); |
|
35485 |
|
35486 // Watchable date attributes |
|
35487 angular.forEach(['minDate', 'maxDate'], function( key ) { |
|
35488 if ( $attrs[key] ) { |
|
35489 $scope.$parent.$watch($parse($attrs[key]), function(value) { |
|
35490 self[key] = value ? new Date(value) : null; |
|
35491 self.refreshView(); |
|
35492 }); |
|
35493 } else { |
|
35494 self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; |
|
35495 } |
|
35496 }); |
|
35497 |
|
35498 $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; |
|
35499 $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); |
|
35500 this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); |
|
35501 |
|
35502 $scope.isActive = function(dateObject) { |
|
35503 if (self.compare(dateObject.date, self.activeDate) === 0) { |
|
35504 $scope.activeDateId = dateObject.uid; |
|
35505 return true; |
|
35506 } |
|
35507 return false; |
|
35508 }; |
|
35509 |
|
35510 this.init = function( ngModelCtrl_ ) { |
|
35511 ngModelCtrl = ngModelCtrl_; |
|
35512 |
|
35513 ngModelCtrl.$render = function() { |
|
35514 self.render(); |
|
35515 }; |
|
35516 }; |
|
35517 |
|
35518 this.render = function() { |
|
35519 if ( ngModelCtrl.$modelValue ) { |
|
35520 var date = new Date( ngModelCtrl.$modelValue ), |
|
35521 isValid = !isNaN(date); |
|
35522 |
|
35523 if ( isValid ) { |
|
35524 this.activeDate = date; |
|
35525 } else { |
|
35526 $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); |
|
35527 } |
|
35528 ngModelCtrl.$setValidity('date', isValid); |
|
35529 } |
|
35530 this.refreshView(); |
|
35531 }; |
|
35532 |
|
35533 this.refreshView = function() { |
|
35534 if ( this.element ) { |
|
35535 this._refreshView(); |
|
35536 |
|
35537 var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; |
|
35538 ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); |
|
35539 } |
|
35540 }; |
|
35541 |
|
35542 this.createDateObject = function(date, format) { |
|
35543 var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; |
|
35544 return { |
|
35545 date: date, |
|
35546 label: dateFilter(date, format), |
|
35547 selected: model && this.compare(date, model) === 0, |
|
35548 disabled: this.isDisabled(date), |
|
35549 current: this.compare(date, new Date()) === 0 |
|
35550 }; |
|
35551 }; |
|
35552 |
|
35553 this.isDisabled = function( date ) { |
|
35554 return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); |
|
35555 }; |
|
35556 |
|
35557 // Split array into smaller arrays |
|
35558 this.split = function(arr, size) { |
|
35559 var arrays = []; |
|
35560 while (arr.length > 0) { |
|
35561 arrays.push(arr.splice(0, size)); |
|
35562 } |
|
35563 return arrays; |
|
35564 }; |
|
35565 |
|
35566 $scope.select = function( date ) { |
|
35567 if ( $scope.datepickerMode === self.minMode ) { |
|
35568 var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0); |
|
35569 dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); |
|
35570 ngModelCtrl.$setViewValue( dt ); |
|
35571 ngModelCtrl.$render(); |
|
35572 } else { |
|
35573 self.activeDate = date; |
|
35574 $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; |
|
35575 } |
|
35576 }; |
|
35577 |
|
35578 $scope.move = function( direction ) { |
|
35579 var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), |
|
35580 month = self.activeDate.getMonth() + direction * (self.step.months || 0); |
|
35581 self.activeDate.setFullYear(year, month, 1); |
|
35582 self.refreshView(); |
|
35583 }; |
|
35584 |
|
35585 $scope.toggleMode = function( direction ) { |
|
35586 direction = direction || 1; |
|
35587 |
|
35588 if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { |
|
35589 return; |
|
35590 } |
|
35591 |
|
35592 $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; |
|
35593 }; |
|
35594 |
|
35595 // Key event mapper |
|
35596 $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; |
|
35597 |
|
35598 var focusElement = function() { |
|
35599 $timeout(function() { |
|
35600 self.element[0].focus(); |
|
35601 }, 0 , false); |
|
35602 }; |
|
35603 |
|
35604 // Listen for focus requests from popup directive |
|
35605 $scope.$on('datepicker.focus', focusElement); |
|
35606 |
|
35607 $scope.keydown = function( evt ) { |
|
35608 var key = $scope.keys[evt.which]; |
|
35609 |
|
35610 if ( !key || evt.shiftKey || evt.altKey ) { |
|
35611 return; |
|
35612 } |
|
35613 |
|
35614 evt.preventDefault(); |
|
35615 evt.stopPropagation(); |
|
35616 |
|
35617 if (key === 'enter' || key === 'space') { |
|
35618 if ( self.isDisabled(self.activeDate)) { |
|
35619 return; // do nothing |
|
35620 } |
|
35621 $scope.select(self.activeDate); |
|
35622 focusElement(); |
|
35623 } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { |
|
35624 $scope.toggleMode(key === 'up' ? 1 : -1); |
|
35625 focusElement(); |
|
35626 } else { |
|
35627 self.handleKeyDown(key, evt); |
|
35628 self.refreshView(); |
|
35629 } |
|
35630 }; |
|
35631 }]) |
|
35632 |
|
35633 .directive( 'datepicker', function () { |
|
35634 return { |
|
35635 restrict: 'EA', |
|
35636 replace: true, |
|
35637 templateUrl: 'template/datepicker/datepicker.html', |
|
35638 scope: { |
|
35639 datepickerMode: '=?', |
|
35640 dateDisabled: '&' |
|
35641 }, |
|
35642 require: ['datepicker', '?^ngModel'], |
|
35643 controller: 'DatepickerController', |
|
35644 link: function(scope, element, attrs, ctrls) { |
|
35645 var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; |
|
35646 |
|
35647 if ( ngModelCtrl ) { |
|
35648 datepickerCtrl.init( ngModelCtrl ); |
|
35649 } |
|
35650 } |
|
35651 }; |
|
35652 }) |
|
35653 |
|
35654 .directive('daypicker', ['dateFilter', function (dateFilter) { |
|
35655 return { |
|
35656 restrict: 'EA', |
|
35657 replace: true, |
|
35658 templateUrl: 'template/datepicker/day.html', |
|
35659 require: '^datepicker', |
|
35660 link: function(scope, element, attrs, ctrl) { |
|
35661 scope.showWeeks = ctrl.showWeeks; |
|
35662 |
|
35663 ctrl.step = { months: 1 }; |
|
35664 ctrl.element = element; |
|
35665 |
|
35666 var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; |
|
35667 function getDaysInMonth( year, month ) { |
|
35668 return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; |
|
35669 } |
|
35670 |
|
35671 function getDates(startDate, n) { |
|
35672 var dates = new Array(n), current = new Date(startDate), i = 0; |
|
35673 current.setHours(12); // Prevent repeated dates because of timezone bug |
|
35674 while ( i < n ) { |
|
35675 dates[i++] = new Date(current); |
|
35676 current.setDate( current.getDate() + 1 ); |
|
35677 } |
|
35678 return dates; |
|
35679 } |
|
35680 |
|
35681 ctrl._refreshView = function() { |
|
35682 var year = ctrl.activeDate.getFullYear(), |
|
35683 month = ctrl.activeDate.getMonth(), |
|
35684 firstDayOfMonth = new Date(year, month, 1), |
|
35685 difference = ctrl.startingDay - firstDayOfMonth.getDay(), |
|
35686 numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, |
|
35687 firstDate = new Date(firstDayOfMonth); |
|
35688 |
|
35689 if ( numDisplayedFromPreviousMonth > 0 ) { |
|
35690 firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); |
|
35691 } |
|
35692 |
|
35693 // 42 is the number of days on a six-month calendar |
|
35694 var days = getDates(firstDate, 42); |
|
35695 for (var i = 0; i < 42; i ++) { |
|
35696 days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { |
|
35697 secondary: days[i].getMonth() !== month, |
|
35698 uid: scope.uniqueId + '-' + i |
|
35699 }); |
|
35700 } |
|
35701 |
|
35702 scope.labels = new Array(7); |
|
35703 for (var j = 0; j < 7; j++) { |
|
35704 scope.labels[j] = { |
|
35705 abbr: dateFilter(days[j].date, ctrl.formatDayHeader), |
|
35706 full: dateFilter(days[j].date, 'EEEE') |
|
35707 }; |
|
35708 } |
|
35709 |
|
35710 scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); |
|
35711 scope.rows = ctrl.split(days, 7); |
|
35712 |
|
35713 if ( scope.showWeeks ) { |
|
35714 scope.weekNumbers = []; |
|
35715 var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ), |
|
35716 numWeeks = scope.rows.length; |
|
35717 while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {} |
|
35718 } |
|
35719 }; |
|
35720 |
|
35721 ctrl.compare = function(date1, date2) { |
|
35722 return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); |
|
35723 }; |
|
35724 |
|
35725 function getISO8601WeekNumber(date) { |
|
35726 var checkDate = new Date(date); |
|
35727 checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday |
|
35728 var time = checkDate.getTime(); |
|
35729 checkDate.setMonth(0); // Compare with Jan 1 |
|
35730 checkDate.setDate(1); |
|
35731 return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; |
|
35732 } |
|
35733 |
|
35734 ctrl.handleKeyDown = function( key, evt ) { |
|
35735 var date = ctrl.activeDate.getDate(); |
|
35736 |
|
35737 if (key === 'left') { |
|
35738 date = date - 1; // up |
|
35739 } else if (key === 'up') { |
|
35740 date = date - 7; // down |
|
35741 } else if (key === 'right') { |
|
35742 date = date + 1; // down |
|
35743 } else if (key === 'down') { |
|
35744 date = date + 7; |
|
35745 } else if (key === 'pageup' || key === 'pagedown') { |
|
35746 var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); |
|
35747 ctrl.activeDate.setMonth(month, 1); |
|
35748 date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); |
|
35749 } else if (key === 'home') { |
|
35750 date = 1; |
|
35751 } else if (key === 'end') { |
|
35752 date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); |
|
35753 } |
|
35754 ctrl.activeDate.setDate(date); |
|
35755 }; |
|
35756 |
|
35757 ctrl.refreshView(); |
|
35758 } |
|
35759 }; |
|
35760 }]) |
|
35761 |
|
35762 .directive('monthpicker', ['dateFilter', function (dateFilter) { |
|
35763 return { |
|
35764 restrict: 'EA', |
|
35765 replace: true, |
|
35766 templateUrl: 'template/datepicker/month.html', |
|
35767 require: '^datepicker', |
|
35768 link: function(scope, element, attrs, ctrl) { |
|
35769 ctrl.step = { years: 1 }; |
|
35770 ctrl.element = element; |
|
35771 |
|
35772 ctrl._refreshView = function() { |
|
35773 var months = new Array(12), |
|
35774 year = ctrl.activeDate.getFullYear(); |
|
35775 |
|
35776 for ( var i = 0; i < 12; i++ ) { |
|
35777 months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { |
|
35778 uid: scope.uniqueId + '-' + i |
|
35779 }); |
|
35780 } |
|
35781 |
|
35782 scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); |
|
35783 scope.rows = ctrl.split(months, 3); |
|
35784 }; |
|
35785 |
|
35786 ctrl.compare = function(date1, date2) { |
|
35787 return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); |
|
35788 }; |
|
35789 |
|
35790 ctrl.handleKeyDown = function( key, evt ) { |
|
35791 var date = ctrl.activeDate.getMonth(); |
|
35792 |
|
35793 if (key === 'left') { |
|
35794 date = date - 1; // up |
|
35795 } else if (key === 'up') { |
|
35796 date = date - 3; // down |
|
35797 } else if (key === 'right') { |
|
35798 date = date + 1; // down |
|
35799 } else if (key === 'down') { |
|
35800 date = date + 3; |
|
35801 } else if (key === 'pageup' || key === 'pagedown') { |
|
35802 var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); |
|
35803 ctrl.activeDate.setFullYear(year); |
|
35804 } else if (key === 'home') { |
|
35805 date = 0; |
|
35806 } else if (key === 'end') { |
|
35807 date = 11; |
|
35808 } |
|
35809 ctrl.activeDate.setMonth(date); |
|
35810 }; |
|
35811 |
|
35812 ctrl.refreshView(); |
|
35813 } |
|
35814 }; |
|
35815 }]) |
|
35816 |
|
35817 .directive('yearpicker', ['dateFilter', function (dateFilter) { |
|
35818 return { |
|
35819 restrict: 'EA', |
|
35820 replace: true, |
|
35821 templateUrl: 'template/datepicker/year.html', |
|
35822 require: '^datepicker', |
|
35823 link: function(scope, element, attrs, ctrl) { |
|
35824 var range = ctrl.yearRange; |
|
35825 |
|
35826 ctrl.step = { years: range }; |
|
35827 ctrl.element = element; |
|
35828 |
|
35829 function getStartingYear( year ) { |
|
35830 return parseInt((year - 1) / range, 10) * range + 1; |
|
35831 } |
|
35832 |
|
35833 ctrl._refreshView = function() { |
|
35834 var years = new Array(range); |
|
35835 |
|
35836 for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { |
|
35837 years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { |
|
35838 uid: scope.uniqueId + '-' + i |
|
35839 }); |
|
35840 } |
|
35841 |
|
35842 scope.title = [years[0].label, years[range - 1].label].join(' - '); |
|
35843 scope.rows = ctrl.split(years, 5); |
|
35844 }; |
|
35845 |
|
35846 ctrl.compare = function(date1, date2) { |
|
35847 return date1.getFullYear() - date2.getFullYear(); |
|
35848 }; |
|
35849 |
|
35850 ctrl.handleKeyDown = function( key, evt ) { |
|
35851 var date = ctrl.activeDate.getFullYear(); |
|
35852 |
|
35853 if (key === 'left') { |
|
35854 date = date - 1; // up |
|
35855 } else if (key === 'up') { |
|
35856 date = date - 5; // down |
|
35857 } else if (key === 'right') { |
|
35858 date = date + 1; // down |
|
35859 } else if (key === 'down') { |
|
35860 date = date + 5; |
|
35861 } else if (key === 'pageup' || key === 'pagedown') { |
|
35862 date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; |
|
35863 } else if (key === 'home') { |
|
35864 date = getStartingYear( ctrl.activeDate.getFullYear() ); |
|
35865 } else if (key === 'end') { |
|
35866 date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; |
|
35867 } |
|
35868 ctrl.activeDate.setFullYear(date); |
|
35869 }; |
|
35870 |
|
35871 ctrl.refreshView(); |
|
35872 } |
|
35873 }; |
|
35874 }]) |
|
35875 |
|
35876 .constant('datepickerPopupConfig', { |
|
35877 datepickerPopup: 'yyyy-MM-dd', |
|
35878 currentText: 'Today', |
|
35879 clearText: 'Clear', |
|
35880 closeText: 'Done', |
|
35881 closeOnDateSelection: true, |
|
35882 appendToBody: false, |
|
35883 showButtonBar: true |
|
35884 }) |
|
35885 |
|
35886 .directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', |
|
35887 function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { |
|
35888 return { |
|
35889 restrict: 'EA', |
|
35890 require: 'ngModel', |
|
35891 scope: { |
|
35892 isOpen: '=?', |
|
35893 currentText: '@', |
|
35894 clearText: '@', |
|
35895 closeText: '@', |
|
35896 dateDisabled: '&' |
|
35897 }, |
|
35898 link: function(scope, element, attrs, ngModel) { |
|
35899 var dateFormat, |
|
35900 closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, |
|
35901 appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; |
|
35902 |
|
35903 scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; |
|
35904 |
|
35905 scope.getText = function( key ) { |
|
35906 return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; |
|
35907 }; |
|
35908 |
|
35909 attrs.$observe('datepickerPopup', function(value) { |
|
35910 dateFormat = value || datepickerPopupConfig.datepickerPopup; |
|
35911 ngModel.$render(); |
|
35912 }); |
|
35913 |
|
35914 // popup element used to display calendar |
|
35915 var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>'); |
|
35916 popupEl.attr({ |
|
35917 'ng-model': 'date', |
|
35918 'ng-change': 'dateSelection()' |
|
35919 }); |
|
35920 |
|
35921 function cameltoDash( string ){ |
|
35922 return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); |
|
35923 } |
|
35924 |
|
35925 // datepicker element |
|
35926 var datepickerEl = angular.element(popupEl.children()[0]); |
|
35927 if ( attrs.datepickerOptions ) { |
|
35928 angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) { |
|
35929 datepickerEl.attr( cameltoDash(option), value ); |
|
35930 }); |
|
35931 } |
|
35932 |
|
35933 scope.watchData = {}; |
|
35934 angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) { |
|
35935 if ( attrs[key] ) { |
|
35936 var getAttribute = $parse(attrs[key]); |
|
35937 scope.$parent.$watch(getAttribute, function(value){ |
|
35938 scope.watchData[key] = value; |
|
35939 }); |
|
35940 datepickerEl.attr(cameltoDash(key), 'watchData.' + key); |
|
35941 |
|
35942 // Propagate changes from datepicker to outside |
|
35943 if ( key === 'datepickerMode' ) { |
|
35944 var setAttribute = getAttribute.assign; |
|
35945 scope.$watch('watchData.' + key, function(value, oldvalue) { |
|
35946 if ( value !== oldvalue ) { |
|
35947 setAttribute(scope.$parent, value); |
|
35948 } |
|
35949 }); |
|
35950 } |
|
35951 } |
|
35952 }); |
|
35953 if (attrs.dateDisabled) { |
|
35954 datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); |
|
35955 } |
|
35956 |
|
35957 function parseDate(viewValue) { |
|
35958 if (!viewValue) { |
|
35959 ngModel.$setValidity('date', true); |
|
35960 return null; |
|
35961 } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { |
|
35962 ngModel.$setValidity('date', true); |
|
35963 return viewValue; |
|
35964 } else if (angular.isString(viewValue)) { |
|
35965 var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); |
|
35966 if (isNaN(date)) { |
|
35967 ngModel.$setValidity('date', false); |
|
35968 return undefined; |
|
35969 } else { |
|
35970 ngModel.$setValidity('date', true); |
|
35971 return date; |
|
35972 } |
|
35973 } else { |
|
35974 ngModel.$setValidity('date', false); |
|
35975 return undefined; |
|
35976 } |
|
35977 } |
|
35978 ngModel.$parsers.unshift(parseDate); |
|
35979 |
|
35980 // Inner change |
|
35981 scope.dateSelection = function(dt) { |
|
35982 if (angular.isDefined(dt)) { |
|
35983 scope.date = dt; |
|
35984 } |
|
35985 ngModel.$setViewValue(scope.date); |
|
35986 ngModel.$render(); |
|
35987 |
|
35988 if ( closeOnDateSelection ) { |
|
35989 scope.isOpen = false; |
|
35990 element[0].focus(); |
|
35991 } |
|
35992 }; |
|
35993 |
|
35994 element.bind('input change keyup', function() { |
|
35995 scope.$apply(function() { |
|
35996 scope.date = ngModel.$modelValue; |
|
35997 }); |
|
35998 }); |
|
35999 |
|
36000 // Outter change |
|
36001 ngModel.$render = function() { |
|
36002 var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; |
|
36003 element.val(date); |
|
36004 scope.date = parseDate( ngModel.$modelValue ); |
|
36005 }; |
|
36006 |
|
36007 var documentClickBind = function(event) { |
|
36008 if (scope.isOpen && event.target !== element[0]) { |
|
36009 scope.$apply(function() { |
|
36010 scope.isOpen = false; |
|
36011 }); |
|
36012 } |
|
36013 }; |
|
36014 |
|
36015 var keydown = function(evt, noApply) { |
|
36016 scope.keydown(evt); |
|
36017 }; |
|
36018 element.bind('keydown', keydown); |
|
36019 |
|
36020 scope.keydown = function(evt) { |
|
36021 if (evt.which === 27) { |
|
36022 evt.preventDefault(); |
|
36023 evt.stopPropagation(); |
|
36024 scope.close(); |
|
36025 } else if (evt.which === 40 && !scope.isOpen) { |
|
36026 scope.isOpen = true; |
|
36027 } |
|
36028 }; |
|
36029 |
|
36030 scope.$watch('isOpen', function(value) { |
|
36031 if (value) { |
|
36032 scope.$broadcast('datepicker.focus'); |
|
36033 scope.position = appendToBody ? $position.offset(element) : $position.position(element); |
|
36034 scope.position.top = scope.position.top + element.prop('offsetHeight'); |
|
36035 |
|
36036 $document.bind('click', documentClickBind); |
|
36037 } else { |
|
36038 $document.unbind('click', documentClickBind); |
|
36039 } |
|
36040 }); |
|
36041 |
|
36042 scope.select = function( date ) { |
|
36043 if (date === 'today') { |
|
36044 var today = new Date(); |
|
36045 if (angular.isDate(ngModel.$modelValue)) { |
|
36046 date = new Date(ngModel.$modelValue); |
|
36047 date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); |
|
36048 } else { |
|
36049 date = new Date(today.setHours(0, 0, 0, 0)); |
|
36050 } |
|
36051 } |
|
36052 scope.dateSelection( date ); |
|
36053 }; |
|
36054 |
|
36055 scope.close = function() { |
|
36056 scope.isOpen = false; |
|
36057 element[0].focus(); |
|
36058 }; |
|
36059 |
|
36060 var $popup = $compile(popupEl)(scope); |
|
36061 // Prevent jQuery cache memory leak (template is now redundant after linking) |
|
36062 popupEl.remove(); |
|
36063 |
|
36064 if ( appendToBody ) { |
|
36065 $document.find('body').append($popup); |
|
36066 } else { |
|
36067 element.after($popup); |
|
36068 } |
|
36069 |
|
36070 scope.$on('$destroy', function() { |
|
36071 $popup.remove(); |
|
36072 element.unbind('keydown', keydown); |
|
36073 $document.unbind('click', documentClickBind); |
|
36074 }); |
|
36075 } |
|
36076 }; |
|
36077 }]) |
|
36078 |
|
36079 .directive('datepickerPopupWrap', function() { |
|
36080 return { |
|
36081 restrict:'EA', |
|
36082 replace: true, |
|
36083 transclude: true, |
|
36084 templateUrl: 'template/datepicker/popup.html', |
|
36085 link:function (scope, element, attrs) { |
|
36086 element.bind('click', function(event) { |
|
36087 event.preventDefault(); |
|
36088 event.stopPropagation(); |
|
36089 }); |
|
36090 } |
|
36091 }; |
|
36092 }); |
|
36093 |
|
36094 angular.module('ui.bootstrap.dropdown', []) |
|
36095 |
|
36096 .constant('dropdownConfig', { |
|
36097 openClass: 'open' |
|
36098 }) |
|
36099 |
|
36100 .service('dropdownService', ['$document', function($document) { |
|
36101 var openScope = null; |
|
36102 |
|
36103 this.open = function( dropdownScope ) { |
|
36104 if ( !openScope ) { |
|
36105 $document.bind('click', closeDropdown); |
|
36106 $document.bind('keydown', escapeKeyBind); |
|
36107 } |
|
36108 |
|
36109 if ( openScope && openScope !== dropdownScope ) { |
|
36110 openScope.isOpen = false; |
|
36111 } |
|
36112 |
|
36113 openScope = dropdownScope; |
|
36114 }; |
|
36115 |
|
36116 this.close = function( dropdownScope ) { |
|
36117 if ( openScope === dropdownScope ) { |
|
36118 openScope = null; |
|
36119 $document.unbind('click', closeDropdown); |
|
36120 $document.unbind('keydown', escapeKeyBind); |
|
36121 } |
|
36122 }; |
|
36123 |
|
36124 var closeDropdown = function( evt ) { |
|
36125 var toggleElement = openScope.getToggleElement(); |
|
36126 if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) { |
|
36127 return; |
|
36128 } |
|
36129 |
|
36130 openScope.$apply(function() { |
|
36131 openScope.isOpen = false; |
|
36132 }); |
|
36133 }; |
|
36134 |
|
36135 var escapeKeyBind = function( evt ) { |
|
36136 if ( evt.which === 27 ) { |
|
36137 openScope.focusToggleElement(); |
|
36138 closeDropdown(); |
|
36139 } |
|
36140 }; |
|
36141 }]) |
|
36142 |
|
36143 .controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { |
|
36144 var self = this, |
|
36145 scope = $scope.$new(), // create a child scope so we are not polluting original one |
|
36146 openClass = dropdownConfig.openClass, |
|
36147 getIsOpen, |
|
36148 setIsOpen = angular.noop, |
|
36149 toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop; |
|
36150 |
|
36151 this.init = function( element ) { |
|
36152 self.$element = element; |
|
36153 |
|
36154 if ( $attrs.isOpen ) { |
|
36155 getIsOpen = $parse($attrs.isOpen); |
|
36156 setIsOpen = getIsOpen.assign; |
|
36157 |
|
36158 $scope.$watch(getIsOpen, function(value) { |
|
36159 scope.isOpen = !!value; |
|
36160 }); |
|
36161 } |
|
36162 }; |
|
36163 |
|
36164 this.toggle = function( open ) { |
|
36165 return scope.isOpen = arguments.length ? !!open : !scope.isOpen; |
|
36166 }; |
|
36167 |
|
36168 // Allow other directives to watch status |
|
36169 this.isOpen = function() { |
|
36170 return scope.isOpen; |
|
36171 }; |
|
36172 |
|
36173 scope.getToggleElement = function() { |
|
36174 return self.toggleElement; |
|
36175 }; |
|
36176 |
|
36177 scope.focusToggleElement = function() { |
|
36178 if ( self.toggleElement ) { |
|
36179 self.toggleElement[0].focus(); |
|
36180 } |
|
36181 }; |
|
36182 |
|
36183 scope.$watch('isOpen', function( isOpen, wasOpen ) { |
|
36184 $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); |
|
36185 |
|
36186 if ( isOpen ) { |
|
36187 scope.focusToggleElement(); |
|
36188 dropdownService.open( scope ); |
|
36189 } else { |
|
36190 dropdownService.close( scope ); |
|
36191 } |
|
36192 |
|
36193 setIsOpen($scope, isOpen); |
|
36194 if (angular.isDefined(isOpen) && isOpen !== wasOpen) { |
|
36195 toggleInvoker($scope, { open: !!isOpen }); |
|
36196 } |
|
36197 }); |
|
36198 |
|
36199 $scope.$on('$locationChangeSuccess', function() { |
|
36200 scope.isOpen = false; |
|
36201 }); |
|
36202 |
|
36203 $scope.$on('$destroy', function() { |
|
36204 scope.$destroy(); |
|
36205 }); |
|
36206 }]) |
|
36207 |
|
36208 .directive('dropdown', function() { |
|
36209 return { |
|
36210 restrict: 'CA', |
|
36211 controller: 'DropdownController', |
|
36212 link: function(scope, element, attrs, dropdownCtrl) { |
|
36213 dropdownCtrl.init( element ); |
|
36214 } |
|
36215 }; |
|
36216 }) |
|
36217 |
|
36218 .directive('dropdownToggle', function() { |
|
36219 return { |
|
36220 restrict: 'CA', |
|
36221 require: '?^dropdown', |
|
36222 link: function(scope, element, attrs, dropdownCtrl) { |
|
36223 if ( !dropdownCtrl ) { |
|
36224 return; |
|
36225 } |
|
36226 |
|
36227 dropdownCtrl.toggleElement = element; |
|
36228 |
|
36229 var toggleDropdown = function(event) { |
|
36230 event.preventDefault(); |
|
36231 |
|
36232 if ( !element.hasClass('disabled') && !attrs.disabled ) { |
|
36233 scope.$apply(function() { |
|
36234 dropdownCtrl.toggle(); |
|
36235 }); |
|
36236 } |
|
36237 }; |
|
36238 |
|
36239 element.bind('click', toggleDropdown); |
|
36240 |
|
36241 // WAI-ARIA |
|
36242 element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); |
|
36243 scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { |
|
36244 element.attr('aria-expanded', !!isOpen); |
|
36245 }); |
|
36246 |
|
36247 scope.$on('$destroy', function() { |
|
36248 element.unbind('click', toggleDropdown); |
|
36249 }); |
|
36250 } |
|
36251 }; |
|
36252 }); |
|
36253 |
|
36254 angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition']) |
|
36255 |
|
36256 /** |
|
36257 * A helper, internal data structure that acts as a map but also allows getting / removing |
|
36258 * elements in the LIFO order |
|
36259 */ |
|
36260 .factory('$$stackedMap', function () { |
|
36261 return { |
|
36262 createNew: function () { |
|
36263 var stack = []; |
|
36264 |
|
36265 return { |
|
36266 add: function (key, value) { |
|
36267 stack.push({ |
|
36268 key: key, |
|
36269 value: value |
|
36270 }); |
|
36271 }, |
|
36272 get: function (key) { |
|
36273 for (var i = 0; i < stack.length; i++) { |
|
36274 if (key == stack[i].key) { |
|
36275 return stack[i]; |
|
36276 } |
|
36277 } |
|
36278 }, |
|
36279 keys: function() { |
|
36280 var keys = []; |
|
36281 for (var i = 0; i < stack.length; i++) { |
|
36282 keys.push(stack[i].key); |
|
36283 } |
|
36284 return keys; |
|
36285 }, |
|
36286 top: function () { |
|
36287 return stack[stack.length - 1]; |
|
36288 }, |
|
36289 remove: function (key) { |
|
36290 var idx = -1; |
|
36291 for (var i = 0; i < stack.length; i++) { |
|
36292 if (key == stack[i].key) { |
|
36293 idx = i; |
|
36294 break; |
|
36295 } |
|
36296 } |
|
36297 return stack.splice(idx, 1)[0]; |
|
36298 }, |
|
36299 removeTop: function () { |
|
36300 return stack.splice(stack.length - 1, 1)[0]; |
|
36301 }, |
|
36302 length: function () { |
|
36303 return stack.length; |
|
36304 } |
|
36305 }; |
|
36306 } |
|
36307 }; |
|
36308 }) |
|
36309 |
|
36310 /** |
|
36311 * A helper directive for the $modal service. It creates a backdrop element. |
|
36312 */ |
|
36313 .directive('modalBackdrop', ['$timeout', function ($timeout) { |
|
36314 return { |
|
36315 restrict: 'EA', |
|
36316 replace: true, |
|
36317 templateUrl: 'template/modal/backdrop.html', |
|
36318 link: function (scope, element, attrs) { |
|
36319 scope.backdropClass = attrs.backdropClass || ''; |
|
36320 |
|
36321 scope.animate = false; |
|
36322 |
|
36323 //trigger CSS transitions |
|
36324 $timeout(function () { |
|
36325 scope.animate = true; |
|
36326 }); |
|
36327 } |
|
36328 }; |
|
36329 }]) |
|
36330 |
|
36331 .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) { |
|
36332 return { |
|
36333 restrict: 'EA', |
|
36334 scope: { |
|
36335 index: '@', |
|
36336 animate: '=' |
|
36337 }, |
|
36338 replace: true, |
|
36339 transclude: true, |
|
36340 templateUrl: function(tElement, tAttrs) { |
|
36341 return tAttrs.templateUrl || 'template/modal/window.html'; |
|
36342 }, |
|
36343 link: function (scope, element, attrs) { |
|
36344 element.addClass(attrs.windowClass || ''); |
|
36345 scope.size = attrs.size; |
|
36346 |
|
36347 $timeout(function () { |
|
36348 // trigger CSS transitions |
|
36349 scope.animate = true; |
|
36350 |
|
36351 /** |
|
36352 * Auto-focusing of a freshly-opened modal element causes any child elements |
|
36353 * with the autofocus attribute to loose focus. This is an issue on touch |
|
36354 * based devices which will show and then hide the onscreen keyboard. |
|
36355 * Attempts to refocus the autofocus element via JavaScript will not reopen |
|
36356 * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus |
|
36357 * the modal element if the modal does not contain an autofocus element. |
|
36358 */ |
|
36359 if (!element[0].querySelectorAll('[autofocus]').length) { |
|
36360 element[0].focus(); |
|
36361 } |
|
36362 }); |
|
36363 |
|
36364 scope.close = function (evt) { |
|
36365 var modal = $modalStack.getTop(); |
|
36366 if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) { |
|
36367 evt.preventDefault(); |
|
36368 evt.stopPropagation(); |
|
36369 $modalStack.dismiss(modal.key, 'backdrop click'); |
|
36370 } |
|
36371 }; |
|
36372 } |
|
36373 }; |
|
36374 }]) |
|
36375 |
|
36376 .directive('modalTransclude', function () { |
|
36377 return { |
|
36378 link: function($scope, $element, $attrs, controller, $transclude) { |
|
36379 $transclude($scope.$parent, function(clone) { |
|
36380 $element.empty(); |
|
36381 $element.append(clone); |
|
36382 }); |
|
36383 } |
|
36384 }; |
|
36385 }) |
|
36386 |
|
36387 .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap', |
|
36388 function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) { |
|
36389 |
|
36390 var OPENED_MODAL_CLASS = 'modal-open'; |
|
36391 |
|
36392 var backdropDomEl, backdropScope; |
|
36393 var openedWindows = $$stackedMap.createNew(); |
|
36394 var $modalStack = {}; |
|
36395 |
|
36396 function backdropIndex() { |
|
36397 var topBackdropIndex = -1; |
|
36398 var opened = openedWindows.keys(); |
|
36399 for (var i = 0; i < opened.length; i++) { |
|
36400 if (openedWindows.get(opened[i]).value.backdrop) { |
|
36401 topBackdropIndex = i; |
|
36402 } |
|
36403 } |
|
36404 return topBackdropIndex; |
|
36405 } |
|
36406 |
|
36407 $rootScope.$watch(backdropIndex, function(newBackdropIndex){ |
|
36408 if (backdropScope) { |
|
36409 backdropScope.index = newBackdropIndex; |
|
36410 } |
|
36411 }); |
|
36412 |
|
36413 function removeModalWindow(modalInstance) { |
|
36414 |
|
36415 var body = $document.find('body').eq(0); |
|
36416 var modalWindow = openedWindows.get(modalInstance).value; |
|
36417 |
|
36418 //clean up the stack |
|
36419 openedWindows.remove(modalInstance); |
|
36420 |
|
36421 //remove window DOM element |
|
36422 removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() { |
|
36423 modalWindow.modalScope.$destroy(); |
|
36424 body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0); |
|
36425 checkRemoveBackdrop(); |
|
36426 }); |
|
36427 } |
|
36428 |
|
36429 function checkRemoveBackdrop() { |
|
36430 //remove backdrop if no longer needed |
|
36431 if (backdropDomEl && backdropIndex() == -1) { |
|
36432 var backdropScopeRef = backdropScope; |
|
36433 removeAfterAnimate(backdropDomEl, backdropScope, 150, function () { |
|
36434 backdropScopeRef.$destroy(); |
|
36435 backdropScopeRef = null; |
|
36436 }); |
|
36437 backdropDomEl = undefined; |
|
36438 backdropScope = undefined; |
|
36439 } |
|
36440 } |
|
36441 |
|
36442 function removeAfterAnimate(domEl, scope, emulateTime, done) { |
|
36443 // Closing animation |
|
36444 scope.animate = false; |
|
36445 |
|
36446 var transitionEndEventName = $transition.transitionEndEventName; |
|
36447 if (transitionEndEventName) { |
|
36448 // transition out |
|
36449 var timeout = $timeout(afterAnimating, emulateTime); |
|
36450 |
|
36451 domEl.bind(transitionEndEventName, function () { |
|
36452 $timeout.cancel(timeout); |
|
36453 afterAnimating(); |
|
36454 scope.$apply(); |
|
36455 }); |
|
36456 } else { |
|
36457 // Ensure this call is async |
|
36458 $timeout(afterAnimating); |
|
36459 } |
|
36460 |
|
36461 function afterAnimating() { |
|
36462 if (afterAnimating.done) { |
|
36463 return; |
|
36464 } |
|
36465 afterAnimating.done = true; |
|
36466 |
|
36467 domEl.remove(); |
|
36468 if (done) { |
|
36469 done(); |
|
36470 } |
|
36471 } |
|
36472 } |
|
36473 |
|
36474 $document.bind('keydown', function (evt) { |
|
36475 var modal; |
|
36476 |
|
36477 if (evt.which === 27) { |
|
36478 modal = openedWindows.top(); |
|
36479 if (modal && modal.value.keyboard) { |
|
36480 evt.preventDefault(); |
|
36481 $rootScope.$apply(function () { |
|
36482 $modalStack.dismiss(modal.key, 'escape key press'); |
|
36483 }); |
|
36484 } |
|
36485 } |
|
36486 }); |
|
36487 |
|
36488 $modalStack.open = function (modalInstance, modal) { |
|
36489 |
|
36490 openedWindows.add(modalInstance, { |
|
36491 deferred: modal.deferred, |
|
36492 modalScope: modal.scope, |
|
36493 backdrop: modal.backdrop, |
|
36494 keyboard: modal.keyboard |
|
36495 }); |
|
36496 |
|
36497 var body = $document.find('body').eq(0), |
|
36498 currBackdropIndex = backdropIndex(); |
|
36499 |
|
36500 if (currBackdropIndex >= 0 && !backdropDomEl) { |
|
36501 backdropScope = $rootScope.$new(true); |
|
36502 backdropScope.index = currBackdropIndex; |
|
36503 var angularBackgroundDomEl = angular.element('<div modal-backdrop></div>'); |
|
36504 angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); |
|
36505 backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); |
|
36506 body.append(backdropDomEl); |
|
36507 } |
|
36508 |
|
36509 var angularDomEl = angular.element('<div modal-window></div>'); |
|
36510 angularDomEl.attr({ |
|
36511 'template-url': modal.windowTemplateUrl, |
|
36512 'window-class': modal.windowClass, |
|
36513 'size': modal.size, |
|
36514 'index': openedWindows.length() - 1, |
|
36515 'animate': 'animate' |
|
36516 }).html(modal.content); |
|
36517 |
|
36518 var modalDomEl = $compile(angularDomEl)(modal.scope); |
|
36519 openedWindows.top().value.modalDomEl = modalDomEl; |
|
36520 body.append(modalDomEl); |
|
36521 body.addClass(OPENED_MODAL_CLASS); |
|
36522 }; |
|
36523 |
|
36524 $modalStack.close = function (modalInstance, result) { |
|
36525 var modalWindow = openedWindows.get(modalInstance); |
|
36526 if (modalWindow) { |
|
36527 modalWindow.value.deferred.resolve(result); |
|
36528 removeModalWindow(modalInstance); |
|
36529 } |
|
36530 }; |
|
36531 |
|
36532 $modalStack.dismiss = function (modalInstance, reason) { |
|
36533 var modalWindow = openedWindows.get(modalInstance); |
|
36534 if (modalWindow) { |
|
36535 modalWindow.value.deferred.reject(reason); |
|
36536 removeModalWindow(modalInstance); |
|
36537 } |
|
36538 }; |
|
36539 |
|
36540 $modalStack.dismissAll = function (reason) { |
|
36541 var topModal = this.getTop(); |
|
36542 while (topModal) { |
|
36543 this.dismiss(topModal.key, reason); |
|
36544 topModal = this.getTop(); |
|
36545 } |
|
36546 }; |
|
36547 |
|
36548 $modalStack.getTop = function () { |
|
36549 return openedWindows.top(); |
|
36550 }; |
|
36551 |
|
36552 return $modalStack; |
|
36553 }]) |
|
36554 |
|
36555 .provider('$modal', function () { |
|
36556 |
|
36557 var $modalProvider = { |
|
36558 options: { |
|
36559 backdrop: true, //can be also false or 'static' |
|
36560 keyboard: true |
|
36561 }, |
|
36562 $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', |
|
36563 function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { |
|
36564 |
|
36565 var $modal = {}; |
|
36566 |
|
36567 function getTemplatePromise(options) { |
|
36568 return options.template ? $q.when(options.template) : |
|
36569 $http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl, |
|
36570 {cache: $templateCache}).then(function (result) { |
|
36571 return result.data; |
|
36572 }); |
|
36573 } |
|
36574 |
|
36575 function getResolvePromises(resolves) { |
|
36576 var promisesArr = []; |
|
36577 angular.forEach(resolves, function (value) { |
|
36578 if (angular.isFunction(value) || angular.isArray(value)) { |
|
36579 promisesArr.push($q.when($injector.invoke(value))); |
|
36580 } |
|
36581 }); |
|
36582 return promisesArr; |
|
36583 } |
|
36584 |
|
36585 $modal.open = function (modalOptions) { |
|
36586 |
|
36587 var modalResultDeferred = $q.defer(); |
|
36588 var modalOpenedDeferred = $q.defer(); |
|
36589 |
|
36590 //prepare an instance of a modal to be injected into controllers and returned to a caller |
|
36591 var modalInstance = { |
|
36592 result: modalResultDeferred.promise, |
|
36593 opened: modalOpenedDeferred.promise, |
|
36594 close: function (result) { |
|
36595 $modalStack.close(modalInstance, result); |
|
36596 }, |
|
36597 dismiss: function (reason) { |
|
36598 $modalStack.dismiss(modalInstance, reason); |
|
36599 } |
|
36600 }; |
|
36601 |
|
36602 //merge and clean up options |
|
36603 modalOptions = angular.extend({}, $modalProvider.options, modalOptions); |
|
36604 modalOptions.resolve = modalOptions.resolve || {}; |
|
36605 |
|
36606 //verify options |
|
36607 if (!modalOptions.template && !modalOptions.templateUrl) { |
|
36608 throw new Error('One of template or templateUrl options is required.'); |
|
36609 } |
|
36610 |
|
36611 var templateAndResolvePromise = |
|
36612 $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); |
|
36613 |
|
36614 |
|
36615 templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { |
|
36616 |
|
36617 var modalScope = (modalOptions.scope || $rootScope).$new(); |
|
36618 modalScope.$close = modalInstance.close; |
|
36619 modalScope.$dismiss = modalInstance.dismiss; |
|
36620 |
|
36621 var ctrlInstance, ctrlLocals = {}; |
|
36622 var resolveIter = 1; |
|
36623 |
|
36624 //controllers |
|
36625 if (modalOptions.controller) { |
|
36626 ctrlLocals.$scope = modalScope; |
|
36627 ctrlLocals.$modalInstance = modalInstance; |
|
36628 angular.forEach(modalOptions.resolve, function (value, key) { |
|
36629 ctrlLocals[key] = tplAndVars[resolveIter++]; |
|
36630 }); |
|
36631 |
|
36632 ctrlInstance = $controller(modalOptions.controller, ctrlLocals); |
|
36633 if (modalOptions.controllerAs) { |
|
36634 modalScope[modalOptions.controllerAs] = ctrlInstance; |
|
36635 } |
|
36636 } |
|
36637 |
|
36638 $modalStack.open(modalInstance, { |
|
36639 scope: modalScope, |
|
36640 deferred: modalResultDeferred, |
|
36641 content: tplAndVars[0], |
|
36642 backdrop: modalOptions.backdrop, |
|
36643 keyboard: modalOptions.keyboard, |
|
36644 backdropClass: modalOptions.backdropClass, |
|
36645 windowClass: modalOptions.windowClass, |
|
36646 windowTemplateUrl: modalOptions.windowTemplateUrl, |
|
36647 size: modalOptions.size |
|
36648 }); |
|
36649 |
|
36650 }, function resolveError(reason) { |
|
36651 modalResultDeferred.reject(reason); |
|
36652 }); |
|
36653 |
|
36654 templateAndResolvePromise.then(function () { |
|
36655 modalOpenedDeferred.resolve(true); |
|
36656 }, function () { |
|
36657 modalOpenedDeferred.reject(false); |
|
36658 }); |
|
36659 |
|
36660 return modalInstance; |
|
36661 }; |
|
36662 |
|
36663 return $modal; |
|
36664 }] |
|
36665 }; |
|
36666 |
|
36667 return $modalProvider; |
|
36668 }); |
|
36669 |
|
36670 angular.module('ui.bootstrap.pagination', []) |
|
36671 |
|
36672 .controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) { |
|
36673 var self = this, |
|
36674 ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl |
|
36675 setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; |
|
36676 |
|
36677 this.init = function(ngModelCtrl_, config) { |
|
36678 ngModelCtrl = ngModelCtrl_; |
|
36679 this.config = config; |
|
36680 |
|
36681 ngModelCtrl.$render = function() { |
|
36682 self.render(); |
|
36683 }; |
|
36684 |
|
36685 if ($attrs.itemsPerPage) { |
|
36686 $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { |
|
36687 self.itemsPerPage = parseInt(value, 10); |
|
36688 $scope.totalPages = self.calculateTotalPages(); |
|
36689 }); |
|
36690 } else { |
|
36691 this.itemsPerPage = config.itemsPerPage; |
|
36692 } |
|
36693 }; |
|
36694 |
|
36695 this.calculateTotalPages = function() { |
|
36696 var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); |
|
36697 return Math.max(totalPages || 0, 1); |
|
36698 }; |
|
36699 |
|
36700 this.render = function() { |
|
36701 $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; |
|
36702 }; |
|
36703 |
|
36704 $scope.selectPage = function(page) { |
|
36705 if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) { |
|
36706 ngModelCtrl.$setViewValue(page); |
|
36707 ngModelCtrl.$render(); |
|
36708 } |
|
36709 }; |
|
36710 |
|
36711 $scope.getText = function( key ) { |
|
36712 return $scope[key + 'Text'] || self.config[key + 'Text']; |
|
36713 }; |
|
36714 $scope.noPrevious = function() { |
|
36715 return $scope.page === 1; |
|
36716 }; |
|
36717 $scope.noNext = function() { |
|
36718 return $scope.page === $scope.totalPages; |
|
36719 }; |
|
36720 |
|
36721 $scope.$watch('totalItems', function() { |
|
36722 $scope.totalPages = self.calculateTotalPages(); |
|
36723 }); |
|
36724 |
|
36725 $scope.$watch('totalPages', function(value) { |
|
36726 setNumPages($scope.$parent, value); // Readonly variable |
|
36727 |
|
36728 if ( $scope.page > value ) { |
|
36729 $scope.selectPage(value); |
|
36730 } else { |
|
36731 ngModelCtrl.$render(); |
|
36732 } |
|
36733 }); |
|
36734 }]) |
|
36735 |
|
36736 .constant('paginationConfig', { |
|
36737 itemsPerPage: 10, |
|
36738 boundaryLinks: false, |
|
36739 directionLinks: true, |
|
36740 firstText: 'First', |
|
36741 previousText: 'Previous', |
|
36742 nextText: 'Next', |
|
36743 lastText: 'Last', |
|
36744 rotate: true |
|
36745 }) |
|
36746 |
|
36747 .directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { |
|
36748 return { |
|
36749 restrict: 'EA', |
|
36750 scope: { |
|
36751 totalItems: '=', |
|
36752 firstText: '@', |
|
36753 previousText: '@', |
|
36754 nextText: '@', |
|
36755 lastText: '@' |
|
36756 }, |
|
36757 require: ['pagination', '?ngModel'], |
|
36758 controller: 'PaginationController', |
|
36759 templateUrl: 'template/pagination/pagination.html', |
|
36760 replace: true, |
|
36761 link: function(scope, element, attrs, ctrls) { |
|
36762 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; |
|
36763 |
|
36764 if (!ngModelCtrl) { |
|
36765 return; // do nothing if no ng-model |
|
36766 } |
|
36767 |
|
36768 // Setup configuration parameters |
|
36769 var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, |
|
36770 rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; |
|
36771 scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; |
|
36772 scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; |
|
36773 |
|
36774 paginationCtrl.init(ngModelCtrl, paginationConfig); |
|
36775 |
|
36776 if (attrs.maxSize) { |
|
36777 scope.$parent.$watch($parse(attrs.maxSize), function(value) { |
|
36778 maxSize = parseInt(value, 10); |
|
36779 paginationCtrl.render(); |
|
36780 }); |
|
36781 } |
|
36782 |
|
36783 // Create page object used in template |
|
36784 function makePage(number, text, isActive) { |
|
36785 return { |
|
36786 number: number, |
|
36787 text: text, |
|
36788 active: isActive |
|
36789 }; |
|
36790 } |
|
36791 |
|
36792 function getPages(currentPage, totalPages) { |
|
36793 var pages = []; |
|
36794 |
|
36795 // Default page limits |
|
36796 var startPage = 1, endPage = totalPages; |
|
36797 var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages ); |
|
36798 |
|
36799 // recompute if maxSize |
|
36800 if ( isMaxSized ) { |
|
36801 if ( rotate ) { |
|
36802 // Current page is displayed in the middle of the visible ones |
|
36803 startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); |
|
36804 endPage = startPage + maxSize - 1; |
|
36805 |
|
36806 // Adjust if limit is exceeded |
|
36807 if (endPage > totalPages) { |
|
36808 endPage = totalPages; |
|
36809 startPage = endPage - maxSize + 1; |
|
36810 } |
|
36811 } else { |
|
36812 // Visible pages are paginated with maxSize |
|
36813 startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; |
|
36814 |
|
36815 // Adjust last page if limit is exceeded |
|
36816 endPage = Math.min(startPage + maxSize - 1, totalPages); |
|
36817 } |
|
36818 } |
|
36819 |
|
36820 // Add page number links |
|
36821 for (var number = startPage; number <= endPage; number++) { |
|
36822 var page = makePage(number, number, number === currentPage); |
|
36823 pages.push(page); |
|
36824 } |
|
36825 |
|
36826 // Add links to move between page sets |
|
36827 if ( isMaxSized && ! rotate ) { |
|
36828 if ( startPage > 1 ) { |
|
36829 var previousPageSet = makePage(startPage - 1, '...', false); |
|
36830 pages.unshift(previousPageSet); |
|
36831 } |
|
36832 |
|
36833 if ( endPage < totalPages ) { |
|
36834 var nextPageSet = makePage(endPage + 1, '...', false); |
|
36835 pages.push(nextPageSet); |
|
36836 } |
|
36837 } |
|
36838 |
|
36839 return pages; |
|
36840 } |
|
36841 |
|
36842 var originalRender = paginationCtrl.render; |
|
36843 paginationCtrl.render = function() { |
|
36844 originalRender(); |
|
36845 if (scope.page > 0 && scope.page <= scope.totalPages) { |
|
36846 scope.pages = getPages(scope.page, scope.totalPages); |
|
36847 } |
|
36848 }; |
|
36849 } |
|
36850 }; |
|
36851 }]) |
|
36852 |
|
36853 .constant('pagerConfig', { |
|
36854 itemsPerPage: 10, |
|
36855 previousText: '« Previous', |
|
36856 nextText: 'Next »', |
|
36857 align: true |
|
36858 }) |
|
36859 |
|
36860 .directive('pager', ['pagerConfig', function(pagerConfig) { |
|
36861 return { |
|
36862 restrict: 'EA', |
|
36863 scope: { |
|
36864 totalItems: '=', |
|
36865 previousText: '@', |
|
36866 nextText: '@' |
|
36867 }, |
|
36868 require: ['pager', '?ngModel'], |
|
36869 controller: 'PaginationController', |
|
36870 templateUrl: 'template/pagination/pager.html', |
|
36871 replace: true, |
|
36872 link: function(scope, element, attrs, ctrls) { |
|
36873 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; |
|
36874 |
|
36875 if (!ngModelCtrl) { |
|
36876 return; // do nothing if no ng-model |
|
36877 } |
|
36878 |
|
36879 scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; |
|
36880 paginationCtrl.init(ngModelCtrl, pagerConfig); |
|
36881 } |
|
36882 }; |
|
36883 }]); |
|
36884 |
|
36885 /** |
|
36886 * The following features are still outstanding: animation as a |
|
36887 * function, placement as a function, inside, support for more triggers than |
|
36888 * just mouse enter/leave, html tooltips, and selector delegation. |
|
36889 */ |
|
36890 angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) |
|
36891 |
|
36892 /** |
|
36893 * The $tooltip service creates tooltip- and popover-like directives as well as |
|
36894 * houses global options for them. |
|
36895 */ |
|
36896 .provider( '$tooltip', function () { |
|
36897 // The default options tooltip and popover. |
|
36898 var defaultOptions = { |
|
36899 placement: 'top', |
|
36900 animation: true, |
|
36901 popupDelay: 0 |
|
36902 }; |
|
36903 |
|
36904 // Default hide triggers for each show trigger |
|
36905 var triggerMap = { |
|
36906 'mouseenter': 'mouseleave', |
|
36907 'click': 'click', |
|
36908 'focus': 'blur' |
|
36909 }; |
|
36910 |
|
36911 // The options specified to the provider globally. |
|
36912 var globalOptions = {}; |
|
36913 |
|
36914 /** |
|
36915 * `options({})` allows global configuration of all tooltips in the |
|
36916 * application. |
|
36917 * |
|
36918 * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { |
|
36919 * // place tooltips left instead of top by default |
|
36920 * $tooltipProvider.options( { placement: 'left' } ); |
|
36921 * }); |
|
36922 */ |
|
36923 this.options = function( value ) { |
|
36924 angular.extend( globalOptions, value ); |
|
36925 }; |
|
36926 |
|
36927 /** |
|
36928 * This allows you to extend the set of trigger mappings available. E.g.: |
|
36929 * |
|
36930 * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); |
|
36931 */ |
|
36932 this.setTriggers = function setTriggers ( triggers ) { |
|
36933 angular.extend( triggerMap, triggers ); |
|
36934 }; |
|
36935 |
|
36936 /** |
|
36937 * This is a helper function for translating camel-case to snake-case. |
|
36938 */ |
|
36939 function snake_case(name){ |
|
36940 var regexp = /[A-Z]/g; |
|
36941 var separator = '-'; |
|
36942 return name.replace(regexp, function(letter, pos) { |
|
36943 return (pos ? separator : '') + letter.toLowerCase(); |
|
36944 }); |
|
36945 } |
|
36946 |
|
36947 /** |
|
36948 * Returns the actual instance of the $tooltip service. |
|
36949 * TODO support multiple triggers |
|
36950 */ |
|
36951 this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) { |
|
36952 return function $tooltip ( type, prefix, defaultTriggerShow ) { |
|
36953 var options = angular.extend( {}, defaultOptions, globalOptions ); |
|
36954 |
|
36955 /** |
|
36956 * Returns an object of show and hide triggers. |
|
36957 * |
|
36958 * If a trigger is supplied, |
|
36959 * it is used to show the tooltip; otherwise, it will use the `trigger` |
|
36960 * option passed to the `$tooltipProvider.options` method; else it will |
|
36961 * default to the trigger supplied to this directive factory. |
|
36962 * |
|
36963 * The hide trigger is based on the show trigger. If the `trigger` option |
|
36964 * was passed to the `$tooltipProvider.options` method, it will use the |
|
36965 * mapped trigger from `triggerMap` or the passed trigger if the map is |
|
36966 * undefined; otherwise, it uses the `triggerMap` value of the show |
|
36967 * trigger; else it will just use the show trigger. |
|
36968 */ |
|
36969 function getTriggers ( trigger ) { |
|
36970 var show = trigger || options.trigger || defaultTriggerShow; |
|
36971 var hide = triggerMap[show] || show; |
|
36972 return { |
|
36973 show: show, |
|
36974 hide: hide |
|
36975 }; |
|
36976 } |
|
36977 |
|
36978 var directiveName = snake_case( type ); |
|
36979 |
|
36980 var startSym = $interpolate.startSymbol(); |
|
36981 var endSym = $interpolate.endSymbol(); |
|
36982 var template = |
|
36983 '<div '+ directiveName +'-popup '+ |
|
36984 'title="'+startSym+'tt_title'+endSym+'" '+ |
|
36985 'content="'+startSym+'tt_content'+endSym+'" '+ |
|
36986 'placement="'+startSym+'tt_placement'+endSym+'" '+ |
|
36987 'animation="tt_animation" '+ |
|
36988 'is-open="tt_isOpen"'+ |
|
36989 '>'+ |
|
36990 '</div>'; |
|
36991 |
|
36992 return { |
|
36993 restrict: 'EA', |
|
36994 scope: true, |
|
36995 compile: function (tElem, tAttrs) { |
|
36996 var tooltipLinker = $compile( template ); |
|
36997 |
|
36998 return function link ( scope, element, attrs ) { |
|
36999 var tooltip; |
|
37000 var transitionTimeout; |
|
37001 var popupTimeout; |
|
37002 var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; |
|
37003 var triggers = getTriggers( undefined ); |
|
37004 var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); |
|
37005 |
|
37006 var positionTooltip = function () { |
|
37007 |
|
37008 var ttPosition = $position.positionElements(element, tooltip, scope.tt_placement, appendToBody); |
|
37009 ttPosition.top += 'px'; |
|
37010 ttPosition.left += 'px'; |
|
37011 |
|
37012 // Now set the calculated positioning. |
|
37013 tooltip.css( ttPosition ); |
|
37014 }; |
|
37015 |
|
37016 // By default, the tooltip is not open. |
|
37017 // TODO add ability to start tooltip opened |
|
37018 scope.tt_isOpen = false; |
|
37019 |
|
37020 function toggleTooltipBind () { |
|
37021 if ( ! scope.tt_isOpen ) { |
|
37022 showTooltipBind(); |
|
37023 } else { |
|
37024 hideTooltipBind(); |
|
37025 } |
|
37026 } |
|
37027 |
|
37028 // Show the tooltip with delay if specified, otherwise show it immediately |
|
37029 function showTooltipBind() { |
|
37030 if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { |
|
37031 return; |
|
37032 } |
|
37033 if ( scope.tt_popupDelay ) { |
|
37034 // Do nothing if the tooltip was already scheduled to pop-up. |
|
37035 // This happens if show is triggered multiple times before any hide is triggered. |
|
37036 if (!popupTimeout) { |
|
37037 popupTimeout = $timeout( show, scope.tt_popupDelay, false ); |
|
37038 popupTimeout.then(function(reposition){reposition();}); |
|
37039 } |
|
37040 } else { |
|
37041 show()(); |
|
37042 } |
|
37043 } |
|
37044 |
|
37045 function hideTooltipBind () { |
|
37046 scope.$apply(function () { |
|
37047 hide(); |
|
37048 }); |
|
37049 } |
|
37050 |
|
37051 // Show the tooltip popup element. |
|
37052 function show() { |
|
37053 |
|
37054 popupTimeout = null; |
|
37055 |
|
37056 // If there is a pending remove transition, we must cancel it, lest the |
|
37057 // tooltip be mysteriously removed. |
|
37058 if ( transitionTimeout ) { |
|
37059 $timeout.cancel( transitionTimeout ); |
|
37060 transitionTimeout = null; |
|
37061 } |
|
37062 |
|
37063 // Don't show empty tooltips. |
|
37064 if ( ! scope.tt_content ) { |
|
37065 return angular.noop; |
|
37066 } |
|
37067 |
|
37068 createTooltip(); |
|
37069 |
|
37070 // Set the initial positioning. |
|
37071 tooltip.css({ top: 0, left: 0, display: 'block' }); |
|
37072 |
|
37073 // Now we add it to the DOM because need some info about it. But it's not |
|
37074 // visible yet anyway. |
|
37075 if ( appendToBody ) { |
|
37076 $document.find( 'body' ).append( tooltip ); |
|
37077 } else { |
|
37078 element.after( tooltip ); |
|
37079 } |
|
37080 |
|
37081 positionTooltip(); |
|
37082 |
|
37083 // And show the tooltip. |
|
37084 scope.tt_isOpen = true; |
|
37085 scope.$digest(); // digest required as $apply is not called |
|
37086 |
|
37087 // Return positioning function as promise callback for correct |
|
37088 // positioning after draw. |
|
37089 return positionTooltip; |
|
37090 } |
|
37091 |
|
37092 // Hide the tooltip popup element. |
|
37093 function hide() { |
|
37094 // First things first: we don't show it anymore. |
|
37095 scope.tt_isOpen = false; |
|
37096 |
|
37097 //if tooltip is going to be shown after delay, we must cancel this |
|
37098 $timeout.cancel( popupTimeout ); |
|
37099 popupTimeout = null; |
|
37100 |
|
37101 // And now we remove it from the DOM. However, if we have animation, we |
|
37102 // need to wait for it to expire beforehand. |
|
37103 // FIXME: this is a placeholder for a port of the transitions library. |
|
37104 if ( scope.tt_animation ) { |
|
37105 if (!transitionTimeout) { |
|
37106 transitionTimeout = $timeout(removeTooltip, 500); |
|
37107 } |
|
37108 } else { |
|
37109 removeTooltip(); |
|
37110 } |
|
37111 } |
|
37112 |
|
37113 function createTooltip() { |
|
37114 // There can only be one tooltip element per directive shown at once. |
|
37115 if (tooltip) { |
|
37116 removeTooltip(); |
|
37117 } |
|
37118 tooltip = tooltipLinker(scope, function () {}); |
|
37119 |
|
37120 // Get contents rendered into the tooltip |
|
37121 scope.$digest(); |
|
37122 } |
|
37123 |
|
37124 function removeTooltip() { |
|
37125 transitionTimeout = null; |
|
37126 if (tooltip) { |
|
37127 tooltip.remove(); |
|
37128 tooltip = null; |
|
37129 } |
|
37130 } |
|
37131 |
|
37132 /** |
|
37133 * Observe the relevant attributes. |
|
37134 */ |
|
37135 attrs.$observe( type, function ( val ) { |
|
37136 scope.tt_content = val; |
|
37137 |
|
37138 if (!val && scope.tt_isOpen ) { |
|
37139 hide(); |
|
37140 } |
|
37141 }); |
|
37142 |
|
37143 attrs.$observe( prefix+'Title', function ( val ) { |
|
37144 scope.tt_title = val; |
|
37145 }); |
|
37146 |
|
37147 attrs.$observe( prefix+'Placement', function ( val ) { |
|
37148 scope.tt_placement = angular.isDefined( val ) ? val : options.placement; |
|
37149 }); |
|
37150 |
|
37151 attrs.$observe( prefix+'PopupDelay', function ( val ) { |
|
37152 var delay = parseInt( val, 10 ); |
|
37153 scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; |
|
37154 }); |
|
37155 |
|
37156 var unregisterTriggers = function () { |
|
37157 element.unbind(triggers.show, showTooltipBind); |
|
37158 element.unbind(triggers.hide, hideTooltipBind); |
|
37159 }; |
|
37160 |
|
37161 attrs.$observe( prefix+'Trigger', function ( val ) { |
|
37162 unregisterTriggers(); |
|
37163 |
|
37164 triggers = getTriggers( val ); |
|
37165 |
|
37166 if ( triggers.show === triggers.hide ) { |
|
37167 element.bind( triggers.show, toggleTooltipBind ); |
|
37168 } else { |
|
37169 element.bind( triggers.show, showTooltipBind ); |
|
37170 element.bind( triggers.hide, hideTooltipBind ); |
|
37171 } |
|
37172 }); |
|
37173 |
|
37174 var animation = scope.$eval(attrs[prefix + 'Animation']); |
|
37175 scope.tt_animation = angular.isDefined(animation) ? !!animation : options.animation; |
|
37176 |
|
37177 attrs.$observe( prefix+'AppendToBody', function ( val ) { |
|
37178 appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; |
|
37179 }); |
|
37180 |
|
37181 // if a tooltip is attached to <body> we need to remove it on |
|
37182 // location change as its parent scope will probably not be destroyed |
|
37183 // by the change. |
|
37184 if ( appendToBody ) { |
|
37185 scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { |
|
37186 if ( scope.tt_isOpen ) { |
|
37187 hide(); |
|
37188 } |
|
37189 }); |
|
37190 } |
|
37191 |
|
37192 // Make sure tooltip is destroyed and removed. |
|
37193 scope.$on('$destroy', function onDestroyTooltip() { |
|
37194 $timeout.cancel( transitionTimeout ); |
|
37195 $timeout.cancel( popupTimeout ); |
|
37196 unregisterTriggers(); |
|
37197 removeTooltip(); |
|
37198 }); |
|
37199 }; |
|
37200 } |
|
37201 }; |
|
37202 }; |
|
37203 }]; |
|
37204 }) |
|
37205 |
|
37206 .directive( 'tooltipPopup', function () { |
|
37207 return { |
|
37208 restrict: 'EA', |
|
37209 replace: true, |
|
37210 scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, |
|
37211 templateUrl: 'template/tooltip/tooltip-popup.html' |
|
37212 }; |
|
37213 }) |
|
37214 |
|
37215 .directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { |
|
37216 return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); |
|
37217 }]) |
|
37218 |
|
37219 .directive( 'tooltipHtmlUnsafePopup', function () { |
|
37220 return { |
|
37221 restrict: 'EA', |
|
37222 replace: true, |
|
37223 scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, |
|
37224 templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' |
|
37225 }; |
|
37226 }) |
|
37227 |
|
37228 .directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { |
|
37229 return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); |
|
37230 }]); |
|
37231 |
|
37232 /** |
|
37233 * The following features are still outstanding: popup delay, animation as a |
|
37234 * function, placement as a function, inside, support for more triggers than |
|
37235 * just mouse enter/leave, html popovers, and selector delegatation. |
|
37236 */ |
|
37237 angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) |
|
37238 |
|
37239 .directive( 'popoverPopup', function () { |
|
37240 return { |
|
37241 restrict: 'EA', |
|
37242 replace: true, |
|
37243 scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' }, |
|
37244 templateUrl: 'template/popover/popover.html' |
|
37245 }; |
|
37246 }) |
|
37247 |
|
37248 .directive( 'popover', [ '$tooltip', function ( $tooltip ) { |
|
37249 return $tooltip( 'popover', 'popover', 'click' ); |
|
37250 }]); |
|
37251 |
|
37252 angular.module('ui.bootstrap.progressbar', []) |
|
37253 |
|
37254 .constant('progressConfig', { |
|
37255 animate: true, |
|
37256 max: 100 |
|
37257 }) |
|
37258 |
|
37259 .controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { |
|
37260 var self = this, |
|
37261 animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; |
|
37262 |
|
37263 this.bars = []; |
|
37264 $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max; |
|
37265 |
|
37266 this.addBar = function(bar, element) { |
|
37267 if ( !animate ) { |
|
37268 element.css({'transition': 'none'}); |
|
37269 } |
|
37270 |
|
37271 this.bars.push(bar); |
|
37272 |
|
37273 bar.$watch('value', function( value ) { |
|
37274 bar.percent = +(100 * value / $scope.max).toFixed(2); |
|
37275 }); |
|
37276 |
|
37277 bar.$on('$destroy', function() { |
|
37278 element = null; |
|
37279 self.removeBar(bar); |
|
37280 }); |
|
37281 }; |
|
37282 |
|
37283 this.removeBar = function(bar) { |
|
37284 this.bars.splice(this.bars.indexOf(bar), 1); |
|
37285 }; |
|
37286 }]) |
|
37287 |
|
37288 .directive('progress', function() { |
|
37289 return { |
|
37290 restrict: 'EA', |
|
37291 replace: true, |
|
37292 transclude: true, |
|
37293 controller: 'ProgressController', |
|
37294 require: 'progress', |
|
37295 scope: {}, |
|
37296 templateUrl: 'template/progressbar/progress.html' |
|
37297 }; |
|
37298 }) |
|
37299 |
|
37300 .directive('bar', function() { |
|
37301 return { |
|
37302 restrict: 'EA', |
|
37303 replace: true, |
|
37304 transclude: true, |
|
37305 require: '^progress', |
|
37306 scope: { |
|
37307 value: '=', |
|
37308 type: '@' |
|
37309 }, |
|
37310 templateUrl: 'template/progressbar/bar.html', |
|
37311 link: function(scope, element, attrs, progressCtrl) { |
|
37312 progressCtrl.addBar(scope, element); |
|
37313 } |
|
37314 }; |
|
37315 }) |
|
37316 |
|
37317 .directive('progressbar', function() { |
|
37318 return { |
|
37319 restrict: 'EA', |
|
37320 replace: true, |
|
37321 transclude: true, |
|
37322 controller: 'ProgressController', |
|
37323 scope: { |
|
37324 value: '=', |
|
37325 type: '@' |
|
37326 }, |
|
37327 templateUrl: 'template/progressbar/progressbar.html', |
|
37328 link: function(scope, element, attrs, progressCtrl) { |
|
37329 progressCtrl.addBar(scope, angular.element(element.children()[0])); |
|
37330 } |
|
37331 }; |
|
37332 }); |
|
37333 angular.module('ui.bootstrap.rating', []) |
|
37334 |
|
37335 .constant('ratingConfig', { |
|
37336 max: 5, |
|
37337 stateOn: null, |
|
37338 stateOff: null |
|
37339 }) |
|
37340 |
|
37341 .controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { |
|
37342 var ngModelCtrl = { $setViewValue: angular.noop }; |
|
37343 |
|
37344 this.init = function(ngModelCtrl_) { |
|
37345 ngModelCtrl = ngModelCtrl_; |
|
37346 ngModelCtrl.$render = this.render; |
|
37347 |
|
37348 this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; |
|
37349 this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; |
|
37350 |
|
37351 var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) : |
|
37352 new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max ); |
|
37353 $scope.range = this.buildTemplateObjects(ratingStates); |
|
37354 }; |
|
37355 |
|
37356 this.buildTemplateObjects = function(states) { |
|
37357 for (var i = 0, n = states.length; i < n; i++) { |
|
37358 states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]); |
|
37359 } |
|
37360 return states; |
|
37361 }; |
|
37362 |
|
37363 $scope.rate = function(value) { |
|
37364 if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) { |
|
37365 ngModelCtrl.$setViewValue(value); |
|
37366 ngModelCtrl.$render(); |
|
37367 } |
|
37368 }; |
|
37369 |
|
37370 $scope.enter = function(value) { |
|
37371 if ( !$scope.readonly ) { |
|
37372 $scope.value = value; |
|
37373 } |
|
37374 $scope.onHover({value: value}); |
|
37375 }; |
|
37376 |
|
37377 $scope.reset = function() { |
|
37378 $scope.value = ngModelCtrl.$viewValue; |
|
37379 $scope.onLeave(); |
|
37380 }; |
|
37381 |
|
37382 $scope.onKeydown = function(evt) { |
|
37383 if (/(37|38|39|40)/.test(evt.which)) { |
|
37384 evt.preventDefault(); |
|
37385 evt.stopPropagation(); |
|
37386 $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) ); |
|
37387 } |
|
37388 }; |
|
37389 |
|
37390 this.render = function() { |
|
37391 $scope.value = ngModelCtrl.$viewValue; |
|
37392 }; |
|
37393 }]) |
|
37394 |
|
37395 .directive('rating', function() { |
|
37396 return { |
|
37397 restrict: 'EA', |
|
37398 require: ['rating', 'ngModel'], |
|
37399 scope: { |
|
37400 readonly: '=?', |
|
37401 onHover: '&', |
|
37402 onLeave: '&' |
|
37403 }, |
|
37404 controller: 'RatingController', |
|
37405 templateUrl: 'template/rating/rating.html', |
|
37406 replace: true, |
|
37407 link: function(scope, element, attrs, ctrls) { |
|
37408 var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; |
|
37409 |
|
37410 if ( ngModelCtrl ) { |
|
37411 ratingCtrl.init( ngModelCtrl ); |
|
37412 } |
|
37413 } |
|
37414 }; |
|
37415 }); |
|
37416 |
|
37417 /** |
|
37418 * @ngdoc overview |
|
37419 * @name ui.bootstrap.tabs |
|
37420 * |
|
37421 * @description |
|
37422 * AngularJS version of the tabs directive. |
|
37423 */ |
|
37424 |
|
37425 angular.module('ui.bootstrap.tabs', []) |
|
37426 |
|
37427 .controller('TabsetController', ['$scope', function TabsetCtrl($scope) { |
|
37428 var ctrl = this, |
|
37429 tabs = ctrl.tabs = $scope.tabs = []; |
|
37430 |
|
37431 ctrl.select = function(selectedTab) { |
|
37432 angular.forEach(tabs, function(tab) { |
|
37433 if (tab.active && tab !== selectedTab) { |
|
37434 tab.active = false; |
|
37435 tab.onDeselect(); |
|
37436 } |
|
37437 }); |
|
37438 selectedTab.active = true; |
|
37439 selectedTab.onSelect(); |
|
37440 }; |
|
37441 |
|
37442 ctrl.addTab = function addTab(tab) { |
|
37443 tabs.push(tab); |
|
37444 // we can't run the select function on the first tab |
|
37445 // since that would select it twice |
|
37446 if (tabs.length === 1) { |
|
37447 tab.active = true; |
|
37448 } else if (tab.active) { |
|
37449 ctrl.select(tab); |
|
37450 } |
|
37451 }; |
|
37452 |
|
37453 ctrl.removeTab = function removeTab(tab) { |
|
37454 var index = tabs.indexOf(tab); |
|
37455 //Select a new tab if the tab to be removed is selected |
|
37456 if (tab.active && tabs.length > 1) { |
|
37457 //If this is the last tab, select the previous tab. else, the next tab. |
|
37458 var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; |
|
37459 ctrl.select(tabs[newActiveIndex]); |
|
37460 } |
|
37461 tabs.splice(index, 1); |
|
37462 }; |
|
37463 }]) |
|
37464 |
|
37465 /** |
|
37466 * @ngdoc directive |
|
37467 * @name ui.bootstrap.tabs.directive:tabset |
|
37468 * @restrict EA |
|
37469 * |
|
37470 * @description |
|
37471 * Tabset is the outer container for the tabs directive |
|
37472 * |
|
37473 * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. |
|
37474 * @param {boolean=} justified Whether or not to use justified styling for the tabs. |
|
37475 * |
|
37476 * @example |
|
37477 <example module="ui.bootstrap"> |
|
37478 <file name="index.html"> |
|
37479 <tabset> |
|
37480 <tab heading="Tab 1"><b>First</b> Content!</tab> |
|
37481 <tab heading="Tab 2"><i>Second</i> Content!</tab> |
|
37482 </tabset> |
|
37483 <hr /> |
|
37484 <tabset vertical="true"> |
|
37485 <tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab> |
|
37486 <tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab> |
|
37487 </tabset> |
|
37488 <tabset justified="true"> |
|
37489 <tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab> |
|
37490 <tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab> |
|
37491 </tabset> |
|
37492 </file> |
|
37493 </example> |
|
37494 */ |
|
37495 .directive('tabset', function() { |
|
37496 return { |
|
37497 restrict: 'EA', |
|
37498 transclude: true, |
|
37499 replace: true, |
|
37500 scope: { |
|
37501 type: '@' |
|
37502 }, |
|
37503 controller: 'TabsetController', |
|
37504 templateUrl: 'template/tabs/tabset.html', |
|
37505 link: function(scope, element, attrs) { |
|
37506 scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; |
|
37507 scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; |
|
37508 } |
|
37509 }; |
|
37510 }) |
|
37511 |
|
37512 /** |
|
37513 * @ngdoc directive |
|
37514 * @name ui.bootstrap.tabs.directive:tab |
|
37515 * @restrict EA |
|
37516 * |
|
37517 * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. |
|
37518 * @param {string=} select An expression to evaluate when the tab is selected. |
|
37519 * @param {boolean=} active A binding, telling whether or not this tab is selected. |
|
37520 * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. |
|
37521 * |
|
37522 * @description |
|
37523 * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. |
|
37524 * |
|
37525 * @example |
|
37526 <example module="ui.bootstrap"> |
|
37527 <file name="index.html"> |
|
37528 <div ng-controller="TabsDemoCtrl"> |
|
37529 <button class="btn btn-small" ng-click="items[0].active = true"> |
|
37530 Select item 1, using active binding |
|
37531 </button> |
|
37532 <button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled"> |
|
37533 Enable/disable item 2, using disabled binding |
|
37534 </button> |
|
37535 <br /> |
|
37536 <tabset> |
|
37537 <tab heading="Tab 1">First Tab</tab> |
|
37538 <tab select="alertMe()"> |
|
37539 <tab-heading><i class="icon-bell"></i> Alert me!</tab-heading> |
|
37540 Second Tab, with alert callback and html heading! |
|
37541 </tab> |
|
37542 <tab ng-repeat="item in items" |
|
37543 heading="{{item.title}}" |
|
37544 disabled="item.disabled" |
|
37545 active="item.active"> |
|
37546 {{item.content}} |
|
37547 </tab> |
|
37548 </tabset> |
|
37549 </div> |
|
37550 </file> |
|
37551 <file name="script.js"> |
|
37552 function TabsDemoCtrl($scope) { |
|
37553 $scope.items = [ |
|
37554 { title:"Dynamic Title 1", content:"Dynamic Item 0" }, |
|
37555 { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } |
|
37556 ]; |
|
37557 |
|
37558 $scope.alertMe = function() { |
|
37559 setTimeout(function() { |
|
37560 alert("You've selected the alert tab!"); |
|
37561 }); |
|
37562 }; |
|
37563 }; |
|
37564 </file> |
|
37565 </example> |
|
37566 */ |
|
37567 |
|
37568 /** |
|
37569 * @ngdoc directive |
|
37570 * @name ui.bootstrap.tabs.directive:tabHeading |
|
37571 * @restrict EA |
|
37572 * |
|
37573 * @description |
|
37574 * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. |
|
37575 * |
|
37576 * @example |
|
37577 <example module="ui.bootstrap"> |
|
37578 <file name="index.html"> |
|
37579 <tabset> |
|
37580 <tab> |
|
37581 <tab-heading><b>HTML</b> in my titles?!</tab-heading> |
|
37582 And some content, too! |
|
37583 </tab> |
|
37584 <tab> |
|
37585 <tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading> |
|
37586 That's right. |
|
37587 </tab> |
|
37588 </tabset> |
|
37589 </file> |
|
37590 </example> |
|
37591 */ |
|
37592 .directive('tab', ['$parse', function($parse) { |
|
37593 return { |
|
37594 require: '^tabset', |
|
37595 restrict: 'EA', |
|
37596 replace: true, |
|
37597 templateUrl: 'template/tabs/tab.html', |
|
37598 transclude: true, |
|
37599 scope: { |
|
37600 active: '=?', |
|
37601 heading: '@', |
|
37602 onSelect: '&select', //This callback is called in contentHeadingTransclude |
|
37603 //once it inserts the tab's content into the dom |
|
37604 onDeselect: '&deselect' |
|
37605 }, |
|
37606 controller: function() { |
|
37607 //Empty controller so other directives can require being 'under' a tab |
|
37608 }, |
|
37609 compile: function(elm, attrs, transclude) { |
|
37610 return function postLink(scope, elm, attrs, tabsetCtrl) { |
|
37611 scope.$watch('active', function(active) { |
|
37612 if (active) { |
|
37613 tabsetCtrl.select(scope); |
|
37614 } |
|
37615 }); |
|
37616 |
|
37617 scope.disabled = false; |
|
37618 if ( attrs.disabled ) { |
|
37619 scope.$parent.$watch($parse(attrs.disabled), function(value) { |
|
37620 scope.disabled = !! value; |
|
37621 }); |
|
37622 } |
|
37623 |
|
37624 scope.select = function() { |
|
37625 if ( !scope.disabled ) { |
|
37626 scope.active = true; |
|
37627 } |
|
37628 }; |
|
37629 |
|
37630 tabsetCtrl.addTab(scope); |
|
37631 scope.$on('$destroy', function() { |
|
37632 tabsetCtrl.removeTab(scope); |
|
37633 }); |
|
37634 |
|
37635 //We need to transclude later, once the content container is ready. |
|
37636 //when this link happens, we're inside a tab heading. |
|
37637 scope.$transcludeFn = transclude; |
|
37638 }; |
|
37639 } |
|
37640 }; |
|
37641 }]) |
|
37642 |
|
37643 .directive('tabHeadingTransclude', [function() { |
|
37644 return { |
|
37645 restrict: 'A', |
|
37646 require: '^tab', |
|
37647 link: function(scope, elm, attrs, tabCtrl) { |
|
37648 scope.$watch('headingElement', function updateHeadingElement(heading) { |
|
37649 if (heading) { |
|
37650 elm.html(''); |
|
37651 elm.append(heading); |
|
37652 } |
|
37653 }); |
|
37654 } |
|
37655 }; |
|
37656 }]) |
|
37657 |
|
37658 .directive('tabContentTransclude', function() { |
|
37659 return { |
|
37660 restrict: 'A', |
|
37661 require: '^tabset', |
|
37662 link: function(scope, elm, attrs) { |
|
37663 var tab = scope.$eval(attrs.tabContentTransclude); |
|
37664 |
|
37665 //Now our tab is ready to be transcluded: both the tab heading area |
|
37666 //and the tab content area are loaded. Transclude 'em both. |
|
37667 tab.$transcludeFn(tab.$parent, function(contents) { |
|
37668 angular.forEach(contents, function(node) { |
|
37669 if (isTabHeading(node)) { |
|
37670 //Let tabHeadingTransclude know. |
|
37671 tab.headingElement = node; |
|
37672 } else { |
|
37673 elm.append(node); |
|
37674 } |
|
37675 }); |
|
37676 }); |
|
37677 } |
|
37678 }; |
|
37679 function isTabHeading(node) { |
|
37680 return node.tagName && ( |
|
37681 node.hasAttribute('tab-heading') || |
|
37682 node.hasAttribute('data-tab-heading') || |
|
37683 node.tagName.toLowerCase() === 'tab-heading' || |
|
37684 node.tagName.toLowerCase() === 'data-tab-heading' |
|
37685 ); |
|
37686 } |
|
37687 }) |
|
37688 |
|
37689 ; |
|
37690 |
|
37691 angular.module('ui.bootstrap.timepicker', []) |
|
37692 |
|
37693 .constant('timepickerConfig', { |
|
37694 hourStep: 1, |
|
37695 minuteStep: 1, |
|
37696 showMeridian: true, |
|
37697 meridians: null, |
|
37698 readonlyInput: false, |
|
37699 mousewheel: true |
|
37700 }) |
|
37701 |
|
37702 .controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { |
|
37703 var selected = new Date(), |
|
37704 ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl |
|
37705 meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; |
|
37706 |
|
37707 this.init = function( ngModelCtrl_, inputs ) { |
|
37708 ngModelCtrl = ngModelCtrl_; |
|
37709 ngModelCtrl.$render = this.render; |
|
37710 |
|
37711 var hoursInputEl = inputs.eq(0), |
|
37712 minutesInputEl = inputs.eq(1); |
|
37713 |
|
37714 var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; |
|
37715 if ( mousewheel ) { |
|
37716 this.setupMousewheelEvents( hoursInputEl, minutesInputEl ); |
|
37717 } |
|
37718 |
|
37719 $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; |
|
37720 this.setupInputEvents( hoursInputEl, minutesInputEl ); |
|
37721 }; |
|
37722 |
|
37723 var hourStep = timepickerConfig.hourStep; |
|
37724 if ($attrs.hourStep) { |
|
37725 $scope.$parent.$watch($parse($attrs.hourStep), function(value) { |
|
37726 hourStep = parseInt(value, 10); |
|
37727 }); |
|
37728 } |
|
37729 |
|
37730 var minuteStep = timepickerConfig.minuteStep; |
|
37731 if ($attrs.minuteStep) { |
|
37732 $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { |
|
37733 minuteStep = parseInt(value, 10); |
|
37734 }); |
|
37735 } |
|
37736 |
|
37737 // 12H / 24H mode |
|
37738 $scope.showMeridian = timepickerConfig.showMeridian; |
|
37739 if ($attrs.showMeridian) { |
|
37740 $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { |
|
37741 $scope.showMeridian = !!value; |
|
37742 |
|
37743 if ( ngModelCtrl.$error.time ) { |
|
37744 // Evaluate from template |
|
37745 var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); |
|
37746 if (angular.isDefined( hours ) && angular.isDefined( minutes )) { |
|
37747 selected.setHours( hours ); |
|
37748 refresh(); |
|
37749 } |
|
37750 } else { |
|
37751 updateTemplate(); |
|
37752 } |
|
37753 }); |
|
37754 } |
|
37755 |
|
37756 // Get $scope.hours in 24H mode if valid |
|
37757 function getHoursFromTemplate ( ) { |
|
37758 var hours = parseInt( $scope.hours, 10 ); |
|
37759 var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); |
|
37760 if ( !valid ) { |
|
37761 return undefined; |
|
37762 } |
|
37763 |
|
37764 if ( $scope.showMeridian ) { |
|
37765 if ( hours === 12 ) { |
|
37766 hours = 0; |
|
37767 } |
|
37768 if ( $scope.meridian === meridians[1] ) { |
|
37769 hours = hours + 12; |
|
37770 } |
|
37771 } |
|
37772 return hours; |
|
37773 } |
|
37774 |
|
37775 function getMinutesFromTemplate() { |
|
37776 var minutes = parseInt($scope.minutes, 10); |
|
37777 return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined; |
|
37778 } |
|
37779 |
|
37780 function pad( value ) { |
|
37781 return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; |
|
37782 } |
|
37783 |
|
37784 // Respond on mousewheel spin |
|
37785 this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) { |
|
37786 var isScrollingUp = function(e) { |
|
37787 if (e.originalEvent) { |
|
37788 e = e.originalEvent; |
|
37789 } |
|
37790 //pick correct delta variable depending on event |
|
37791 var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; |
|
37792 return (e.detail || delta > 0); |
|
37793 }; |
|
37794 |
|
37795 hoursInputEl.bind('mousewheel wheel', function(e) { |
|
37796 $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() ); |
|
37797 e.preventDefault(); |
|
37798 }); |
|
37799 |
|
37800 minutesInputEl.bind('mousewheel wheel', function(e) { |
|
37801 $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() ); |
|
37802 e.preventDefault(); |
|
37803 }); |
|
37804 |
|
37805 }; |
|
37806 |
|
37807 this.setupInputEvents = function( hoursInputEl, minutesInputEl ) { |
|
37808 if ( $scope.readonlyInput ) { |
|
37809 $scope.updateHours = angular.noop; |
|
37810 $scope.updateMinutes = angular.noop; |
|
37811 return; |
|
37812 } |
|
37813 |
|
37814 var invalidate = function(invalidHours, invalidMinutes) { |
|
37815 ngModelCtrl.$setViewValue( null ); |
|
37816 ngModelCtrl.$setValidity('time', false); |
|
37817 if (angular.isDefined(invalidHours)) { |
|
37818 $scope.invalidHours = invalidHours; |
|
37819 } |
|
37820 if (angular.isDefined(invalidMinutes)) { |
|
37821 $scope.invalidMinutes = invalidMinutes; |
|
37822 } |
|
37823 }; |
|
37824 |
|
37825 $scope.updateHours = function() { |
|
37826 var hours = getHoursFromTemplate(); |
|
37827 |
|
37828 if ( angular.isDefined(hours) ) { |
|
37829 selected.setHours( hours ); |
|
37830 refresh( 'h' ); |
|
37831 } else { |
|
37832 invalidate(true); |
|
37833 } |
|
37834 }; |
|
37835 |
|
37836 hoursInputEl.bind('blur', function(e) { |
|
37837 if ( !$scope.invalidHours && $scope.hours < 10) { |
|
37838 $scope.$apply( function() { |
|
37839 $scope.hours = pad( $scope.hours ); |
|
37840 }); |
|
37841 } |
|
37842 }); |
|
37843 |
|
37844 $scope.updateMinutes = function() { |
|
37845 var minutes = getMinutesFromTemplate(); |
|
37846 |
|
37847 if ( angular.isDefined(minutes) ) { |
|
37848 selected.setMinutes( minutes ); |
|
37849 refresh( 'm' ); |
|
37850 } else { |
|
37851 invalidate(undefined, true); |
|
37852 } |
|
37853 }; |
|
37854 |
|
37855 minutesInputEl.bind('blur', function(e) { |
|
37856 if ( !$scope.invalidMinutes && $scope.minutes < 10 ) { |
|
37857 $scope.$apply( function() { |
|
37858 $scope.minutes = pad( $scope.minutes ); |
|
37859 }); |
|
37860 } |
|
37861 }); |
|
37862 |
|
37863 }; |
|
37864 |
|
37865 this.render = function() { |
|
37866 var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null; |
|
37867 |
|
37868 if ( isNaN(date) ) { |
|
37869 ngModelCtrl.$setValidity('time', false); |
|
37870 $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); |
|
37871 } else { |
|
37872 if ( date ) { |
|
37873 selected = date; |
|
37874 } |
|
37875 makeValid(); |
|
37876 updateTemplate(); |
|
37877 } |
|
37878 }; |
|
37879 |
|
37880 // Call internally when we know that model is valid. |
|
37881 function refresh( keyboardChange ) { |
|
37882 makeValid(); |
|
37883 ngModelCtrl.$setViewValue( new Date(selected) ); |
|
37884 updateTemplate( keyboardChange ); |
|
37885 } |
|
37886 |
|
37887 function makeValid() { |
|
37888 ngModelCtrl.$setValidity('time', true); |
|
37889 $scope.invalidHours = false; |
|
37890 $scope.invalidMinutes = false; |
|
37891 } |
|
37892 |
|
37893 function updateTemplate( keyboardChange ) { |
|
37894 var hours = selected.getHours(), minutes = selected.getMinutes(); |
|
37895 |
|
37896 if ( $scope.showMeridian ) { |
|
37897 hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system |
|
37898 } |
|
37899 |
|
37900 $scope.hours = keyboardChange === 'h' ? hours : pad(hours); |
|
37901 $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); |
|
37902 $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; |
|
37903 } |
|
37904 |
|
37905 function addMinutes( minutes ) { |
|
37906 var dt = new Date( selected.getTime() + minutes * 60000 ); |
|
37907 selected.setHours( dt.getHours(), dt.getMinutes() ); |
|
37908 refresh(); |
|
37909 } |
|
37910 |
|
37911 $scope.incrementHours = function() { |
|
37912 addMinutes( hourStep * 60 ); |
|
37913 }; |
|
37914 $scope.decrementHours = function() { |
|
37915 addMinutes( - hourStep * 60 ); |
|
37916 }; |
|
37917 $scope.incrementMinutes = function() { |
|
37918 addMinutes( minuteStep ); |
|
37919 }; |
|
37920 $scope.decrementMinutes = function() { |
|
37921 addMinutes( - minuteStep ); |
|
37922 }; |
|
37923 $scope.toggleMeridian = function() { |
|
37924 addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) ); |
|
37925 }; |
|
37926 }]) |
|
37927 |
|
37928 .directive('timepicker', function () { |
|
37929 return { |
|
37930 restrict: 'EA', |
|
37931 require: ['timepicker', '?^ngModel'], |
|
37932 controller:'TimepickerController', |
|
37933 replace: true, |
|
37934 scope: {}, |
|
37935 templateUrl: 'template/timepicker/timepicker.html', |
|
37936 link: function(scope, element, attrs, ctrls) { |
|
37937 var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; |
|
37938 |
|
37939 if ( ngModelCtrl ) { |
|
37940 timepickerCtrl.init( ngModelCtrl, element.find('input') ); |
|
37941 } |
|
37942 } |
|
37943 }; |
|
37944 }); |
|
37945 |
|
37946 angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) |
|
37947 |
|
37948 /** |
|
37949 * A helper service that can parse typeahead's syntax (string provided by users) |
|
37950 * Extracted to a separate service for ease of unit testing |
|
37951 */ |
|
37952 .factory('typeaheadParser', ['$parse', function ($parse) { |
|
37953 |
|
37954 // 00000111000000000000022200000000000000003333333333333330000000000044000 |
|
37955 var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; |
|
37956 |
|
37957 return { |
|
37958 parse:function (input) { |
|
37959 |
|
37960 var match = input.match(TYPEAHEAD_REGEXP); |
|
37961 if (!match) { |
|
37962 throw new Error( |
|
37963 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + |
|
37964 ' but got "' + input + '".'); |
|
37965 } |
|
37966 |
|
37967 return { |
|
37968 itemName:match[3], |
|
37969 source:$parse(match[4]), |
|
37970 viewMapper:$parse(match[2] || match[1]), |
|
37971 modelMapper:$parse(match[1]) |
|
37972 }; |
|
37973 } |
|
37974 }; |
|
37975 }]) |
|
37976 |
|
37977 .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', |
|
37978 function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { |
|
37979 |
|
37980 var HOT_KEYS = [9, 13, 27, 38, 40]; |
|
37981 |
|
37982 return { |
|
37983 require:'ngModel', |
|
37984 link:function (originalScope, element, attrs, modelCtrl) { |
|
37985 |
|
37986 //SUPPORTED ATTRIBUTES (OPTIONS) |
|
37987 |
|
37988 //minimal no of characters that needs to be entered before typeahead kicks-in |
|
37989 var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; |
|
37990 |
|
37991 //minimal wait time after last character typed before typehead kicks-in |
|
37992 var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; |
|
37993 |
|
37994 //should it restrict model values to the ones selected from the popup only? |
|
37995 var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; |
|
37996 |
|
37997 //binding to a variable that indicates if matches are being retrieved asynchronously |
|
37998 var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; |
|
37999 |
|
38000 //a callback executed when a match is selected |
|
38001 var onSelectCallback = $parse(attrs.typeaheadOnSelect); |
|
38002 |
|
38003 var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; |
|
38004 |
|
38005 var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; |
|
38006 |
|
38007 //INTERNAL VARIABLES |
|
38008 |
|
38009 //model setter executed upon match selection |
|
38010 var $setModelValue = $parse(attrs.ngModel).assign; |
|
38011 |
|
38012 //expressions used by typeahead |
|
38013 var parserResult = typeaheadParser.parse(attrs.typeahead); |
|
38014 |
|
38015 var hasFocus; |
|
38016 |
|
38017 //create a child scope for the typeahead directive so we are not polluting original scope |
|
38018 //with typeahead-specific data (matches, query etc.) |
|
38019 var scope = originalScope.$new(); |
|
38020 originalScope.$on('$destroy', function(){ |
|
38021 scope.$destroy(); |
|
38022 }); |
|
38023 |
|
38024 // WAI-ARIA |
|
38025 var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); |
|
38026 element.attr({ |
|
38027 'aria-autocomplete': 'list', |
|
38028 'aria-expanded': false, |
|
38029 'aria-owns': popupId |
|
38030 }); |
|
38031 |
|
38032 //pop-up element used to display matches |
|
38033 var popUpEl = angular.element('<div typeahead-popup></div>'); |
|
38034 popUpEl.attr({ |
|
38035 id: popupId, |
|
38036 matches: 'matches', |
|
38037 active: 'activeIdx', |
|
38038 select: 'select(activeIdx)', |
|
38039 query: 'query', |
|
38040 position: 'position' |
|
38041 }); |
|
38042 //custom item template |
|
38043 if (angular.isDefined(attrs.typeaheadTemplateUrl)) { |
|
38044 popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); |
|
38045 } |
|
38046 |
|
38047 var resetMatches = function() { |
|
38048 scope.matches = []; |
|
38049 scope.activeIdx = -1; |
|
38050 element.attr('aria-expanded', false); |
|
38051 }; |
|
38052 |
|
38053 var getMatchId = function(index) { |
|
38054 return popupId + '-option-' + index; |
|
38055 }; |
|
38056 |
|
38057 // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. |
|
38058 // This attribute is added or removed automatically when the `activeIdx` changes. |
|
38059 scope.$watch('activeIdx', function(index) { |
|
38060 if (index < 0) { |
|
38061 element.removeAttr('aria-activedescendant'); |
|
38062 } else { |
|
38063 element.attr('aria-activedescendant', getMatchId(index)); |
|
38064 } |
|
38065 }); |
|
38066 |
|
38067 var getMatchesAsync = function(inputValue) { |
|
38068 |
|
38069 var locals = {$viewValue: inputValue}; |
|
38070 isLoadingSetter(originalScope, true); |
|
38071 $q.when(parserResult.source(originalScope, locals)).then(function(matches) { |
|
38072 |
|
38073 //it might happen that several async queries were in progress if a user were typing fast |
|
38074 //but we are interested only in responses that correspond to the current view value |
|
38075 var onCurrentRequest = (inputValue === modelCtrl.$viewValue); |
|
38076 if (onCurrentRequest && hasFocus) { |
|
38077 if (matches.length > 0) { |
|
38078 |
|
38079 scope.activeIdx = 0; |
|
38080 scope.matches.length = 0; |
|
38081 |
|
38082 //transform labels |
|
38083 for(var i=0; i<matches.length; i++) { |
|
38084 locals[parserResult.itemName] = matches[i]; |
|
38085 scope.matches.push({ |
|
38086 id: getMatchId(i), |
|
38087 label: parserResult.viewMapper(scope, locals), |
|
38088 model: matches[i] |
|
38089 }); |
|
38090 } |
|
38091 |
|
38092 scope.query = inputValue; |
|
38093 //position pop-up with matches - we need to re-calculate its position each time we are opening a window |
|
38094 //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page |
|
38095 //due to other elements being rendered |
|
38096 scope.position = appendToBody ? $position.offset(element) : $position.position(element); |
|
38097 scope.position.top = scope.position.top + element.prop('offsetHeight'); |
|
38098 |
|
38099 element.attr('aria-expanded', true); |
|
38100 } else { |
|
38101 resetMatches(); |
|
38102 } |
|
38103 } |
|
38104 if (onCurrentRequest) { |
|
38105 isLoadingSetter(originalScope, false); |
|
38106 } |
|
38107 }, function(){ |
|
38108 resetMatches(); |
|
38109 isLoadingSetter(originalScope, false); |
|
38110 }); |
|
38111 }; |
|
38112 |
|
38113 resetMatches(); |
|
38114 |
|
38115 //we need to propagate user's query so we can higlight matches |
|
38116 scope.query = undefined; |
|
38117 |
|
38118 //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later |
|
38119 var timeoutPromise; |
|
38120 |
|
38121 var scheduleSearchWithTimeout = function(inputValue) { |
|
38122 timeoutPromise = $timeout(function () { |
|
38123 getMatchesAsync(inputValue); |
|
38124 }, waitTime); |
|
38125 }; |
|
38126 |
|
38127 var cancelPreviousTimeout = function() { |
|
38128 if (timeoutPromise) { |
|
38129 $timeout.cancel(timeoutPromise); |
|
38130 } |
|
38131 }; |
|
38132 |
|
38133 //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM |
|
38134 //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue |
|
38135 modelCtrl.$parsers.unshift(function (inputValue) { |
|
38136 |
|
38137 hasFocus = true; |
|
38138 |
|
38139 if (inputValue && inputValue.length >= minSearch) { |
|
38140 if (waitTime > 0) { |
|
38141 cancelPreviousTimeout(); |
|
38142 scheduleSearchWithTimeout(inputValue); |
|
38143 } else { |
|
38144 getMatchesAsync(inputValue); |
|
38145 } |
|
38146 } else { |
|
38147 isLoadingSetter(originalScope, false); |
|
38148 cancelPreviousTimeout(); |
|
38149 resetMatches(); |
|
38150 } |
|
38151 |
|
38152 if (isEditable) { |
|
38153 return inputValue; |
|
38154 } else { |
|
38155 if (!inputValue) { |
|
38156 // Reset in case user had typed something previously. |
|
38157 modelCtrl.$setValidity('editable', true); |
|
38158 return inputValue; |
|
38159 } else { |
|
38160 modelCtrl.$setValidity('editable', false); |
|
38161 return undefined; |
|
38162 } |
|
38163 } |
|
38164 }); |
|
38165 |
|
38166 modelCtrl.$formatters.push(function (modelValue) { |
|
38167 |
|
38168 var candidateViewValue, emptyViewValue; |
|
38169 var locals = {}; |
|
38170 |
|
38171 if (inputFormatter) { |
|
38172 |
|
38173 locals['$model'] = modelValue; |
|
38174 return inputFormatter(originalScope, locals); |
|
38175 |
|
38176 } else { |
|
38177 |
|
38178 //it might happen that we don't have enough info to properly render input value |
|
38179 //we need to check for this situation and simply return model value if we can't apply custom formatting |
|
38180 locals[parserResult.itemName] = modelValue; |
|
38181 candidateViewValue = parserResult.viewMapper(originalScope, locals); |
|
38182 locals[parserResult.itemName] = undefined; |
|
38183 emptyViewValue = parserResult.viewMapper(originalScope, locals); |
|
38184 |
|
38185 return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; |
|
38186 } |
|
38187 }); |
|
38188 |
|
38189 scope.select = function (activeIdx) { |
|
38190 //called from within the $digest() cycle |
|
38191 var locals = {}; |
|
38192 var model, item; |
|
38193 |
|
38194 locals[parserResult.itemName] = item = scope.matches[activeIdx].model; |
|
38195 model = parserResult.modelMapper(originalScope, locals); |
|
38196 $setModelValue(originalScope, model); |
|
38197 modelCtrl.$setValidity('editable', true); |
|
38198 |
|
38199 onSelectCallback(originalScope, { |
|
38200 $item: item, |
|
38201 $model: model, |
|
38202 $label: parserResult.viewMapper(originalScope, locals) |
|
38203 }); |
|
38204 |
|
38205 resetMatches(); |
|
38206 |
|
38207 //return focus to the input element if a match was selected via a mouse click event |
|
38208 // use timeout to avoid $rootScope:inprog error |
|
38209 $timeout(function() { element[0].focus(); }, 0, false); |
|
38210 }; |
|
38211 |
|
38212 //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) |
|
38213 element.bind('keydown', function (evt) { |
|
38214 |
|
38215 //typeahead is open and an "interesting" key was pressed |
|
38216 if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { |
|
38217 return; |
|
38218 } |
|
38219 |
|
38220 evt.preventDefault(); |
|
38221 |
|
38222 if (evt.which === 40) { |
|
38223 scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; |
|
38224 scope.$digest(); |
|
38225 |
|
38226 } else if (evt.which === 38) { |
|
38227 scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; |
|
38228 scope.$digest(); |
|
38229 |
|
38230 } else if (evt.which === 13 || evt.which === 9) { |
|
38231 scope.$apply(function () { |
|
38232 scope.select(scope.activeIdx); |
|
38233 }); |
|
38234 |
|
38235 } else if (evt.which === 27) { |
|
38236 evt.stopPropagation(); |
|
38237 |
|
38238 resetMatches(); |
|
38239 scope.$digest(); |
|
38240 } |
|
38241 }); |
|
38242 |
|
38243 element.bind('blur', function (evt) { |
|
38244 hasFocus = false; |
|
38245 }); |
|
38246 |
|
38247 // Keep reference to click handler to unbind it. |
|
38248 var dismissClickHandler = function (evt) { |
|
38249 if (element[0] !== evt.target) { |
|
38250 resetMatches(); |
|
38251 scope.$digest(); |
|
38252 } |
|
38253 }; |
|
38254 |
|
38255 $document.bind('click', dismissClickHandler); |
|
38256 |
|
38257 originalScope.$on('$destroy', function(){ |
|
38258 $document.unbind('click', dismissClickHandler); |
|
38259 }); |
|
38260 |
|
38261 var $popup = $compile(popUpEl)(scope); |
|
38262 if ( appendToBody ) { |
|
38263 $document.find('body').append($popup); |
|
38264 } else { |
|
38265 element.after($popup); |
|
38266 } |
|
38267 } |
|
38268 }; |
|
38269 |
|
38270 }]) |
|
38271 |
|
38272 .directive('typeaheadPopup', function () { |
|
38273 return { |
|
38274 restrict:'EA', |
|
38275 scope:{ |
|
38276 matches:'=', |
|
38277 query:'=', |
|
38278 active:'=', |
|
38279 position:'=', |
|
38280 select:'&' |
|
38281 }, |
|
38282 replace:true, |
|
38283 templateUrl:'template/typeahead/typeahead-popup.html', |
|
38284 link:function (scope, element, attrs) { |
|
38285 |
|
38286 scope.templateUrl = attrs.templateUrl; |
|
38287 |
|
38288 scope.isOpen = function () { |
|
38289 return scope.matches.length > 0; |
|
38290 }; |
|
38291 |
|
38292 scope.isActive = function (matchIdx) { |
|
38293 return scope.active == matchIdx; |
|
38294 }; |
|
38295 |
|
38296 scope.selectActive = function (matchIdx) { |
|
38297 scope.active = matchIdx; |
|
38298 }; |
|
38299 |
|
38300 scope.selectMatch = function (activeIdx) { |
|
38301 scope.select({activeIdx:activeIdx}); |
|
38302 }; |
|
38303 } |
|
38304 }; |
|
38305 }) |
|
38306 |
|
38307 .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { |
|
38308 return { |
|
38309 restrict:'EA', |
|
38310 scope:{ |
|
38311 index:'=', |
|
38312 match:'=', |
|
38313 query:'=' |
|
38314 }, |
|
38315 link:function (scope, element, attrs) { |
|
38316 var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; |
|
38317 $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){ |
|
38318 element.replaceWith($compile(tplContent.trim())(scope)); |
|
38319 }); |
|
38320 } |
|
38321 }; |
|
38322 }]) |
|
38323 |
|
38324 .filter('typeaheadHighlight', function() { |
|
38325 |
|
38326 function escapeRegexp(queryToEscape) { |
|
38327 return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); |
|
38328 } |
|
38329 |
|
38330 return function(matchItem, query) { |
|
38331 return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem; |
|
38332 }; |
|
38333 }); |
|
38334 |
|
38335 angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { |
|
38336 $templateCache.put("template/accordion/accordion-group.html", |
|
38337 "<div class=\"panel panel-default\">\n" + |
|
38338 " <div class=\"panel-heading\">\n" + |
|
38339 " <h4 class=\"panel-title\">\n" + |
|
38340 " <a class=\"accordion-toggle\" ng-click=\"toggleOpen()\" accordion-transclude=\"heading\"><span ng-class=\"{'text-muted': isDisabled}\">{{heading}}</span></a>\n" + |
|
38341 " </h4>\n" + |
|
38342 " </div>\n" + |
|
38343 " <div class=\"panel-collapse\" collapse=\"!isOpen\">\n" + |
|
38344 " <div class=\"panel-body\" ng-transclude></div>\n" + |
|
38345 " </div>\n" + |
|
38346 "</div>"); |
|
38347 }]); |
|
38348 |
|
38349 angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { |
|
38350 $templateCache.put("template/accordion/accordion.html", |
|
38351 "<div class=\"panel-group\" ng-transclude></div>"); |
|
38352 }]); |
|
38353 |
|
38354 angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { |
|
38355 $templateCache.put("template/alert/alert.html", |
|
38356 "<div class=\"alert\" ng-class=\"['alert-' + (type || 'warning'), closeable ? 'alert-dismissable' : null]\" role=\"alert\">\n" + |
|
38357 " <button ng-show=\"closeable\" type=\"button\" class=\"close\" ng-click=\"close()\">\n" + |
|
38358 " <span aria-hidden=\"true\">×</span>\n" + |
|
38359 " <span class=\"sr-only\">Close</span>\n" + |
|
38360 " </button>\n" + |
|
38361 " <div ng-transclude></div>\n" + |
|
38362 "</div>\n" + |
|
38363 ""); |
|
38364 }]); |
|
38365 |
|
38366 angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { |
|
38367 $templateCache.put("template/carousel/carousel.html", |
|
38368 "<div ng-mouseenter=\"pause()\" ng-mouseleave=\"play()\" class=\"carousel\" ng-swipe-right=\"prev()\" ng-swipe-left=\"next()\">\n" + |
|
38369 " <ol class=\"carousel-indicators\" ng-show=\"slides.length > 1\">\n" + |
|
38370 " <li ng-repeat=\"slide in slides track by $index\" ng-class=\"{active: isActive(slide)}\" ng-click=\"select(slide)\"></li>\n" + |
|
38371 " </ol>\n" + |
|
38372 " <div class=\"carousel-inner\" ng-transclude></div>\n" + |
|
38373 " <a class=\"left carousel-control\" ng-click=\"prev()\" ng-show=\"slides.length > 1\"><span class=\"glyphicon glyphicon-chevron-left\"></span></a>\n" + |
|
38374 " <a class=\"right carousel-control\" ng-click=\"next()\" ng-show=\"slides.length > 1\"><span class=\"glyphicon glyphicon-chevron-right\"></span></a>\n" + |
|
38375 "</div>\n" + |
|
38376 ""); |
|
38377 }]); |
|
38378 |
|
38379 angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { |
|
38380 $templateCache.put("template/carousel/slide.html", |
|
38381 "<div ng-class=\"{\n" + |
|
38382 " 'active': leaving || (active && !entering),\n" + |
|
38383 " 'prev': (next || active) && direction=='prev',\n" + |
|
38384 " 'next': (next || active) && direction=='next',\n" + |
|
38385 " 'right': direction=='prev',\n" + |
|
38386 " 'left': direction=='next'\n" + |
|
38387 " }\" class=\"item text-center\" ng-transclude></div>\n" + |
|
38388 ""); |
|
38389 }]); |
|
38390 |
|
38391 angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { |
|
38392 $templateCache.put("template/datepicker/datepicker.html", |
|
38393 "<div ng-switch=\"datepickerMode\" role=\"application\" ng-keydown=\"keydown($event)\">\n" + |
|
38394 " <daypicker ng-switch-when=\"day\" tabindex=\"0\"></daypicker>\n" + |
|
38395 " <monthpicker ng-switch-when=\"month\" tabindex=\"0\"></monthpicker>\n" + |
|
38396 " <yearpicker ng-switch-when=\"year\" tabindex=\"0\"></yearpicker>\n" + |
|
38397 "</div>"); |
|
38398 }]); |
|
38399 |
|
38400 angular.module("template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { |
|
38401 $templateCache.put("template/datepicker/day.html", |
|
38402 "<table role=\"grid\" aria-labelledby=\"{{uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + |
|
38403 " <thead>\n" + |
|
38404 " <tr>\n" + |
|
38405 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + |
|
38406 " <th colspan=\"{{5 + showWeeks}}\"><button id=\"{{uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm\" ng-click=\"toggleMode()\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" + |
|
38407 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + |
|
38408 " </tr>\n" + |
|
38409 " <tr>\n" + |
|
38410 " <th ng-show=\"showWeeks\" class=\"text-center\"></th>\n" + |
|
38411 " <th ng-repeat=\"label in labels track by $index\" class=\"text-center\"><small aria-label=\"{{label.full}}\">{{label.abbr}}</small></th>\n" + |
|
38412 " </tr>\n" + |
|
38413 " </thead>\n" + |
|
38414 " <tbody>\n" + |
|
38415 " <tr ng-repeat=\"row in rows track by $index\">\n" + |
|
38416 " <td ng-show=\"showWeeks\" class=\"text-center h6\"><em>{{ weekNumbers[$index] }}</em></td>\n" + |
|
38417 " <td ng-repeat=\"dt in row track by dt.date\" class=\"text-center\" role=\"gridcell\" id=\"{{dt.uid}}\" aria-disabled=\"{{!!dt.disabled}}\">\n" + |
|
38418 " <button type=\"button\" style=\"width:100%;\" class=\"btn btn-default btn-sm\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"{'text-muted': dt.secondary, 'text-info': dt.current}\">{{dt.label}}</span></button>\n" + |
|
38419 " </td>\n" + |
|
38420 " </tr>\n" + |
|
38421 " </tbody>\n" + |
|
38422 "</table>\n" + |
|
38423 ""); |
|
38424 }]); |
|
38425 |
|
38426 angular.module("template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { |
|
38427 $templateCache.put("template/datepicker/month.html", |
|
38428 "<table role=\"grid\" aria-labelledby=\"{{uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + |
|
38429 " <thead>\n" + |
|
38430 " <tr>\n" + |
|
38431 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + |
|
38432 " <th><button id=\"{{uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm\" ng-click=\"toggleMode()\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" + |
|
38433 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + |
|
38434 " </tr>\n" + |
|
38435 " </thead>\n" + |
|
38436 " <tbody>\n" + |
|
38437 " <tr ng-repeat=\"row in rows track by $index\">\n" + |
|
38438 " <td ng-repeat=\"dt in row track by dt.date\" class=\"text-center\" role=\"gridcell\" id=\"{{dt.uid}}\" aria-disabled=\"{{!!dt.disabled}}\">\n" + |
|
38439 " <button type=\"button\" style=\"width:100%;\" class=\"btn btn-default\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"{'text-info': dt.current}\">{{dt.label}}</span></button>\n" + |
|
38440 " </td>\n" + |
|
38441 " </tr>\n" + |
|
38442 " </tbody>\n" + |
|
38443 "</table>\n" + |
|
38444 ""); |
|
38445 }]); |
|
38446 |
|
38447 angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { |
|
38448 $templateCache.put("template/datepicker/popup.html", |
|
38449 "<ul class=\"dropdown-menu\" ng-style=\"{display: (isOpen && 'block') || 'none', top: position.top+'px', left: position.left+'px'}\" ng-keydown=\"keydown($event)\">\n" + |
|
38450 " <li ng-transclude></li>\n" + |
|
38451 " <li ng-if=\"showButtonBar\" style=\"padding:10px 9px 2px\">\n" + |
|
38452 " <span class=\"btn-group\">\n" + |
|
38453 " <button type=\"button\" class=\"btn btn-sm btn-info\" ng-click=\"select('today')\">{{ getText('current') }}</button>\n" + |
|
38454 " <button type=\"button\" class=\"btn btn-sm btn-danger\" ng-click=\"select(null)\">{{ getText('clear') }}</button>\n" + |
|
38455 " </span>\n" + |
|
38456 " <button type=\"button\" class=\"btn btn-sm btn-success pull-right\" ng-click=\"close()\">{{ getText('close') }}</button>\n" + |
|
38457 " </li>\n" + |
|
38458 "</ul>\n" + |
|
38459 ""); |
|
38460 }]); |
|
38461 |
|
38462 angular.module("template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { |
|
38463 $templateCache.put("template/datepicker/year.html", |
|
38464 "<table role=\"grid\" aria-labelledby=\"{{uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + |
|
38465 " <thead>\n" + |
|
38466 " <tr>\n" + |
|
38467 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + |
|
38468 " <th colspan=\"3\"><button id=\"{{uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm\" ng-click=\"toggleMode()\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" + |
|
38469 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + |
|
38470 " </tr>\n" + |
|
38471 " </thead>\n" + |
|
38472 " <tbody>\n" + |
|
38473 " <tr ng-repeat=\"row in rows track by $index\">\n" + |
|
38474 " <td ng-repeat=\"dt in row track by dt.date\" class=\"text-center\" role=\"gridcell\" id=\"{{dt.uid}}\" aria-disabled=\"{{!!dt.disabled}}\">\n" + |
|
38475 " <button type=\"button\" style=\"width:100%;\" class=\"btn btn-default\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"{'text-info': dt.current}\">{{dt.label}}</span></button>\n" + |
|
38476 " </td>\n" + |
|
38477 " </tr>\n" + |
|
38478 " </tbody>\n" + |
|
38479 "</table>\n" + |
|
38480 ""); |
|
38481 }]); |
|
38482 |
|
38483 angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { |
|
38484 $templateCache.put("template/modal/backdrop.html", |
|
38485 "<div class=\"modal-backdrop fade {{ backdropClass }}\"\n" + |
|
38486 " ng-class=\"{in: animate}\"\n" + |
|
38487 " ng-style=\"{'z-index': 1040 + (index && 1 || 0) + index*10}\"\n" + |
|
38488 "></div>\n" + |
|
38489 ""); |
|
38490 }]); |
|
38491 |
|
38492 angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { |
|
38493 $templateCache.put("template/modal/window.html", |
|
38494 "<div tabindex=\"-1\" role=\"dialog\" class=\"modal fade\" ng-class=\"{in: animate}\" ng-style=\"{'z-index': 1050 + index*10, display: 'block'}\" ng-click=\"close($event)\">\n" + |
|
38495 " <div class=\"modal-dialog\" ng-class=\"{'modal-sm': size == 'sm', 'modal-lg': size == 'lg'}\"><div class=\"modal-content\" modal-transclude></div></div>\n" + |
|
38496 "</div>"); |
|
38497 }]); |
|
38498 |
|
38499 angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { |
|
38500 $templateCache.put("template/pagination/pager.html", |
|
38501 "<ul class=\"pager\">\n" + |
|
38502 " <li ng-class=\"{disabled: noPrevious(), previous: align}\"><a href ng-click=\"selectPage(page - 1)\">{{getText('previous')}}</a></li>\n" + |
|
38503 " <li ng-class=\"{disabled: noNext(), next: align}\"><a href ng-click=\"selectPage(page + 1)\">{{getText('next')}}</a></li>\n" + |
|
38504 "</ul>"); |
|
38505 }]); |
|
38506 |
|
38507 angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { |
|
38508 $templateCache.put("template/pagination/pagination.html", |
|
38509 "<ul class=\"pagination\">\n" + |
|
38510 " <li ng-if=\"boundaryLinks\" ng-class=\"{disabled: noPrevious()}\"><a href ng-click=\"selectPage(1)\">{{getText('first')}}</a></li>\n" + |
|
38511 " <li ng-if=\"directionLinks\" ng-class=\"{disabled: noPrevious()}\"><a href ng-click=\"selectPage(page - 1)\">{{getText('previous')}}</a></li>\n" + |
|
38512 " <li ng-repeat=\"page in pages track by $index\" ng-class=\"{active: page.active}\"><a href ng-click=\"selectPage(page.number)\">{{page.text}}</a></li>\n" + |
|
38513 " <li ng-if=\"directionLinks\" ng-class=\"{disabled: noNext()}\"><a href ng-click=\"selectPage(page + 1)\">{{getText('next')}}</a></li>\n" + |
|
38514 " <li ng-if=\"boundaryLinks\" ng-class=\"{disabled: noNext()}\"><a href ng-click=\"selectPage(totalPages)\">{{getText('last')}}</a></li>\n" + |
|
38515 "</ul>"); |
|
38516 }]); |
|
38517 |
|
38518 angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { |
|
38519 $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", |
|
38520 "<div class=\"tooltip {{placement}}\" ng-class=\"{ in: isOpen(), fade: animation() }\">\n" + |
|
38521 " <div class=\"tooltip-arrow\"></div>\n" + |
|
38522 " <div class=\"tooltip-inner\" bind-html-unsafe=\"content\"></div>\n" + |
|
38523 "</div>\n" + |
|
38524 ""); |
|
38525 }]); |
|
38526 |
|
38527 angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { |
|
38528 $templateCache.put("template/tooltip/tooltip-popup.html", |
|
38529 "<div class=\"tooltip {{placement}}\" ng-class=\"{ in: isOpen(), fade: animation() }\">\n" + |
|
38530 " <div class=\"tooltip-arrow\"></div>\n" + |
|
38531 " <div class=\"tooltip-inner\" ng-bind=\"content\"></div>\n" + |
|
38532 "</div>\n" + |
|
38533 ""); |
|
38534 }]); |
|
38535 |
|
38536 angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) { |
|
38537 $templateCache.put("template/popover/popover.html", |
|
38538 "<div class=\"popover {{placement}}\" ng-class=\"{ in: isOpen(), fade: animation() }\">\n" + |
|
38539 " <div class=\"arrow\"></div>\n" + |
|
38540 "\n" + |
|
38541 " <div class=\"popover-inner\">\n" + |
|
38542 " <h3 class=\"popover-title\" ng-bind=\"title\" ng-show=\"title\"></h3>\n" + |
|
38543 " <div class=\"popover-content\" ng-bind=\"content\"></div>\n" + |
|
38544 " </div>\n" + |
|
38545 "</div>\n" + |
|
38546 ""); |
|
38547 }]); |
|
38548 |
|
38549 angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { |
|
38550 $templateCache.put("template/progressbar/bar.html", |
|
38551 "<div class=\"progress-bar\" ng-class=\"type && 'progress-bar-' + type\" role=\"progressbar\" aria-valuenow=\"{{value}}\" aria-valuemin=\"0\" aria-valuemax=\"{{max}}\" ng-style=\"{width: percent + '%'}\" aria-valuetext=\"{{percent | number:0}}%\" ng-transclude></div>"); |
|
38552 }]); |
|
38553 |
|
38554 angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { |
|
38555 $templateCache.put("template/progressbar/progress.html", |
|
38556 "<div class=\"progress\" ng-transclude></div>"); |
|
38557 }]); |
|
38558 |
|
38559 angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { |
|
38560 $templateCache.put("template/progressbar/progressbar.html", |
|
38561 "<div class=\"progress\">\n" + |
|
38562 " <div class=\"progress-bar\" ng-class=\"type && 'progress-bar-' + type\" role=\"progressbar\" aria-valuenow=\"{{value}}\" aria-valuemin=\"0\" aria-valuemax=\"{{max}}\" ng-style=\"{width: percent + '%'}\" aria-valuetext=\"{{percent | number:0}}%\" ng-transclude></div>\n" + |
|
38563 "</div>"); |
|
38564 }]); |
|
38565 |
|
38566 angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) { |
|
38567 $templateCache.put("template/rating/rating.html", |
|
38568 "<span ng-mouseleave=\"reset()\" ng-keydown=\"onKeydown($event)\" tabindex=\"0\" role=\"slider\" aria-valuemin=\"0\" aria-valuemax=\"{{range.length}}\" aria-valuenow=\"{{value}}\">\n" + |
|
38569 " <i ng-repeat=\"r in range track by $index\" ng-mouseenter=\"enter($index + 1)\" ng-click=\"rate($index + 1)\" class=\"glyphicon\" ng-class=\"$index < value && (r.stateOn || 'glyphicon-star') || (r.stateOff || 'glyphicon-star-empty')\">\n" + |
|
38570 " <span class=\"sr-only\">({{ $index < value ? '*' : ' ' }})</span>\n" + |
|
38571 " </i>\n" + |
|
38572 "</span>"); |
|
38573 }]); |
|
38574 |
|
38575 angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { |
|
38576 $templateCache.put("template/tabs/tab.html", |
|
38577 "<li ng-class=\"{active: active, disabled: disabled}\">\n" + |
|
38578 " <a ng-click=\"select()\" tab-heading-transclude>{{heading}}</a>\n" + |
|
38579 "</li>\n" + |
|
38580 ""); |
|
38581 }]); |
|
38582 |
|
38583 angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { |
|
38584 $templateCache.put("template/tabs/tabset.html", |
|
38585 "<div>\n" + |
|
38586 " <ul class=\"nav nav-{{type || 'tabs'}}\" ng-class=\"{'nav-stacked': vertical, 'nav-justified': justified}\" ng-transclude></ul>\n" + |
|
38587 " <div class=\"tab-content\">\n" + |
|
38588 " <div class=\"tab-pane\" \n" + |
|
38589 " ng-repeat=\"tab in tabs\" \n" + |
|
38590 " ng-class=\"{active: tab.active}\"\n" + |
|
38591 " tab-content-transclude=\"tab\">\n" + |
|
38592 " </div>\n" + |
|
38593 " </div>\n" + |
|
38594 "</div>\n" + |
|
38595 ""); |
|
38596 }]); |
|
38597 |
|
38598 angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { |
|
38599 $templateCache.put("template/timepicker/timepicker.html", |
|
38600 "<table>\n" + |
|
38601 " <tbody>\n" + |
|
38602 " <tr class=\"text-center\">\n" + |
|
38603 " <td><a ng-click=\"incrementHours()\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" + |
|
38604 " <td> </td>\n" + |
|
38605 " <td><a ng-click=\"incrementMinutes()\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" + |
|
38606 " <td ng-show=\"showMeridian\"></td>\n" + |
|
38607 " </tr>\n" + |
|
38608 " <tr>\n" + |
|
38609 " <td style=\"width:50px;\" class=\"form-group\" ng-class=\"{'has-error': invalidHours}\">\n" + |
|
38610 " <input type=\"text\" ng-model=\"hours\" ng-change=\"updateHours()\" class=\"form-control text-center\" ng-mousewheel=\"incrementHours()\" ng-readonly=\"readonlyInput\" maxlength=\"2\">\n" + |
|
38611 " </td>\n" + |
|
38612 " <td>:</td>\n" + |
|
38613 " <td style=\"width:50px;\" class=\"form-group\" ng-class=\"{'has-error': invalidMinutes}\">\n" + |
|
38614 " <input type=\"text\" ng-model=\"minutes\" ng-change=\"updateMinutes()\" class=\"form-control text-center\" ng-readonly=\"readonlyInput\" maxlength=\"2\">\n" + |
|
38615 " </td>\n" + |
|
38616 " <td ng-show=\"showMeridian\"><button type=\"button\" class=\"btn btn-default text-center\" ng-click=\"toggleMeridian()\">{{meridian}}</button></td>\n" + |
|
38617 " </tr>\n" + |
|
38618 " <tr class=\"text-center\">\n" + |
|
38619 " <td><a ng-click=\"decrementHours()\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" + |
|
38620 " <td> </td>\n" + |
|
38621 " <td><a ng-click=\"decrementMinutes()\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" + |
|
38622 " <td ng-show=\"showMeridian\"></td>\n" + |
|
38623 " </tr>\n" + |
|
38624 " </tbody>\n" + |
|
38625 "</table>\n" + |
|
38626 ""); |
|
38627 }]); |
|
38628 |
|
38629 angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { |
|
38630 $templateCache.put("template/typeahead/typeahead-match.html", |
|
38631 "<a tabindex=\"-1\" bind-html-unsafe=\"match.label | typeaheadHighlight:query\"></a>"); |
|
38632 }]); |
|
38633 |
|
38634 angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { |
|
38635 $templateCache.put("template/typeahead/typeahead-popup.html", |
|
38636 "<ul class=\"dropdown-menu\" ng-show=\"isOpen()\" ng-style=\"{top: position.top+'px', left: position.left+'px'}\" style=\"display: block;\" role=\"listbox\" aria-hidden=\"{{!isOpen()}}\">\n" + |
|
38637 " <li ng-repeat=\"match in matches track by $index\" ng-class=\"{active: isActive($index) }\" ng-mouseenter=\"selectActive($index)\" ng-click=\"selectMatch($index)\" role=\"option\" id=\"{{match.id}}\">\n" + |
|
38638 " <div typeahead-match index=\"$index\" match=\"match\" query=\"query\" template-url=\"templateUrl\"></div>\n" + |
|
38639 " </li>\n" + |
|
38640 "</ul>\n" + |
|
38641 ""); |
|
38642 }]); |
|
38643 |
|
38644 /** |
|
38645 * @license AngularJS v1.3.0-rc.5 |
|
38646 * (c) 2010-2014 Google, Inc. http://angularjs.org |
|
38647 * License: MIT |
|
38648 */ |
|
38649 (function(window, angular, undefined) {'use strict'; |
|
38650 |
|
38651 var $resourceMinErr = angular.$$minErr('$resource'); |
|
38652 |
|
38653 // Helper functions and regex to lookup a dotted path on an object |
|
38654 // stopping at undefined/null. The path must be composed of ASCII |
|
38655 // identifiers (just like $parse) |
|
38656 var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/; |
|
38657 |
|
38658 function isValidDottedPath(path) { |
|
38659 return (path != null && path !== '' && path !== 'hasOwnProperty' && |
|
38660 MEMBER_NAME_REGEX.test('.' + path)); |
|
38661 } |
|
38662 |
|
38663 function lookupDottedPath(obj, path) { |
|
38664 if (!isValidDottedPath(path)) { |
|
38665 throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); |
|
38666 } |
|
38667 var keys = path.split('.'); |
|
38668 for (var i = 0, ii = keys.length; i < ii && obj !== undefined; i++) { |
|
38669 var key = keys[i]; |
|
38670 obj = (obj !== null) ? obj[key] : undefined; |
|
38671 } |
|
38672 return obj; |
|
38673 } |
|
38674 |
|
38675 /** |
|
38676 * Create a shallow copy of an object and clear other fields from the destination |
|
38677 */ |
|
38678 function shallowClearAndCopy(src, dst) { |
|
38679 dst = dst || {}; |
|
38680 |
|
38681 angular.forEach(dst, function(value, key){ |
|
38682 delete dst[key]; |
|
38683 }); |
|
38684 |
|
38685 for (var key in src) { |
|
38686 if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { |
|
38687 dst[key] = src[key]; |
|
38688 } |
|
38689 } |
|
38690 |
|
38691 return dst; |
|
38692 } |
|
38693 |
|
38694 /** |
|
38695 * @ngdoc module |
|
38696 * @name ngResource |
|
38697 * @description |
|
38698 * |
|
38699 * # ngResource |
|
38700 * |
|
38701 * The `ngResource` module provides interaction support with RESTful services |
|
38702 * via the $resource service. |
|
38703 * |
|
38704 * |
|
38705 * <div doc-module-components="ngResource"></div> |
|
38706 * |
|
38707 * See {@link ngResource.$resource `$resource`} for usage. |
|
38708 */ |
|
38709 |
|
38710 /** |
|
38711 * @ngdoc service |
|
38712 * @name $resource |
|
38713 * @requires $http |
|
38714 * |
|
38715 * @description |
|
38716 * A factory which creates a resource object that lets you interact with |
|
38717 * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. |
|
38718 * |
|
38719 * The returned resource object has action methods which provide high-level behaviors without |
|
38720 * the need to interact with the low level {@link ng.$http $http} service. |
|
38721 * |
|
38722 * Requires the {@link ngResource `ngResource`} module to be installed. |
|
38723 * |
|
38724 * By default, trailing slashes will be stripped from the calculated URLs, |
|
38725 * which can pose problems with server backends that do not expect that |
|
38726 * behavior. This can be disabled by configuring the `$resourceProvider` like |
|
38727 * this: |
|
38728 * |
|
38729 * ```js |
|
38730 app.config(['$resourceProvider', function ($resourceProvider) { |
|
38731 // Don't strip trailing slashes from calculated URLs |
|
38732 $resourceProvider.defaults.stripTrailingSlashes = false; |
|
38733 }]); |
|
38734 * ``` |
|
38735 * |
|
38736 * @param {string} url A parametrized URL template with parameters prefixed by `:` as in |
|
38737 * `/user/:username`. If you are using a URL with a port number (e.g. |
|
38738 * `http://example.com:8080/api`), it will be respected. |
|
38739 * |
|
38740 * If you are using a url with a suffix, just add the suffix, like this: |
|
38741 * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` |
|
38742 * or even `$resource('http://example.com/resource/:resource_id.:format')` |
|
38743 * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be |
|
38744 * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you |
|
38745 * can escape it with `/\.`. |
|
38746 * |
|
38747 * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in |
|
38748 * `actions` methods. If any of the parameter value is a function, it will be executed every time |
|
38749 * when a param value needs to be obtained for a request (unless the param was overridden). |
|
38750 * |
|
38751 * Each key value in the parameter object is first bound to url template if present and then any |
|
38752 * excess keys are appended to the url search query after the `?`. |
|
38753 * |
|
38754 * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in |
|
38755 * URL `/path/greet?salutation=Hello`. |
|
38756 * |
|
38757 * If the parameter value is prefixed with `@` then the value for that parameter will be extracted |
|
38758 * from the corresponding property on the `data` object (provided when calling an action method). For |
|
38759 * example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of `someParam` |
|
38760 * will be `data.someProp`. |
|
38761 * |
|
38762 * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend |
|
38763 * the default set of resource actions. The declaration should be created in the format of {@link |
|
38764 * ng.$http#usage_parameters $http.config}: |
|
38765 * |
|
38766 * {action1: {method:?, params:?, isArray:?, headers:?, ...}, |
|
38767 * action2: {method:?, params:?, isArray:?, headers:?, ...}, |
|
38768 * ...} |
|
38769 * |
|
38770 * Where: |
|
38771 * |
|
38772 * - **`action`** – {string} – The name of action. This name becomes the name of the method on |
|
38773 * your resource object. |
|
38774 * - **`method`** – {string} – Case insensitive HTTP method (e.g. `GET`, `POST`, `PUT`, |
|
38775 * `DELETE`, `JSONP`, etc). |
|
38776 * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of |
|
38777 * the parameter value is a function, it will be executed every time when a param value needs to |
|
38778 * be obtained for a request (unless the param was overridden). |
|
38779 * - **`url`** – {string} – action specific `url` override. The url templating is supported just |
|
38780 * like for the resource-level urls. |
|
38781 * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, |
|
38782 * see `returns` section. |
|
38783 * - **`transformRequest`** – |
|
38784 * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – |
|
38785 * transform function or an array of such functions. The transform function takes the http |
|
38786 * request body and headers and returns its transformed (typically serialized) version. |
|
38787 * By default, transformRequest will contain one function that checks if the request data is |
|
38788 * an object and serializes to using `angular.toJson`. To prevent this behavior, set |
|
38789 * `transformRequest` to an empty array: `transformRequest: []` |
|
38790 * - **`transformResponse`** – |
|
38791 * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – |
|
38792 * transform function or an array of such functions. The transform function takes the http |
|
38793 * response body and headers and returns its transformed (typically deserialized) version. |
|
38794 * By default, transformResponse will contain one function that checks if the response looks like |
|
38795 * a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior, set |
|
38796 * `transformResponse` to an empty array: `transformResponse: []` |
|
38797 * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the |
|
38798 * GET request, otherwise if a cache instance built with |
|
38799 * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for |
|
38800 * caching. |
|
38801 * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that |
|
38802 * should abort the request when resolved. |
|
38803 * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the |
|
38804 * XHR object. See |
|
38805 * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) |
|
38806 * for more information. |
|
38807 * - **`responseType`** - `{string}` - see |
|
38808 * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). |
|
38809 * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - |
|
38810 * `response` and `responseError`. Both `response` and `responseError` interceptors get called |
|
38811 * with `http response` object. See {@link ng.$http $http interceptors}. |
|
38812 * |
|
38813 * @param {Object} options Hash with custom settings that should extend the |
|
38814 * default `$resourceProvider` behavior. The only supported option is |
|
38815 * |
|
38816 * Where: |
|
38817 * |
|
38818 * - **`stripTrailingSlashes`** – {boolean} – If true then the trailing |
|
38819 * slashes from any calculated URL will be stripped. (Defaults to true.) |
|
38820 * |
|
38821 * @returns {Object} A resource "class" object with methods for the default set of resource actions |
|
38822 * optionally extended with custom `actions`. The default set contains these actions: |
|
38823 * ```js |
|
38824 * { 'get': {method:'GET'}, |
|
38825 * 'save': {method:'POST'}, |
|
38826 * 'query': {method:'GET', isArray:true}, |
|
38827 * 'remove': {method:'DELETE'}, |
|
38828 * 'delete': {method:'DELETE'} }; |
|
38829 * ``` |
|
38830 * |
|
38831 * Calling these methods invoke an {@link ng.$http} with the specified http method, |
|
38832 * destination and parameters. When the data is returned from the server then the object is an |
|
38833 * instance of the resource class. The actions `save`, `remove` and `delete` are available on it |
|
38834 * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, |
|
38835 * read, update, delete) on server-side data like this: |
|
38836 * ```js |
|
38837 * var User = $resource('/user/:userId', {userId:'@id'}); |
|
38838 * var user = User.get({userId:123}, function() { |
|
38839 * user.abc = true; |
|
38840 * user.$save(); |
|
38841 * }); |
|
38842 * ``` |
|
38843 * |
|
38844 * It is important to realize that invoking a $resource object method immediately returns an |
|
38845 * empty reference (object or array depending on `isArray`). Once the data is returned from the |
|
38846 * server the existing reference is populated with the actual data. This is a useful trick since |
|
38847 * usually the resource is assigned to a model which is then rendered by the view. Having an empty |
|
38848 * object results in no rendering, once the data arrives from the server then the object is |
|
38849 * populated with the data and the view automatically re-renders itself showing the new data. This |
|
38850 * means that in most cases one never has to write a callback function for the action methods. |
|
38851 * |
|
38852 * The action methods on the class object or instance object can be invoked with the following |
|
38853 * parameters: |
|
38854 * |
|
38855 * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` |
|
38856 * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` |
|
38857 * - non-GET instance actions: `instance.$action([parameters], [success], [error])` |
|
38858 * |
|
38859 * Success callback is called with (value, responseHeaders) arguments. Error callback is called |
|
38860 * with (httpResponse) argument. |
|
38861 * |
|
38862 * Class actions return empty instance (with additional properties below). |
|
38863 * Instance actions return promise of the action. |
|
38864 * |
|
38865 * The Resource instances and collection have these additional properties: |
|
38866 * |
|
38867 * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this |
|
38868 * instance or collection. |
|
38869 * |
|
38870 * On success, the promise is resolved with the same resource instance or collection object, |
|
38871 * updated with data from server. This makes it easy to use in |
|
38872 * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view |
|
38873 * rendering until the resource(s) are loaded. |
|
38874 * |
|
38875 * On failure, the promise is resolved with the {@link ng.$http http response} object, without |
|
38876 * the `resource` property. |
|
38877 * |
|
38878 * If an interceptor object was provided, the promise will instead be resolved with the value |
|
38879 * returned by the interceptor. |
|
38880 * |
|
38881 * - `$resolved`: `true` after first server interaction is completed (either with success or |
|
38882 * rejection), `false` before that. Knowing if the Resource has been resolved is useful in |
|
38883 * data-binding. |
|
38884 * |
|
38885 * @example |
|
38886 * |
|
38887 * # Credit card resource |
|
38888 * |
|
38889 * ```js |
|
38890 // Define CreditCard class |
|
38891 var CreditCard = $resource('/user/:userId/card/:cardId', |
|
38892 {userId:123, cardId:'@id'}, { |
|
38893 charge: {method:'POST', params:{charge:true}} |
|
38894 }); |
|
38895 |
|
38896 // We can retrieve a collection from the server |
|
38897 var cards = CreditCard.query(function() { |
|
38898 // GET: /user/123/card |
|
38899 // server returns: [ {id:456, number:'1234', name:'Smith'} ]; |
|
38900 |
|
38901 var card = cards[0]; |
|
38902 // each item is an instance of CreditCard |
|
38903 expect(card instanceof CreditCard).toEqual(true); |
|
38904 card.name = "J. Smith"; |
|
38905 // non GET methods are mapped onto the instances |
|
38906 card.$save(); |
|
38907 // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} |
|
38908 // server returns: {id:456, number:'1234', name: 'J. Smith'}; |
|
38909 |
|
38910 // our custom method is mapped as well. |
|
38911 card.$charge({amount:9.99}); |
|
38912 // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} |
|
38913 }); |
|
38914 |
|
38915 // we can create an instance as well |
|
38916 var newCard = new CreditCard({number:'0123'}); |
|
38917 newCard.name = "Mike Smith"; |
|
38918 newCard.$save(); |
|
38919 // POST: /user/123/card {number:'0123', name:'Mike Smith'} |
|
38920 // server returns: {id:789, number:'0123', name: 'Mike Smith'}; |
|
38921 expect(newCard.id).toEqual(789); |
|
38922 * ``` |
|
38923 * |
|
38924 * The object returned from this function execution is a resource "class" which has "static" method |
|
38925 * for each action in the definition. |
|
38926 * |
|
38927 * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and |
|
38928 * `headers`. |
|
38929 * When the data is returned from the server then the object is an instance of the resource type and |
|
38930 * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD |
|
38931 * operations (create, read, update, delete) on server-side data. |
|
38932 |
|
38933 ```js |
|
38934 var User = $resource('/user/:userId', {userId:'@id'}); |
|
38935 User.get({userId:123}, function(user) { |
|
38936 user.abc = true; |
|
38937 user.$save(); |
|
38938 }); |
|
38939 ``` |
|
38940 * |
|
38941 * It's worth noting that the success callback for `get`, `query` and other methods gets passed |
|
38942 * in the response that came from the server as well as $http header getter function, so one |
|
38943 * could rewrite the above example and get access to http headers as: |
|
38944 * |
|
38945 ```js |
|
38946 var User = $resource('/user/:userId', {userId:'@id'}); |
|
38947 User.get({userId:123}, function(u, getResponseHeaders){ |
|
38948 u.abc = true; |
|
38949 u.$save(function(u, putResponseHeaders) { |
|
38950 //u => saved user object |
|
38951 //putResponseHeaders => $http header getter |
|
38952 }); |
|
38953 }); |
|
38954 ``` |
|
38955 * |
|
38956 * You can also access the raw `$http` promise via the `$promise` property on the object returned |
|
38957 * |
|
38958 ``` |
|
38959 var User = $resource('/user/:userId', {userId:'@id'}); |
|
38960 User.get({userId:123}) |
|
38961 .$promise.then(function(user) { |
|
38962 $scope.user = user; |
|
38963 }); |
|
38964 ``` |
|
38965 |
|
38966 * # Creating a custom 'PUT' request |
|
38967 * In this example we create a custom method on our resource to make a PUT request |
|
38968 * ```js |
|
38969 * var app = angular.module('app', ['ngResource', 'ngRoute']); |
|
38970 * |
|
38971 * // Some APIs expect a PUT request in the format URL/object/ID |
|
38972 * // Here we are creating an 'update' method |
|
38973 * app.factory('Notes', ['$resource', function($resource) { |
|
38974 * return $resource('/notes/:id', null, |
|
38975 * { |
|
38976 * 'update': { method:'PUT' } |
|
38977 * }); |
|
38978 * }]); |
|
38979 * |
|
38980 * // In our controller we get the ID from the URL using ngRoute and $routeParams |
|
38981 * // We pass in $routeParams and our Notes factory along with $scope |
|
38982 * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', |
|
38983 function($scope, $routeParams, Notes) { |
|
38984 * // First get a note object from the factory |
|
38985 * var note = Notes.get({ id:$routeParams.id }); |
|
38986 * $id = note.id; |
|
38987 * |
|
38988 * // Now call update passing in the ID first then the object you are updating |
|
38989 * Notes.update({ id:$id }, note); |
|
38990 * |
|
38991 * // This will PUT /notes/ID with the note object in the request payload |
|
38992 * }]); |
|
38993 * ``` |
|
38994 */ |
|
38995 angular.module('ngResource', ['ng']). |
|
38996 provider('$resource', function () { |
|
38997 var provider = this; |
|
38998 |
|
38999 this.defaults = { |
|
39000 // Strip slashes by default |
|
39001 stripTrailingSlashes: true, |
|
39002 |
|
39003 // Default actions configuration |
|
39004 actions: { |
|
39005 'get': {method: 'GET'}, |
|
39006 'save': {method: 'POST'}, |
|
39007 'query': {method: 'GET', isArray: true}, |
|
39008 'remove': {method: 'DELETE'}, |
|
39009 'delete': {method: 'DELETE'} |
|
39010 } |
|
39011 }; |
|
39012 |
|
39013 this.$get = ['$http', '$q', function ($http, $q) { |
|
39014 |
|
39015 var noop = angular.noop, |
|
39016 forEach = angular.forEach, |
|
39017 extend = angular.extend, |
|
39018 copy = angular.copy, |
|
39019 isFunction = angular.isFunction; |
|
39020 |
|
39021 /** |
|
39022 * We need our custom method because encodeURIComponent is too aggressive and doesn't follow |
|
39023 * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set |
|
39024 * (pchar) allowed in path segments: |
|
39025 * segment = *pchar |
|
39026 * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
|
39027 * pct-encoded = "%" HEXDIG HEXDIG |
|
39028 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" |
|
39029 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" |
|
39030 * / "*" / "+" / "," / ";" / "=" |
|
39031 */ |
|
39032 function encodeUriSegment(val) { |
|
39033 return encodeUriQuery(val, true). |
|
39034 replace(/%26/gi, '&'). |
|
39035 replace(/%3D/gi, '='). |
|
39036 replace(/%2B/gi, '+'); |
|
39037 } |
|
39038 |
|
39039 |
|
39040 /** |
|
39041 * This method is intended for encoding *key* or *value* parts of query component. We need a |
|
39042 * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't |
|
39043 * have to be encoded per http://tools.ietf.org/html/rfc3986: |
|
39044 * query = *( pchar / "/" / "?" ) |
|
39045 * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
|
39046 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" |
|
39047 * pct-encoded = "%" HEXDIG HEXDIG |
|
39048 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" |
|
39049 * / "*" / "+" / "," / ";" / "=" |
|
39050 */ |
|
39051 function encodeUriQuery(val, pctEncodeSpaces) { |
|
39052 return encodeURIComponent(val). |
|
39053 replace(/%40/gi, '@'). |
|
39054 replace(/%3A/gi, ':'). |
|
39055 replace(/%24/g, '$'). |
|
39056 replace(/%2C/gi, ','). |
|
39057 replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); |
|
39058 } |
|
39059 |
|
39060 function Route(template, defaults) { |
|
39061 this.template = template; |
|
39062 this.defaults = extend({}, provider.defaults, defaults); |
|
39063 this.urlParams = {}; |
|
39064 } |
|
39065 |
|
39066 Route.prototype = { |
|
39067 setUrlParams: function (config, params, actionUrl) { |
|
39068 var self = this, |
|
39069 url = actionUrl || self.template, |
|
39070 val, |
|
39071 encodedVal; |
|
39072 |
|
39073 var urlParams = self.urlParams = {}; |
|
39074 forEach(url.split(/\W/), function (param) { |
|
39075 if (param === 'hasOwnProperty') { |
|
39076 throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); |
|
39077 } |
|
39078 if (!(new RegExp("^\\d+$").test(param)) && param && |
|
39079 (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { |
|
39080 urlParams[param] = true; |
|
39081 } |
|
39082 }); |
|
39083 url = url.replace(/\\:/g, ':'); |
|
39084 |
|
39085 params = params || {}; |
|
39086 forEach(self.urlParams, function (_, urlParam) { |
|
39087 val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; |
|
39088 if (angular.isDefined(val) && val !== null) { |
|
39089 encodedVal = encodeUriSegment(val); |
|
39090 url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function (match, p1) { |
|
39091 return encodedVal + p1; |
|
39092 }); |
|
39093 } else { |
|
39094 url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function (match, |
|
39095 leadingSlashes, tail) { |
|
39096 if (tail.charAt(0) == '/') { |
|
39097 return tail; |
|
39098 } else { |
|
39099 return leadingSlashes + tail; |
|
39100 } |
|
39101 }); |
|
39102 } |
|
39103 }); |
|
39104 |
|
39105 // strip trailing slashes and set the url (unless this behavior is specifically disabled) |
|
39106 if (self.defaults.stripTrailingSlashes) { |
|
39107 url = url.replace(/\/+$/, '') || '/'; |
|
39108 } |
|
39109 |
|
39110 // then replace collapse `/.` if found in the last URL path segment before the query |
|
39111 // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` |
|
39112 url = url.replace(/\/\.(?=\w+($|\?))/, '.'); |
|
39113 // replace escaped `/\.` with `/.` |
|
39114 config.url = url.replace(/\/\\\./, '/.'); |
|
39115 |
|
39116 |
|
39117 // set params - delegate param encoding to $http |
|
39118 forEach(params, function (value, key) { |
|
39119 if (!self.urlParams[key]) { |
|
39120 config.params = config.params || {}; |
|
39121 config.params[key] = value; |
|
39122 } |
|
39123 }); |
|
39124 } |
|
39125 }; |
|
39126 |
|
39127 |
|
39128 function resourceFactory(url, paramDefaults, actions, options) { |
|
39129 var route = new Route(url, options); |
|
39130 |
|
39131 actions = extend({}, provider.defaults.actions, actions); |
|
39132 |
|
39133 function extractParams(data, actionParams) { |
|
39134 var ids = {}; |
|
39135 actionParams = extend({}, paramDefaults, actionParams); |
|
39136 forEach(actionParams, function (value, key) { |
|
39137 if (isFunction(value)) { value = value(); } |
|
39138 ids[key] = value && value.charAt && value.charAt(0) == '@' ? |
|
39139 lookupDottedPath(data, value.substr(1)) : value; |
|
39140 }); |
|
39141 return ids; |
|
39142 } |
|
39143 |
|
39144 function defaultResponseInterceptor(response) { |
|
39145 return response.resource; |
|
39146 } |
|
39147 |
|
39148 function Resource(value) { |
|
39149 shallowClearAndCopy(value || {}, this); |
|
39150 } |
|
39151 |
|
39152 Resource.prototype.toJSON = function () { |
|
39153 var data = extend({}, this); |
|
39154 delete data.$promise; |
|
39155 delete data.$resolved; |
|
39156 return data; |
|
39157 }; |
|
39158 |
|
39159 forEach(actions, function (action, name) { |
|
39160 var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); |
|
39161 |
|
39162 Resource[name] = function (a1, a2, a3, a4) { |
|
39163 var params = {}, data, success, error; |
|
39164 |
|
39165 /* jshint -W086 */ /* (purposefully fall through case statements) */ |
|
39166 switch (arguments.length) { |
|
39167 case 4: |
|
39168 error = a4; |
|
39169 success = a3; |
|
39170 //fallthrough |
|
39171 case 3: |
|
39172 case 2: |
|
39173 if (isFunction(a2)) { |
|
39174 if (isFunction(a1)) { |
|
39175 success = a1; |
|
39176 error = a2; |
|
39177 break; |
|
39178 } |
|
39179 |
|
39180 success = a2; |
|
39181 error = a3; |
|
39182 //fallthrough |
|
39183 } else { |
|
39184 params = a1; |
|
39185 data = a2; |
|
39186 success = a3; |
|
39187 break; |
|
39188 } |
|
39189 case 1: |
|
39190 if (isFunction(a1)) success = a1; |
|
39191 else if (hasBody) data = a1; |
|
39192 else params = a1; |
|
39193 break; |
|
39194 case 0: break; |
|
39195 default: |
|
39196 throw $resourceMinErr('badargs', |
|
39197 "Expected up to 4 arguments [params, data, success, error], got {0} arguments", |
|
39198 arguments.length); |
|
39199 } |
|
39200 /* jshint +W086 */ /* (purposefully fall through case statements) */ |
|
39201 |
|
39202 var isInstanceCall = this instanceof Resource; |
|
39203 var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); |
|
39204 var httpConfig = {}; |
|
39205 var responseInterceptor = action.interceptor && action.interceptor.response || |
|
39206 defaultResponseInterceptor; |
|
39207 var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || |
|
39208 undefined; |
|
39209 |
|
39210 forEach(action, function (value, key) { |
|
39211 if (key != 'params' && key != 'isArray' && key != 'interceptor') { |
|
39212 httpConfig[key] = copy(value); |
|
39213 } |
|
39214 }); |
|
39215 |
|
39216 if (hasBody) httpConfig.data = data; |
|
39217 route.setUrlParams(httpConfig, |
|
39218 extend({}, extractParams(data, action.params || {}), params), |
|
39219 action.url); |
|
39220 |
|
39221 var promise = $http(httpConfig).then(function (response) { |
|
39222 var data = response.data, |
|
39223 promise = value.$promise; |
|
39224 |
|
39225 if (data) { |
|
39226 // Need to convert action.isArray to boolean in case it is undefined |
|
39227 // jshint -W018 |
|
39228 if (angular.isArray(data) !== (!!action.isArray)) { |
|
39229 throw $resourceMinErr('badcfg', |
|
39230 'Error in resource configuration for action `{0}`. Expected response to ' + |
|
39231 'contain an {1} but got an {2}', name, action.isArray ? 'array' : 'object', |
|
39232 angular.isArray(data) ? 'array' : 'object'); |
|
39233 } |
|
39234 // jshint +W018 |
|
39235 if (action.isArray) { |
|
39236 value.length = 0; |
|
39237 forEach(data, function (item) { |
|
39238 if (typeof item === "object") { |
|
39239 value.push(new Resource(item)); |
|
39240 } else { |
|
39241 // Valid JSON values may be string literals, and these should not be converted |
|
39242 // into objects. These items will not have access to the Resource prototype |
|
39243 // methods, but unfortunately there |
|
39244 value.push(item); |
|
39245 } |
|
39246 }); |
|
39247 } else { |
|
39248 shallowClearAndCopy(data, value); |
|
39249 value.$promise = promise; |
|
39250 } |
|
39251 } |
|
39252 |
|
39253 value.$resolved = true; |
|
39254 |
|
39255 response.resource = value; |
|
39256 |
|
39257 return response; |
|
39258 }, function (response) { |
|
39259 value.$resolved = true; |
|
39260 |
|
39261 (error || noop)(response); |
|
39262 |
|
39263 return $q.reject(response); |
|
39264 }); |
|
39265 |
|
39266 promise = promise.then( |
|
39267 function (response) { |
|
39268 var value = responseInterceptor(response); |
|
39269 (success || noop)(value, response.headers); |
|
39270 return value; |
|
39271 }, |
|
39272 responseErrorInterceptor); |
|
39273 |
|
39274 if (!isInstanceCall) { |
|
39275 // we are creating instance / collection |
|
39276 // - set the initial promise |
|
39277 // - return the instance / collection |
|
39278 value.$promise = promise; |
|
39279 value.$resolved = false; |
|
39280 |
|
39281 return value; |
|
39282 } |
|
39283 |
|
39284 // instance call |
|
39285 return promise; |
|
39286 }; |
|
39287 |
|
39288 |
|
39289 Resource.prototype['$' + name] = function (params, success, error) { |
|
39290 if (isFunction(params)) { |
|
39291 error = success; success = params; params = {}; |
|
39292 } |
|
39293 var result = Resource[name].call(this, params, this, success, error); |
|
39294 return result.$promise || result; |
|
39295 }; |
|
39296 }); |
|
39297 |
|
39298 Resource.bind = function (additionalParamDefaults) { |
|
39299 return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); |
|
39300 }; |
|
39301 |
|
39302 return Resource; |
|
39303 } |
|
39304 |
|
39305 return resourceFactory; |
|
39306 }]; |
|
39307 }); |
|
39308 |
|
39309 |
|
39310 })(window, window.angular); |
|
39311 |
|
39312 /** |
|
39313 * @license AngularJS v1.3.0-rc.5 |
|
39314 * (c) 2010-2014 Google, Inc. http://angularjs.org |
|
39315 * License: MIT |
|
39316 */ |
|
39317 (function(window, angular, undefined) {'use strict'; |
|
39318 |
|
39319 /** |
|
39320 * @ngdoc module |
|
39321 * @name ngRoute |
|
39322 * @description |
|
39323 * |
|
39324 * # ngRoute |
|
39325 * |
|
39326 * The `ngRoute` module provides routing and deeplinking services and directives for angular apps. |
|
39327 * |
|
39328 * ## Example |
|
39329 * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. |
|
39330 * |
|
39331 * |
|
39332 * <div doc-module-components="ngRoute"></div> |
|
39333 */ |
|
39334 /* global -ngRouteModule */ |
|
39335 var ngRouteModule = angular.module('ngRoute', ['ng']). |
|
39336 provider('$route', $RouteProvider), |
|
39337 $routeMinErr = angular.$$minErr('ngRoute'); |
|
39338 |
|
39339 /** |
|
39340 * @ngdoc provider |
|
39341 * @name $routeProvider |
|
39342 * |
|
39343 * @description |
|
39344 * |
|
39345 * Used for configuring routes. |
|
39346 * |
|
39347 * ## Example |
|
39348 * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. |
|
39349 * |
|
39350 * ## Dependencies |
|
39351 * Requires the {@link ngRoute `ngRoute`} module to be installed. |
|
39352 */ |
|
39353 function $RouteProvider(){ |
|
39354 function inherit(parent, extra) { |
|
39355 return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra); |
|
39356 } |
|
39357 |
|
39358 var routes = {}; |
|
39359 |
|
39360 /** |
|
39361 * @ngdoc method |
|
39362 * @name $routeProvider#when |
|
39363 * |
|
39364 * @param {string} path Route path (matched against `$location.path`). If `$location.path` |
|
39365 * contains redundant trailing slash or is missing one, the route will still match and the |
|
39366 * `$location.path` will be updated to add or drop the trailing slash to exactly match the |
|
39367 * route definition. |
|
39368 * |
|
39369 * * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up |
|
39370 * to the next slash are matched and stored in `$routeParams` under the given `name` |
|
39371 * when the route matches. |
|
39372 * * `path` can contain named groups starting with a colon and ending with a star: |
|
39373 * e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name` |
|
39374 * when the route matches. |
|
39375 * * `path` can contain optional named groups with a question mark: e.g.`:name?`. |
|
39376 * |
|
39377 * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match |
|
39378 * `/color/brown/largecode/code/with/slashes/edit` and extract: |
|
39379 * |
|
39380 * * `color: brown` |
|
39381 * * `largecode: code/with/slashes`. |
|
39382 * |
|
39383 * |
|
39384 * @param {Object} route Mapping information to be assigned to `$route.current` on route |
|
39385 * match. |
|
39386 * |
|
39387 * Object properties: |
|
39388 * |
|
39389 * - `controller` – `{(string|function()=}` – Controller fn that should be associated with |
|
39390 * newly created scope or the name of a {@link angular.Module#controller registered |
|
39391 * controller} if passed as a string. |
|
39392 * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be |
|
39393 * published to scope under the `controllerAs` name. |
|
39394 * - `template` – `{string=|function()=}` – html template as a string or a function that |
|
39395 * returns an html template as a string which should be used by {@link |
|
39396 * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. |
|
39397 * This property takes precedence over `templateUrl`. |
|
39398 * |
|
39399 * If `template` is a function, it will be called with the following parameters: |
|
39400 * |
|
39401 * - `{Array.<Object>}` - route parameters extracted from the current |
|
39402 * `$location.path()` by applying the current route |
|
39403 * |
|
39404 * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html |
|
39405 * template that should be used by {@link ngRoute.directive:ngView ngView}. |
|
39406 * |
|
39407 * If `templateUrl` is a function, it will be called with the following parameters: |
|
39408 * |
|
39409 * - `{Array.<Object>}` - route parameters extracted from the current |
|
39410 * `$location.path()` by applying the current route |
|
39411 * |
|
39412 * - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should |
|
39413 * be injected into the controller. If any of these dependencies are promises, the router |
|
39414 * will wait for them all to be resolved or one to be rejected before the controller is |
|
39415 * instantiated. |
|
39416 * If all the promises are resolved successfully, the values of the resolved promises are |
|
39417 * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is |
|
39418 * fired. If any of the promises are rejected the |
|
39419 * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object |
|
39420 * is: |
|
39421 * |
|
39422 * - `key` – `{string}`: a name of a dependency to be injected into the controller. |
|
39423 * - `factory` - `{string|function}`: If `string` then it is an alias for a service. |
|
39424 * Otherwise if function, then it is {@link auto.$injector#invoke injected} |
|
39425 * and the return value is treated as the dependency. If the result is a promise, it is |
|
39426 * resolved before its value is injected into the controller. Be aware that |
|
39427 * `ngRoute.$routeParams` will still refer to the previous route within these resolve |
|
39428 * functions. Use `$route.current.params` to access the new route parameters, instead. |
|
39429 * |
|
39430 * - `redirectTo` – {(string|function())=} – value to update |
|
39431 * {@link ng.$location $location} path with and trigger route redirection. |
|
39432 * |
|
39433 * If `redirectTo` is a function, it will be called with the following parameters: |
|
39434 * |
|
39435 * - `{Object.<string>}` - route parameters extracted from the current |
|
39436 * `$location.path()` by applying the current route templateUrl. |
|
39437 * - `{string}` - current `$location.path()` |
|
39438 * - `{Object}` - current `$location.search()` |
|
39439 * |
|
39440 * The custom `redirectTo` function is expected to return a string which will be used |
|
39441 * to update `$location.path()` and `$location.search()`. |
|
39442 * |
|
39443 * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` |
|
39444 * or `$location.hash()` changes. |
|
39445 * |
|
39446 * If the option is set to `false` and url in the browser changes, then |
|
39447 * `$routeUpdate` event is broadcasted on the root scope. |
|
39448 * |
|
39449 * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive |
|
39450 * |
|
39451 * If the option is set to `true`, then the particular route can be matched without being |
|
39452 * case sensitive |
|
39453 * |
|
39454 * @returns {Object} self |
|
39455 * |
|
39456 * @description |
|
39457 * Adds a new route definition to the `$route` service. |
|
39458 */ |
|
39459 this.when = function(path, route) { |
|
39460 routes[path] = angular.extend( |
|
39461 {reloadOnSearch: true}, |
|
39462 route, |
|
39463 path && pathRegExp(path, route) |
|
39464 ); |
|
39465 |
|
39466 // create redirection for trailing slashes |
|
39467 if (path) { |
|
39468 var redirectPath = (path[path.length-1] == '/') |
|
39469 ? path.substr(0, path.length-1) |
|
39470 : path +'/'; |
|
39471 |
|
39472 routes[redirectPath] = angular.extend( |
|
39473 {redirectTo: path}, |
|
39474 pathRegExp(redirectPath, route) |
|
39475 ); |
|
39476 } |
|
39477 |
|
39478 return this; |
|
39479 }; |
|
39480 |
|
39481 /** |
|
39482 * @param path {string} path |
|
39483 * @param opts {Object} options |
|
39484 * @return {?Object} |
|
39485 * |
|
39486 * @description |
|
39487 * Normalizes the given path, returning a regular expression |
|
39488 * and the original path. |
|
39489 * |
|
39490 * Inspired by pathRexp in visionmedia/express/lib/utils.js. |
|
39491 */ |
|
39492 function pathRegExp(path, opts) { |
|
39493 var insensitive = opts.caseInsensitiveMatch, |
|
39494 ret = { |
|
39495 originalPath: path, |
|
39496 regexp: path |
|
39497 }, |
|
39498 keys = ret.keys = []; |
|
39499 |
|
39500 path = path |
|
39501 .replace(/([().])/g, '\\$1') |
|
39502 .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option){ |
|
39503 var optional = option === '?' ? option : null; |
|
39504 var star = option === '*' ? option : null; |
|
39505 keys.push({ name: key, optional: !!optional }); |
|
39506 slash = slash || ''; |
|
39507 return '' |
|
39508 + (optional ? '' : slash) |
|
39509 + '(?:' |
|
39510 + (optional ? slash : '') |
|
39511 + (star && '(.+?)' || '([^/]+)') |
|
39512 + (optional || '') |
|
39513 + ')' |
|
39514 + (optional || ''); |
|
39515 }) |
|
39516 .replace(/([\/$\*])/g, '\\$1'); |
|
39517 |
|
39518 ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); |
|
39519 return ret; |
|
39520 } |
|
39521 |
|
39522 /** |
|
39523 * @ngdoc method |
|
39524 * @name $routeProvider#otherwise |
|
39525 * |
|
39526 * @description |
|
39527 * Sets route definition that will be used on route change when no other route definition |
|
39528 * is matched. |
|
39529 * |
|
39530 * @param {Object|string} params Mapping information to be assigned to `$route.current`. |
|
39531 * If called with a string, the value maps to `redirectTo`. |
|
39532 * @returns {Object} self |
|
39533 */ |
|
39534 this.otherwise = function(params) { |
|
39535 if (typeof params === 'string') { |
|
39536 params = {redirectTo: params}; |
|
39537 } |
|
39538 this.when(null, params); |
|
39539 return this; |
|
39540 }; |
|
39541 |
|
39542 |
|
39543 this.$get = ['$rootScope', |
|
39544 '$location', |
|
39545 '$routeParams', |
|
39546 '$q', |
|
39547 '$injector', |
|
39548 '$templateRequest', |
|
39549 '$sce', |
|
39550 function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { |
|
39551 |
|
39552 /** |
|
39553 * @ngdoc service |
|
39554 * @name $route |
|
39555 * @requires $location |
|
39556 * @requires $routeParams |
|
39557 * |
|
39558 * @property {Object} current Reference to the current route definition. |
|
39559 * The route definition contains: |
|
39560 * |
|
39561 * - `controller`: The controller constructor as define in route definition. |
|
39562 * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for |
|
39563 * controller instantiation. The `locals` contain |
|
39564 * the resolved values of the `resolve` map. Additionally the `locals` also contain: |
|
39565 * |
|
39566 * - `$scope` - The current route scope. |
|
39567 * - `$template` - The current route template HTML. |
|
39568 * |
|
39569 * @property {Object} routes Object with all route configuration Objects as its properties. |
|
39570 * |
|
39571 * @description |
|
39572 * `$route` is used for deep-linking URLs to controllers and views (HTML partials). |
|
39573 * It watches `$location.url()` and tries to map the path to an existing route definition. |
|
39574 * |
|
39575 * Requires the {@link ngRoute `ngRoute`} module to be installed. |
|
39576 * |
|
39577 * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. |
|
39578 * |
|
39579 * The `$route` service is typically used in conjunction with the |
|
39580 * {@link ngRoute.directive:ngView `ngView`} directive and the |
|
39581 * {@link ngRoute.$routeParams `$routeParams`} service. |
|
39582 * |
|
39583 * @example |
|
39584 * This example shows how changing the URL hash causes the `$route` to match a route against the |
|
39585 * URL, and the `ngView` pulls in the partial. |
|
39586 * |
|
39587 * <example name="$route-service" module="ngRouteExample" |
|
39588 * deps="angular-route.js" fixBase="true"> |
|
39589 * <file name="index.html"> |
|
39590 * <div ng-controller="MainController"> |
|
39591 * Choose: |
|
39592 * <a href="Book/Moby">Moby</a> | |
|
39593 * <a href="Book/Moby/ch/1">Moby: Ch1</a> | |
|
39594 * <a href="Book/Gatsby">Gatsby</a> | |
|
39595 * <a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> | |
|
39596 * <a href="Book/Scarlet">Scarlet Letter</a><br/> |
|
39597 * |
|
39598 * <div ng-view></div> |
|
39599 * |
|
39600 * <hr /> |
|
39601 * |
|
39602 * <pre>$location.path() = {{$location.path()}}</pre> |
|
39603 * <pre>$route.current.templateUrl = {{$route.current.templateUrl}}</pre> |
|
39604 * <pre>$route.current.params = {{$route.current.params}}</pre> |
|
39605 * <pre>$route.current.scope.name = {{$route.current.scope.name}}</pre> |
|
39606 * <pre>$routeParams = {{$routeParams}}</pre> |
|
39607 * </div> |
|
39608 * </file> |
|
39609 * |
|
39610 * <file name="book.html"> |
|
39611 * controller: {{name}}<br /> |
|
39612 * Book Id: {{params.bookId}}<br /> |
|
39613 * </file> |
|
39614 * |
|
39615 * <file name="chapter.html"> |
|
39616 * controller: {{name}}<br /> |
|
39617 * Book Id: {{params.bookId}}<br /> |
|
39618 * Chapter Id: {{params.chapterId}} |
|
39619 * </file> |
|
39620 * |
|
39621 * <file name="script.js"> |
|
39622 * angular.module('ngRouteExample', ['ngRoute']) |
|
39623 * |
|
39624 * .controller('MainController', function($scope, $route, $routeParams, $location) { |
|
39625 * $scope.$route = $route; |
|
39626 * $scope.$location = $location; |
|
39627 * $scope.$routeParams = $routeParams; |
|
39628 * }) |
|
39629 * |
|
39630 * .controller('BookController', function($scope, $routeParams) { |
|
39631 * $scope.name = "BookController"; |
|
39632 * $scope.params = $routeParams; |
|
39633 * }) |
|
39634 * |
|
39635 * .controller('ChapterController', function($scope, $routeParams) { |
|
39636 * $scope.name = "ChapterController"; |
|
39637 * $scope.params = $routeParams; |
|
39638 * }) |
|
39639 * |
|
39640 * .config(function($routeProvider, $locationProvider) { |
|
39641 * $routeProvider |
|
39642 * .when('/Book/:bookId', { |
|
39643 * templateUrl: 'book.html', |
|
39644 * controller: 'BookController', |
|
39645 * resolve: { |
|
39646 * // I will cause a 1 second delay |
|
39647 * delay: function($q, $timeout) { |
|
39648 * var delay = $q.defer(); |
|
39649 * $timeout(delay.resolve, 1000); |
|
39650 * return delay.promise; |
|
39651 * } |
|
39652 * } |
|
39653 * }) |
|
39654 * .when('/Book/:bookId/ch/:chapterId', { |
|
39655 * templateUrl: 'chapter.html', |
|
39656 * controller: 'ChapterController' |
|
39657 * }); |
|
39658 * |
|
39659 * // configure html5 to get links working on jsfiddle |
|
39660 * $locationProvider.html5Mode(true); |
|
39661 * }); |
|
39662 * |
|
39663 * </file> |
|
39664 * |
|
39665 * <file name="protractor.js" type="protractor"> |
|
39666 * it('should load and compile correct template', function() { |
|
39667 * element(by.linkText('Moby: Ch1')).click(); |
|
39668 * var content = element(by.css('[ng-view]')).getText(); |
|
39669 * expect(content).toMatch(/controller\: ChapterController/); |
|
39670 * expect(content).toMatch(/Book Id\: Moby/); |
|
39671 * expect(content).toMatch(/Chapter Id\: 1/); |
|
39672 * |
|
39673 * element(by.partialLinkText('Scarlet')).click(); |
|
39674 * |
|
39675 * content = element(by.css('[ng-view]')).getText(); |
|
39676 * expect(content).toMatch(/controller\: BookController/); |
|
39677 * expect(content).toMatch(/Book Id\: Scarlet/); |
|
39678 * }); |
|
39679 * </file> |
|
39680 * </example> |
|
39681 */ |
|
39682 |
|
39683 /** |
|
39684 * @ngdoc event |
|
39685 * @name $route#$routeChangeStart |
|
39686 * @eventType broadcast on root scope |
|
39687 * @description |
|
39688 * Broadcasted before a route change. At this point the route services starts |
|
39689 * resolving all of the dependencies needed for the route change to occur. |
|
39690 * Typically this involves fetching the view template as well as any dependencies |
|
39691 * defined in `resolve` route property. Once all of the dependencies are resolved |
|
39692 * `$routeChangeSuccess` is fired. |
|
39693 * |
|
39694 * The route change (and the `$location` change that triggered it) can be prevented |
|
39695 * by calling `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} |
|
39696 * for more details about event object. |
|
39697 * |
|
39698 * @param {Object} angularEvent Synthetic event object. |
|
39699 * @param {Route} next Future route information. |
|
39700 * @param {Route} current Current route information. |
|
39701 */ |
|
39702 |
|
39703 /** |
|
39704 * @ngdoc event |
|
39705 * @name $route#$routeChangeSuccess |
|
39706 * @eventType broadcast on root scope |
|
39707 * @description |
|
39708 * Broadcasted after a route dependencies are resolved. |
|
39709 * {@link ngRoute.directive:ngView ngView} listens for the directive |
|
39710 * to instantiate the controller and render the view. |
|
39711 * |
|
39712 * @param {Object} angularEvent Synthetic event object. |
|
39713 * @param {Route} current Current route information. |
|
39714 * @param {Route|Undefined} previous Previous route information, or undefined if current is |
|
39715 * first route entered. |
|
39716 */ |
|
39717 |
|
39718 /** |
|
39719 * @ngdoc event |
|
39720 * @name $route#$routeChangeError |
|
39721 * @eventType broadcast on root scope |
|
39722 * @description |
|
39723 * Broadcasted if any of the resolve promises are rejected. |
|
39724 * |
|
39725 * @param {Object} angularEvent Synthetic event object |
|
39726 * @param {Route} current Current route information. |
|
39727 * @param {Route} previous Previous route information. |
|
39728 * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. |
|
39729 */ |
|
39730 |
|
39731 /** |
|
39732 * @ngdoc event |
|
39733 * @name $route#$routeUpdate |
|
39734 * @eventType broadcast on root scope |
|
39735 * @description |
|
39736 * |
|
39737 * The `reloadOnSearch` property has been set to false, and we are reusing the same |
|
39738 * instance of the Controller. |
|
39739 */ |
|
39740 |
|
39741 var forceReload = false, |
|
39742 preparedRoute, |
|
39743 preparedRouteIsUpdateOnly, |
|
39744 $route = { |
|
39745 routes: routes, |
|
39746 |
|
39747 /** |
|
39748 * @ngdoc method |
|
39749 * @name $route#reload |
|
39750 * |
|
39751 * @description |
|
39752 * Causes `$route` service to reload the current route even if |
|
39753 * {@link ng.$location $location} hasn't changed. |
|
39754 * |
|
39755 * As a result of that, {@link ngRoute.directive:ngView ngView} |
|
39756 * creates new scope, reinstantiates the controller. |
|
39757 */ |
|
39758 reload: function() { |
|
39759 forceReload = true; |
|
39760 $rootScope.$evalAsync(function() { |
|
39761 // Don't support cancellation of a reload for now... |
|
39762 prepareRoute(); |
|
39763 commitRoute(); |
|
39764 }); |
|
39765 }, |
|
39766 |
|
39767 /** |
|
39768 * @ngdoc method |
|
39769 * @name $route#updateParams |
|
39770 * |
|
39771 * @description |
|
39772 * Causes `$route` service to update the current URL, replacing |
|
39773 * current route parameters with those specified in `newParams`. |
|
39774 * Provided property names that match the route's path segment |
|
39775 * definitions will be interpolated into the location's path, while |
|
39776 * remaining properties will be treated as query params. |
|
39777 * |
|
39778 * @param {Object} newParams mapping of URL parameter names to values |
|
39779 */ |
|
39780 updateParams: function(newParams) { |
|
39781 if (this.current && this.current.$$route) { |
|
39782 var searchParams = {}, self=this; |
|
39783 |
|
39784 angular.forEach(Object.keys(newParams), function(key) { |
|
39785 if (!self.current.pathParams[key]) searchParams[key] = newParams[key]; |
|
39786 }); |
|
39787 |
|
39788 newParams = angular.extend({}, this.current.params, newParams); |
|
39789 $location.path(interpolate(this.current.$$route.originalPath, newParams)); |
|
39790 $location.search(angular.extend({}, $location.search(), searchParams)); |
|
39791 } |
|
39792 else { |
|
39793 throw $routeMinErr('norout', 'Tried updating route when with no current route'); |
|
39794 } |
|
39795 } |
|
39796 }; |
|
39797 |
|
39798 $rootScope.$on('$locationChangeStart', prepareRoute); |
|
39799 $rootScope.$on('$locationChangeSuccess', commitRoute); |
|
39800 |
|
39801 return $route; |
|
39802 |
|
39803 ///////////////////////////////////////////////////// |
|
39804 |
|
39805 /** |
|
39806 * @param on {string} current url |
|
39807 * @param route {Object} route regexp to match the url against |
|
39808 * @return {?Object} |
|
39809 * |
|
39810 * @description |
|
39811 * Check if the route matches the current url. |
|
39812 * |
|
39813 * Inspired by match in |
|
39814 * visionmedia/express/lib/router/router.js. |
|
39815 */ |
|
39816 function switchRouteMatcher(on, route) { |
|
39817 var keys = route.keys, |
|
39818 params = {}; |
|
39819 |
|
39820 if (!route.regexp) return null; |
|
39821 |
|
39822 var m = route.regexp.exec(on); |
|
39823 if (!m) return null; |
|
39824 |
|
39825 for (var i = 1, len = m.length; i < len; ++i) { |
|
39826 var key = keys[i - 1]; |
|
39827 |
|
39828 var val = m[i]; |
|
39829 |
|
39830 if (key && val) { |
|
39831 params[key.name] = val; |
|
39832 } |
|
39833 } |
|
39834 return params; |
|
39835 } |
|
39836 |
|
39837 function prepareRoute($locationEvent) { |
|
39838 var lastRoute = $route.current; |
|
39839 |
|
39840 preparedRoute = parseRoute(); |
|
39841 preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route |
|
39842 && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) |
|
39843 && !preparedRoute.reloadOnSearch && !forceReload; |
|
39844 |
|
39845 if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) { |
|
39846 if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) { |
|
39847 if ($locationEvent) { |
|
39848 $locationEvent.preventDefault(); |
|
39849 } |
|
39850 } |
|
39851 } |
|
39852 } |
|
39853 |
|
39854 function commitRoute() { |
|
39855 var lastRoute = $route.current; |
|
39856 var nextRoute = preparedRoute; |
|
39857 |
|
39858 if (preparedRouteIsUpdateOnly) { |
|
39859 lastRoute.params = nextRoute.params; |
|
39860 angular.copy(lastRoute.params, $routeParams); |
|
39861 $rootScope.$broadcast('$routeUpdate', lastRoute); |
|
39862 } else if (nextRoute || lastRoute) { |
|
39863 forceReload = false; |
|
39864 $route.current = nextRoute; |
|
39865 if (nextRoute) { |
|
39866 if (nextRoute.redirectTo) { |
|
39867 if (angular.isString(nextRoute.redirectTo)) { |
|
39868 $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params) |
|
39869 .replace(); |
|
39870 } else { |
|
39871 $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search())) |
|
39872 .replace(); |
|
39873 } |
|
39874 } |
|
39875 } |
|
39876 |
|
39877 $q.when(nextRoute). |
|
39878 then(function() { |
|
39879 if (nextRoute) { |
|
39880 var locals = angular.extend({}, nextRoute.resolve), |
|
39881 template, templateUrl; |
|
39882 |
|
39883 angular.forEach(locals, function(value, key) { |
|
39884 locals[key] = angular.isString(value) ? |
|
39885 $injector.get(value) : $injector.invoke(value, null, null, key); |
|
39886 }); |
|
39887 |
|
39888 if (angular.isDefined(template = nextRoute.template)) { |
|
39889 if (angular.isFunction(template)) { |
|
39890 template = template(nextRoute.params); |
|
39891 } |
|
39892 } else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) { |
|
39893 if (angular.isFunction(templateUrl)) { |
|
39894 templateUrl = templateUrl(nextRoute.params); |
|
39895 } |
|
39896 templateUrl = $sce.getTrustedResourceUrl(templateUrl); |
|
39897 if (angular.isDefined(templateUrl)) { |
|
39898 nextRoute.loadedTemplateUrl = templateUrl; |
|
39899 template = $templateRequest(templateUrl); |
|
39900 } |
|
39901 } |
|
39902 if (angular.isDefined(template)) { |
|
39903 locals['$template'] = template; |
|
39904 } |
|
39905 return $q.all(locals); |
|
39906 } |
|
39907 }). |
|
39908 // after route change |
|
39909 then(function(locals) { |
|
39910 if (nextRoute == $route.current) { |
|
39911 if (nextRoute) { |
|
39912 nextRoute.locals = locals; |
|
39913 angular.copy(nextRoute.params, $routeParams); |
|
39914 } |
|
39915 $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); |
|
39916 } |
|
39917 }, function(error) { |
|
39918 if (nextRoute == $route.current) { |
|
39919 $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); |
|
39920 } |
|
39921 }); |
|
39922 } |
|
39923 } |
|
39924 |
|
39925 |
|
39926 /** |
|
39927 * @returns {Object} the current active route, by matching it against the URL |
|
39928 */ |
|
39929 function parseRoute() { |
|
39930 // Match a route |
|
39931 var params, match; |
|
39932 angular.forEach(routes, function(route, path) { |
|
39933 if (!match && (params = switchRouteMatcher($location.path(), route))) { |
|
39934 match = inherit(route, { |
|
39935 params: angular.extend({}, $location.search(), params), |
|
39936 pathParams: params}); |
|
39937 match.$$route = route; |
|
39938 } |
|
39939 }); |
|
39940 // No route matched; fallback to "otherwise" route |
|
39941 return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); |
|
39942 } |
|
39943 |
|
39944 /** |
|
39945 * @returns {string} interpolation of the redirect path with the parameters |
|
39946 */ |
|
39947 function interpolate(string, params) { |
|
39948 var result = []; |
|
39949 angular.forEach((string||'').split(':'), function(segment, i) { |
|
39950 if (i === 0) { |
|
39951 result.push(segment); |
|
39952 } else { |
|
39953 var segmentMatch = segment.match(/(\w+)(.*)/); |
|
39954 var key = segmentMatch[1]; |
|
39955 result.push(params[key]); |
|
39956 result.push(segmentMatch[2] || ''); |
|
39957 delete params[key]; |
|
39958 } |
|
39959 }); |
|
39960 return result.join(''); |
|
39961 } |
|
39962 }]; |
|
39963 } |
|
39964 |
|
39965 ngRouteModule.provider('$routeParams', $RouteParamsProvider); |
|
39966 |
|
39967 |
|
39968 /** |
|
39969 * @ngdoc service |
|
39970 * @name $routeParams |
|
39971 * @requires $route |
|
39972 * |
|
39973 * @description |
|
39974 * The `$routeParams` service allows you to retrieve the current set of route parameters. |
|
39975 * |
|
39976 * Requires the {@link ngRoute `ngRoute`} module to be installed. |
|
39977 * |
|
39978 * The route parameters are a combination of {@link ng.$location `$location`}'s |
|
39979 * {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}. |
|
39980 * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched. |
|
39981 * |
|
39982 * In case of parameter name collision, `path` params take precedence over `search` params. |
|
39983 * |
|
39984 * The service guarantees that the identity of the `$routeParams` object will remain unchanged |
|
39985 * (but its properties will likely change) even when a route change occurs. |
|
39986 * |
|
39987 * Note that the `$routeParams` are only updated *after* a route change completes successfully. |
|
39988 * This means that you cannot rely on `$routeParams` being correct in route resolve functions. |
|
39989 * Instead you can use `$route.current.params` to access the new route's parameters. |
|
39990 * |
|
39991 * @example |
|
39992 * ```js |
|
39993 * // Given: |
|
39994 * // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby |
|
39995 * // Route: /Chapter/:chapterId/Section/:sectionId |
|
39996 * // |
|
39997 * // Then |
|
39998 * $routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'} |
|
39999 * ``` |
|
40000 */ |
|
40001 function $RouteParamsProvider() { |
|
40002 this.$get = function() { return {}; }; |
|
40003 } |
|
40004 |
|
40005 ngRouteModule.directive('ngView', ngViewFactory); |
|
40006 ngRouteModule.directive('ngView', ngViewFillContentFactory); |
|
40007 |
|
40008 |
|
40009 /** |
|
40010 * @ngdoc directive |
|
40011 * @name ngView |
|
40012 * @restrict ECA |
|
40013 * |
|
40014 * @description |
|
40015 * # Overview |
|
40016 * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by |
|
40017 * including the rendered template of the current route into the main layout (`index.html`) file. |
|
40018 * Every time the current route changes, the included view changes with it according to the |
|
40019 * configuration of the `$route` service. |
|
40020 * |
|
40021 * Requires the {@link ngRoute `ngRoute`} module to be installed. |
|
40022 * |
|
40023 * @animations |
|
40024 * enter - animation is used to bring new content into the browser. |
|
40025 * leave - animation is used to animate existing content away. |
|
40026 * |
|
40027 * The enter and leave animation occur concurrently. |
|
40028 * |
|
40029 * @scope |
|
40030 * @priority 400 |
|
40031 * @param {string=} onload Expression to evaluate whenever the view updates. |
|
40032 * |
|
40033 * @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll |
|
40034 * $anchorScroll} to scroll the viewport after the view is updated. |
|
40035 * |
|
40036 * - If the attribute is not set, disable scrolling. |
|
40037 * - If the attribute is set without value, enable scrolling. |
|
40038 * - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated |
|
40039 * as an expression yields a truthy value. |
|
40040 * @example |
|
40041 <example name="ngView-directive" module="ngViewExample" |
|
40042 deps="angular-route.js;angular-animate.js" |
|
40043 animations="true" fixBase="true"> |
|
40044 <file name="index.html"> |
|
40045 <div ng-controller="MainCtrl as main"> |
|
40046 Choose: |
|
40047 <a href="Book/Moby">Moby</a> | |
|
40048 <a href="Book/Moby/ch/1">Moby: Ch1</a> | |
|
40049 <a href="Book/Gatsby">Gatsby</a> | |
|
40050 <a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> | |
|
40051 <a href="Book/Scarlet">Scarlet Letter</a><br/> |
|
40052 |
|
40053 <div class="view-animate-container"> |
|
40054 <div ng-view class="view-animate"></div> |
|
40055 </div> |
|
40056 <hr /> |
|
40057 |
|
40058 <pre>$location.path() = {{main.$location.path()}}</pre> |
|
40059 <pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre> |
|
40060 <pre>$route.current.params = {{main.$route.current.params}}</pre> |
|
40061 <pre>$routeParams = {{main.$routeParams}}</pre> |
|
40062 </div> |
|
40063 </file> |
|
40064 |
|
40065 <file name="book.html"> |
|
40066 <div> |
|
40067 controller: {{book.name}}<br /> |
|
40068 Book Id: {{book.params.bookId}}<br /> |
|
40069 </div> |
|
40070 </file> |
|
40071 |
|
40072 <file name="chapter.html"> |
|
40073 <div> |
|
40074 controller: {{chapter.name}}<br /> |
|
40075 Book Id: {{chapter.params.bookId}}<br /> |
|
40076 Chapter Id: {{chapter.params.chapterId}} |
|
40077 </div> |
|
40078 </file> |
|
40079 |
|
40080 <file name="animations.css"> |
|
40081 .view-animate-container { |
|
40082 position:relative; |
|
40083 height:100px!important; |
|
40084 position:relative; |
|
40085 background:white; |
|
40086 border:1px solid black; |
|
40087 height:40px; |
|
40088 overflow:hidden; |
|
40089 } |
|
40090 |
|
40091 .view-animate { |
|
40092 padding:10px; |
|
40093 } |
|
40094 |
|
40095 .view-animate.ng-enter, .view-animate.ng-leave { |
|
40096 -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; |
|
40097 transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; |
|
40098 |
|
40099 display:block; |
|
40100 width:100%; |
|
40101 border-left:1px solid black; |
|
40102 |
|
40103 position:absolute; |
|
40104 top:0; |
|
40105 left:0; |
|
40106 right:0; |
|
40107 bottom:0; |
|
40108 padding:10px; |
|
40109 } |
|
40110 |
|
40111 .view-animate.ng-enter { |
|
40112 left:100%; |
|
40113 } |
|
40114 .view-animate.ng-enter.ng-enter-active { |
|
40115 left:0; |
|
40116 } |
|
40117 .view-animate.ng-leave.ng-leave-active { |
|
40118 left:-100%; |
|
40119 } |
|
40120 </file> |
|
40121 |
|
40122 <file name="script.js"> |
|
40123 angular.module('ngViewExample', ['ngRoute', 'ngAnimate']) |
|
40124 .config(['$routeProvider', '$locationProvider', |
|
40125 function($routeProvider, $locationProvider) { |
|
40126 $routeProvider |
|
40127 .when('/Book/:bookId', { |
|
40128 templateUrl: 'book.html', |
|
40129 controller: 'BookCtrl', |
|
40130 controllerAs: 'book' |
|
40131 }) |
|
40132 .when('/Book/:bookId/ch/:chapterId', { |
|
40133 templateUrl: 'chapter.html', |
|
40134 controller: 'ChapterCtrl', |
|
40135 controllerAs: 'chapter' |
|
40136 }); |
|
40137 |
|
40138 $locationProvider.html5Mode(true); |
|
40139 }]) |
|
40140 .controller('MainCtrl', ['$route', '$routeParams', '$location', |
|
40141 function($route, $routeParams, $location) { |
|
40142 this.$route = $route; |
|
40143 this.$location = $location; |
|
40144 this.$routeParams = $routeParams; |
|
40145 }]) |
|
40146 .controller('BookCtrl', ['$routeParams', function($routeParams) { |
|
40147 this.name = "BookCtrl"; |
|
40148 this.params = $routeParams; |
|
40149 }]) |
|
40150 .controller('ChapterCtrl', ['$routeParams', function($routeParams) { |
|
40151 this.name = "ChapterCtrl"; |
|
40152 this.params = $routeParams; |
|
40153 }]); |
|
40154 |
|
40155 </file> |
|
40156 |
|
40157 <file name="protractor.js" type="protractor"> |
|
40158 it('should load and compile correct template', function() { |
|
40159 element(by.linkText('Moby: Ch1')).click(); |
|
40160 var content = element(by.css('[ng-view]')).getText(); |
|
40161 expect(content).toMatch(/controller\: ChapterCtrl/); |
|
40162 expect(content).toMatch(/Book Id\: Moby/); |
|
40163 expect(content).toMatch(/Chapter Id\: 1/); |
|
40164 |
|
40165 element(by.partialLinkText('Scarlet')).click(); |
|
40166 |
|
40167 content = element(by.css('[ng-view]')).getText(); |
|
40168 expect(content).toMatch(/controller\: BookCtrl/); |
|
40169 expect(content).toMatch(/Book Id\: Scarlet/); |
|
40170 }); |
|
40171 </file> |
|
40172 </example> |
|
40173 */ |
|
40174 |
|
40175 |
|
40176 /** |
|
40177 * @ngdoc event |
|
40178 * @name ngView#$viewContentLoaded |
|
40179 * @eventType emit on the current ngView scope |
|
40180 * @description |
|
40181 * Emitted every time the ngView content is reloaded. |
|
40182 */ |
|
40183 ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; |
|
40184 function ngViewFactory( $route, $anchorScroll, $animate) { |
|
40185 return { |
|
40186 restrict: 'ECA', |
|
40187 terminal: true, |
|
40188 priority: 400, |
|
40189 transclude: 'element', |
|
40190 link: function(scope, $element, attr, ctrl, $transclude) { |
|
40191 var currentScope, |
|
40192 currentElement, |
|
40193 previousLeaveAnimation, |
|
40194 autoScrollExp = attr.autoscroll, |
|
40195 onloadExp = attr.onload || ''; |
|
40196 |
|
40197 scope.$on('$routeChangeSuccess', update); |
|
40198 update(); |
|
40199 |
|
40200 function cleanupLastView() { |
|
40201 if(previousLeaveAnimation) { |
|
40202 $animate.cancel(previousLeaveAnimation); |
|
40203 previousLeaveAnimation = null; |
|
40204 } |
|
40205 |
|
40206 if(currentScope) { |
|
40207 currentScope.$destroy(); |
|
40208 currentScope = null; |
|
40209 } |
|
40210 if(currentElement) { |
|
40211 previousLeaveAnimation = $animate.leave(currentElement); |
|
40212 previousLeaveAnimation.then(function() { |
|
40213 previousLeaveAnimation = null; |
|
40214 }); |
|
40215 currentElement = null; |
|
40216 } |
|
40217 } |
|
40218 |
|
40219 function update() { |
|
40220 var locals = $route.current && $route.current.locals, |
|
40221 template = locals && locals.$template; |
|
40222 |
|
40223 if (angular.isDefined(template)) { |
|
40224 var newScope = scope.$new(); |
|
40225 var current = $route.current; |
|
40226 |
|
40227 // Note: This will also link all children of ng-view that were contained in the original |
|
40228 // html. If that content contains controllers, ... they could pollute/change the scope. |
|
40229 // However, using ng-view on an element with additional content does not make sense... |
|
40230 // Note: We can't remove them in the cloneAttchFn of $transclude as that |
|
40231 // function is called before linking the content, which would apply child |
|
40232 // directives to non existing elements. |
|
40233 var clone = $transclude(newScope, function(clone) { |
|
40234 $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter () { |
|
40235 if (angular.isDefined(autoScrollExp) |
|
40236 && (!autoScrollExp || scope.$eval(autoScrollExp))) { |
|
40237 $anchorScroll(); |
|
40238 } |
|
40239 }); |
|
40240 cleanupLastView(); |
|
40241 }); |
|
40242 |
|
40243 currentElement = clone; |
|
40244 currentScope = current.scope = newScope; |
|
40245 currentScope.$emit('$viewContentLoaded'); |
|
40246 currentScope.$eval(onloadExp); |
|
40247 } else { |
|
40248 cleanupLastView(); |
|
40249 } |
|
40250 } |
|
40251 } |
|
40252 }; |
|
40253 } |
|
40254 |
|
40255 // This directive is called during the $transclude call of the first `ngView` directive. |
|
40256 // It will replace and compile the content of the element with the loaded template. |
|
40257 // We need this directive so that the element content is already filled when |
|
40258 // the link function of another directive on the same element as ngView |
|
40259 // is called. |
|
40260 ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route']; |
|
40261 function ngViewFillContentFactory($compile, $controller, $route) { |
|
40262 return { |
|
40263 restrict: 'ECA', |
|
40264 priority: -400, |
|
40265 link: function(scope, $element) { |
|
40266 var current = $route.current, |
|
40267 locals = current.locals; |
|
40268 |
|
40269 $element.html(locals.$template); |
|
40270 |
|
40271 var link = $compile($element.contents()); |
|
40272 |
|
40273 if (current.controller) { |
|
40274 locals.$scope = scope; |
|
40275 var controller = $controller(current.controller, locals); |
|
40276 if (current.controllerAs) { |
|
40277 scope[current.controllerAs] = controller; |
|
40278 } |
|
40279 $element.data('$ngControllerController', controller); |
|
40280 $element.children().data('$ngControllerController', controller); |
|
40281 } |
|
40282 |
|
40283 link(scope); |
|
40284 } |
|
40285 }; |
|
40286 } |
|
40287 |
|
40288 |
|
40289 })(window, window.angular); |
|
40290 |
|
40291 var app = angular.module('app', ['autocomplete']); |
|
40292 |
|
40293 // the service that retrieves some movie title from an url |
|
40294 app.factory('MovieRetriever', function($http, $q, $timeout){ |
|
40295 var MovieRetriever = new Object(); |
|
40296 |
|
40297 MovieRetriever.getmovies = function(i) { |
|
40298 var moviedata = $q.defer(); |
|
40299 var movies; |
|
40300 |
|
40301 var someMovies = ["The Wolverine", "The Smurfs 2", "The Mortal Instruments: City of Bones", "Drinking Buddies", "All the Boys Love Mandy Lane", "The Act Of Killing", "Red 2", "Jobs", "Getaway", "Red Obsession", "2 Guns", "The World's End", "Planes", "Paranoia", "The To Do List", "Man of Steel"]; |
|
40302 |
|
40303 var moreMovies = ["The Wolverine", "The Smurfs 2", "The Mortal Instruments: City of Bones", "Drinking Buddies", "All the Boys Love Mandy Lane", "The Act Of Killing", "Red 2", "Jobs", "Getaway", "Red Obsession", "2 Guns", "The World's End", "Planes", "Paranoia", "The To Do List", "Man of Steel", "The Way Way Back", "Before Midnight", "Only God Forgives", "I Give It a Year", "The Heat", "Pacific Rim", "Pacific Rim", "Kevin Hart: Let Me Explain", "A Hijacking", "Maniac", "After Earth", "The Purge", "Much Ado About Nothing", "Europa Report", "Stuck in Love", "We Steal Secrets: The Story Of Wikileaks", "The Croods", "This Is the End", "The Frozen Ground", "Turbo", "Blackfish", "Frances Ha", "Prince Avalanche", "The Attack", "Grown Ups 2", "White House Down", "Lovelace", "Girl Most Likely", "Parkland", "Passion", "Monsters University", "R.I.P.D.", "Byzantium", "The Conjuring", "The Internship"] |
|
40304 |
|
40305 if(i && i.indexOf('T')!=-1) |
|
40306 movies=moreMovies; |
|
40307 else |
|
40308 movies=moreMovies; |
|
40309 |
|
40310 $timeout(function(){ |
|
40311 moviedata.resolve(movies); |
|
40312 },1000); |
|
40313 |
|
40314 return moviedata.promise |
|
40315 } |
|
40316 |
|
40317 return MovieRetriever; |
|
40318 }); |
|
40319 |
|
40320 app.controller('MyCtrl', function($scope, MovieRetriever){ |
|
40321 |
|
40322 $scope.movies = MovieRetriever.getmovies("..."); |
|
40323 $scope.movies.then(function(data){ |
|
40324 $scope.movies = data; |
|
40325 }); |
|
40326 |
|
40327 $scope.getmovies = function(){ |
|
40328 return $scope.movies; |
|
40329 } |
|
40330 |
|
40331 $scope.doSomething = function(typedthings){ |
|
40332 console.log("Do something like reload data with this: " + typedthings ); |
|
40333 $scope.newmovies = MovieRetriever.getmovies(typedthings); |
|
40334 $scope.newmovies.then(function(data){ |
|
40335 $scope.movies = data; |
|
40336 }); |
|
40337 } |
|
40338 |
|
40339 $scope.doSomethingElse = function(suggestion){ |
|
40340 console.log("Suggestion selected: " + suggestion ); |
|
40341 } |
|
40342 |
|
40343 }); |
|
40344 |
|
40345 /* --- Made by justgoscha and licensed under MIT license --- */ |
|
40346 |
|
40347 var app = angular.module('autocomplete', []); |
|
40348 |
|
40349 app.directive('autocomplete', function() { |
|
40350 var index = -1; |
|
40351 |
|
40352 return { |
|
40353 restrict: 'E', |
|
40354 scope: { |
|
40355 searchParam: '=ngModel', |
|
40356 suggestions: '=data', |
|
40357 onType: '=onType', |
|
40358 onSelect: '=onSelect' |
|
40359 }, |
|
40360 controller: ['$scope', function($scope){ |
|
40361 // the index of the suggestions that's currently selected |
|
40362 $scope.selectedIndex = -1; |
|
40363 |
|
40364 // set new index |
|
40365 $scope.setIndex = function(i){ |
|
40366 $scope.selectedIndex = parseInt(i); |
|
40367 }; |
|
40368 |
|
40369 this.setIndex = function(i){ |
|
40370 $scope.setIndex(i); |
|
40371 $scope.$apply(); |
|
40372 }; |
|
40373 |
|
40374 $scope.getIndex = function(i){ |
|
40375 return $scope.selectedIndex; |
|
40376 }; |
|
40377 |
|
40378 // watches if the parameter filter should be changed |
|
40379 var watching = true; |
|
40380 |
|
40381 // autocompleting drop down on/off |
|
40382 $scope.completing = false; |
|
40383 |
|
40384 // starts autocompleting on typing in something |
|
40385 $scope.$watch('searchParam', function(newValue, oldValue){ |
|
40386 |
|
40387 if (oldValue === newValue || !oldValue) { |
|
40388 return; |
|
40389 } |
|
40390 |
|
40391 if(watching && typeof $scope.searchParam !== 'undefined' && $scope.searchParam !== null) { |
|
40392 $scope.completing = true; |
|
40393 $scope.searchFilter = $scope.searchParam; |
|
40394 $scope.selectedIndex = -1; |
|
40395 } |
|
40396 |
|
40397 // function thats passed to on-type attribute gets executed |
|
40398 if($scope.onType) |
|
40399 $scope.onType($scope.searchParam); |
|
40400 }); |
|
40401 |
|
40402 // for hovering over suggestions |
|
40403 this.preSelect = function(suggestion){ |
|
40404 |
|
40405 watching = false; |
|
40406 |
|
40407 // this line determines if it is shown |
|
40408 // in the input field before it's selected: |
|
40409 //$scope.searchParam = suggestion; |
|
40410 |
|
40411 $scope.$apply(); |
|
40412 watching = true; |
|
40413 |
|
40414 }; |
|
40415 |
|
40416 $scope.preSelect = this.preSelect; |
|
40417 |
|
40418 this.preSelectOff = function(){ |
|
40419 watching = true; |
|
40420 }; |
|
40421 |
|
40422 $scope.preSelectOff = this.preSelectOff; |
|
40423 |
|
40424 // selecting a suggestion with RIGHT ARROW or ENTER |
|
40425 $scope.select = function(suggestion){ |
|
40426 if(suggestion){ |
|
40427 $scope.searchParam = suggestion; |
|
40428 $scope.searchFilter = suggestion; |
|
40429 if($scope.onSelect) |
|
40430 $scope.onSelect(suggestion); |
|
40431 } |
|
40432 watching = false; |
|
40433 $scope.completing = false; |
|
40434 setTimeout(function(){watching = true;},1000); |
|
40435 $scope.setIndex(-1); |
|
40436 }; |
|
40437 |
|
40438 |
|
40439 }], |
|
40440 link: function(scope, element, attrs){ |
|
40441 |
|
40442 var attr = ''; |
|
40443 |
|
40444 // Default atts |
|
40445 scope.attrs = { |
|
40446 "placeholder": "start typing...", |
|
40447 "class": "", |
|
40448 "id": "", |
|
40449 "inputclass": "", |
|
40450 "inputid": "" |
|
40451 }; |
|
40452 |
|
40453 for (var a in attrs) { |
|
40454 attr = a.replace('attr', '').toLowerCase(); |
|
40455 // add attribute overriding defaults |
|
40456 // and preventing duplication |
|
40457 if (a.indexOf('attr') === 0) { |
|
40458 scope.attrs[attr] = attrs[a]; |
|
40459 } |
|
40460 } |
|
40461 |
|
40462 if (attrs.clickActivation) { |
|
40463 element[0].onclick = function(e){ |
|
40464 if(!scope.searchParam){ |
|
40465 scope.completing = true; |
|
40466 scope.$apply(); |
|
40467 } |
|
40468 }; |
|
40469 } |
|
40470 |
|
40471 var key = {left: 37, up: 38, right: 39, down: 40 , enter: 13, esc: 27, tab: 9}; |
|
40472 |
|
40473 document.addEventListener("keydown", function(e){ |
|
40474 var keycode = e.keyCode || e.which; |
|
40475 |
|
40476 switch (keycode){ |
|
40477 case key.esc: |
|
40478 // disable suggestions on escape |
|
40479 scope.select(); |
|
40480 scope.setIndex(-1); |
|
40481 scope.$apply(); |
|
40482 e.preventDefault(); |
|
40483 } |
|
40484 }, true); |
|
40485 |
|
40486 document.addEventListener("blur", function(e){ |
|
40487 // disable suggestions on blur |
|
40488 // we do a timeout to prevent hiding it before a click event is registered |
|
40489 setTimeout(function() { |
|
40490 scope.select(); |
|
40491 scope.setIndex(-1); |
|
40492 scope.$apply(); |
|
40493 }, 200); |
|
40494 }, true); |
|
40495 |
|
40496 element[0].addEventListener("keydown",function (e){ |
|
40497 var keycode = e.keyCode || e.which; |
|
40498 |
|
40499 var l = angular.element(this).find('li').length; |
|
40500 |
|
40501 // implementation of the up and down movement in the list of suggestions |
|
40502 switch (keycode){ |
|
40503 case key.up: |
|
40504 |
|
40505 index = scope.getIndex()-1; |
|
40506 if(index<-1){ |
|
40507 index = l-1; |
|
40508 } else if (index >= l ){ |
|
40509 index = -1; |
|
40510 scope.setIndex(index); |
|
40511 scope.preSelectOff(); |
|
40512 break; |
|
40513 } |
|
40514 scope.setIndex(index); |
|
40515 |
|
40516 if(index!==-1) |
|
40517 scope.preSelect(angular.element(angular.element(this).find('li')[index]).text()); |
|
40518 |
|
40519 scope.$apply(); |
|
40520 |
|
40521 break; |
|
40522 case key.down: |
|
40523 index = scope.getIndex()+1; |
|
40524 if(index<-1){ |
|
40525 index = l-1; |
|
40526 } else if (index >= l ){ |
|
40527 index = -1; |
|
40528 scope.setIndex(index); |
|
40529 scope.preSelectOff(); |
|
40530 scope.$apply(); |
|
40531 break; |
|
40532 } |
|
40533 scope.setIndex(index); |
|
40534 |
|
40535 if(index!==-1) |
|
40536 scope.preSelect(angular.element(angular.element(this).find('li')[index]).text()); |
|
40537 |
|
40538 break; |
|
40539 case key.left: |
|
40540 break; |
|
40541 case key.right: |
|
40542 case key.enter: |
|
40543 case key.tab: |
|
40544 |
|
40545 index = scope.getIndex(); |
|
40546 // scope.preSelectOff(); |
|
40547 if(index !== -1) { |
|
40548 scope.select(angular.element(angular.element(this).find('li')[index]).text()); |
|
40549 if(keycode == key.enter) { |
|
40550 e.preventDefault(); |
|
40551 } |
|
40552 } else { |
|
40553 if(keycode == key.enter) { |
|
40554 scope.select(); |
|
40555 } |
|
40556 } |
|
40557 scope.setIndex(-1); |
|
40558 scope.$apply(); |
|
40559 |
|
40560 break; |
|
40561 case key.esc: |
|
40562 // disable suggestions on escape |
|
40563 scope.select(); |
|
40564 scope.setIndex(-1); |
|
40565 scope.$apply(); |
|
40566 e.preventDefault(); |
|
40567 break; |
|
40568 default: |
|
40569 return; |
|
40570 } |
|
40571 |
|
40572 }); |
|
40573 }, |
|
40574 template: '\ |
|
40575 <div class="autocomplete {{ attrs.class }}" id="{{ attrs.id }}">\ |
|
40576 <input\ |
|
40577 type="text"\ |
|
40578 ng-model="searchParam"\ |
|
40579 placeholder="{{ attrs.placeholder }}"\ |
|
40580 class="{{ attrs.inputclass }}"\ |
|
40581 id="{{ attrs.inputid }}"/>\ |
|
40582 <ul ng-show="completing && suggestions.length>0">\ |
|
40583 <li\ |
|
40584 suggestion\ |
|
40585 ng-repeat="suggestion in suggestions | filter:searchFilter | orderBy:\'toString()\' track by $index"\ |
|
40586 index="{{ $index }}"\ |
|
40587 val="{{ suggestion }}"\ |
|
40588 ng-class="{ active: ($index === selectedIndex) }"\ |
|
40589 ng-click="select(suggestion)"\ |
|
40590 ng-bind-html="suggestion | highlight:searchParam"></li>\ |
|
40591 </ul>\ |
|
40592 </div>' |
|
40593 }; |
|
40594 }); |
|
40595 |
|
40596 app.filter('highlight', ['$sce', function ($sce) { |
|
40597 return function (input, searchParam) { |
|
40598 if (typeof input === 'function') return ''; |
|
40599 if (searchParam) { |
|
40600 var words = '(' + |
|
40601 searchParam.split(/\ /).join(' |') + '|' + |
|
40602 searchParam.split(/\ /).join('|') + |
|
40603 ')', |
|
40604 exp = new RegExp(words, 'gi'); |
|
40605 if (words.length) { |
|
40606 input = input.replace(exp, "<span class=\"highlight\">$1</span>"); |
|
40607 } |
|
40608 } |
|
40609 return $sce.trustAsHtml(input); |
|
40610 }; |
|
40611 }]); |
|
40612 |
|
40613 app.directive('suggestion', function(){ |
|
40614 return { |
|
40615 restrict: 'A', |
|
40616 require: '^autocomplete', // ^look for controller on parents element |
|
40617 link: function(scope, element, attrs, autoCtrl){ |
|
40618 element.bind('mouseenter', function() { |
|
40619 autoCtrl.preSelect(attrs.val); |
|
40620 autoCtrl.setIndex(attrs.index); |
|
40621 }); |
|
40622 |
|
40623 element.bind('mouseleave', function() { |
|
40624 autoCtrl.preSelectOff(); |
|
40625 }); |
|
40626 } |
|
40627 }; |
|
40628 }); |
|
40629 |
|
40630 /* |
|
40631 string.js - Copyright (C) 2012-2014, JP Richardson <jprichardson@gmail.com> |
|
40632 */ |
|
40633 |
|
40634 !(function() { |
|
40635 "use strict"; |
|
40636 |
|
40637 var VERSION = '2.1.0'; |
|
40638 |
|
40639 var ENTITIES = {}; |
|
40640 |
|
40641 // from http://semplicewebsites.com/removing-accents-javascript |
|
40642 var latin_map={"Á":"A","Ă":"A","Ắ":"A","Ặ":"A","Ằ":"A","Ẳ":"A","Ẵ":"A","Ǎ":"A","Â":"A","Ấ":"A","Ậ":"A","Ầ":"A","Ẩ":"A","Ẫ":"A","Ä":"A","Ǟ":"A","Ȧ":"A","Ǡ":"A","Ạ":"A","Ȁ":"A","À":"A","Ả":"A","Ȃ":"A","Ā":"A","Ą":"A","Å":"A","Ǻ":"A","Ḁ":"A","Ⱥ":"A","Ã":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ḃ":"B","Ḅ":"B","Ɓ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ć":"C","Č":"C","Ç":"C","Ḉ":"C","Ĉ":"C","Ċ":"C","Ƈ":"C","Ȼ":"C","Ď":"D","Ḑ":"D","Ḓ":"D","Ḋ":"D","Ḍ":"D","Ɗ":"D","Ḏ":"D","Dz":"D","Dž":"D","Đ":"D","Ƌ":"D","DZ":"DZ","DŽ":"DZ","É":"E","Ĕ":"E","Ě":"E","Ȩ":"E","Ḝ":"E","Ê":"E","Ế":"E","Ệ":"E","Ề":"E","Ể":"E","Ễ":"E","Ḙ":"E","Ë":"E","Ė":"E","Ẹ":"E","Ȅ":"E","È":"E","Ẻ":"E","Ȇ":"E","Ē":"E","Ḗ":"E","Ḕ":"E","Ę":"E","Ɇ":"E","Ẽ":"E","Ḛ":"E","Ꝫ":"ET","Ḟ":"F","Ƒ":"F","Ǵ":"G","Ğ":"G","Ǧ":"G","Ģ":"G","Ĝ":"G","Ġ":"G","Ɠ":"G","Ḡ":"G","Ǥ":"G","Ḫ":"H","Ȟ":"H","Ḩ":"H","Ĥ":"H","Ⱨ":"H","Ḧ":"H","Ḣ":"H","Ḥ":"H","Ħ":"H","Í":"I","Ĭ":"I","Ǐ":"I","Î":"I","Ï":"I","Ḯ":"I","İ":"I","Ị":"I","Ȉ":"I","Ì":"I","Ỉ":"I","Ȋ":"I","Ī":"I","Į":"I","Ɨ":"I","Ĩ":"I","Ḭ":"I","Ꝺ":"D","Ꝼ":"F","Ᵹ":"G","Ꞃ":"R","Ꞅ":"S","Ꞇ":"T","Ꝭ":"IS","Ĵ":"J","Ɉ":"J","Ḱ":"K","Ǩ":"K","Ķ":"K","Ⱪ":"K","Ꝃ":"K","Ḳ":"K","Ƙ":"K","Ḵ":"K","Ꝁ":"K","Ꝅ":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ļ":"L","Ḽ":"L","Ḷ":"L","Ḹ":"L","Ⱡ":"L","Ꝉ":"L","Ḻ":"L","Ŀ":"L","Ɫ":"L","Lj":"L","Ł":"L","LJ":"LJ","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ń":"N","Ň":"N","Ņ":"N","Ṋ":"N","Ṅ":"N","Ṇ":"N","Ǹ":"N","Ɲ":"N","Ṉ":"N","Ƞ":"N","Nj":"N","Ñ":"N","NJ":"NJ","Ó":"O","Ŏ":"O","Ǒ":"O","Ô":"O","Ố":"O","Ộ":"O","Ồ":"O","Ổ":"O","Ỗ":"O","Ö":"O","Ȫ":"O","Ȯ":"O","Ȱ":"O","Ọ":"O","Ő":"O","Ȍ":"O","Ò":"O","Ỏ":"O","Ơ":"O","Ớ":"O","Ợ":"O","Ờ":"O","Ở":"O","Ỡ":"O","Ȏ":"O","Ꝋ":"O","Ꝍ":"O","Ō":"O","Ṓ":"O","Ṑ":"O","Ɵ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","Ṍ":"O","Ṏ":"O","Ȭ":"O","Ƣ":"OI","Ꝏ":"OO","Ɛ":"E","Ɔ":"O","Ȣ":"OU","Ṕ":"P","Ṗ":"P","Ꝓ":"P","Ƥ":"P","Ꝕ":"P","Ᵽ":"P","Ꝑ":"P","Ꝙ":"Q","Ꝗ":"Q","Ŕ":"R","Ř":"R","Ŗ":"R","Ṙ":"R","Ṛ":"R","Ṝ":"R","Ȑ":"R","Ȓ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꜿ":"C","Ǝ":"E","Ś":"S","Ṥ":"S","Š":"S","Ṧ":"S","Ş":"S","Ŝ":"S","Ș":"S","Ṡ":"S","Ṣ":"S","Ṩ":"S","ẞ":"SS","Ť":"T","Ţ":"T","Ṱ":"T","Ț":"T","Ⱦ":"T","Ṫ":"T","Ṭ":"T","Ƭ":"T","Ṯ":"T","Ʈ":"T","Ŧ":"T","Ɐ":"A","Ꞁ":"L","Ɯ":"M","Ʌ":"V","Ꜩ":"TZ","Ú":"U","Ŭ":"U","Ǔ":"U","Û":"U","Ṷ":"U","Ü":"U","Ǘ":"U","Ǚ":"U","Ǜ":"U","Ǖ":"U","Ṳ":"U","Ụ":"U","Ű":"U","Ȕ":"U","Ù":"U","Ủ":"U","Ư":"U","Ứ":"U","Ự":"U","Ừ":"U","Ử":"U","Ữ":"U","Ȗ":"U","Ū":"U","Ṻ":"U","Ų":"U","Ů":"U","Ũ":"U","Ṹ":"U","Ṵ":"U","Ꝟ":"V","Ṿ":"V","Ʋ":"V","Ṽ":"V","Ꝡ":"VY","Ẃ":"W","Ŵ":"W","Ẅ":"W","Ẇ":"W","Ẉ":"W","Ẁ":"W","Ⱳ":"W","Ẍ":"X","Ẋ":"X","Ý":"Y","Ŷ":"Y","Ÿ":"Y","Ẏ":"Y","Ỵ":"Y","Ỳ":"Y","Ƴ":"Y","Ỷ":"Y","Ỿ":"Y","Ȳ":"Y","Ɏ":"Y","Ỹ":"Y","Ź":"Z","Ž":"Z","Ẑ":"Z","Ⱬ":"Z","Ż":"Z","Ẓ":"Z","Ȥ":"Z","Ẕ":"Z","Ƶ":"Z","IJ":"IJ","Œ":"OE","ᴀ":"A","ᴁ":"AE","ʙ":"B","ᴃ":"B","ᴄ":"C","ᴅ":"D","ᴇ":"E","ꜰ":"F","ɢ":"G","ʛ":"G","ʜ":"H","ɪ":"I","ʁ":"R","ᴊ":"J","ᴋ":"K","ʟ":"L","ᴌ":"L","ᴍ":"M","ɴ":"N","ᴏ":"O","ɶ":"OE","ᴐ":"O","ᴕ":"OU","ᴘ":"P","ʀ":"R","ᴎ":"N","ᴙ":"R","ꜱ":"S","ᴛ":"T","ⱻ":"E","ᴚ":"R","ᴜ":"U","ᴠ":"V","ᴡ":"W","ʏ":"Y","ᴢ":"Z","á":"a","ă":"a","ắ":"a","ặ":"a","ằ":"a","ẳ":"a","ẵ":"a","ǎ":"a","â":"a","ấ":"a","ậ":"a","ầ":"a","ẩ":"a","ẫ":"a","ä":"a","ǟ":"a","ȧ":"a","ǡ":"a","ạ":"a","ȁ":"a","à":"a","ả":"a","ȃ":"a","ā":"a","ą":"a","ᶏ":"a","ẚ":"a","å":"a","ǻ":"a","ḁ":"a","ⱥ":"a","ã":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ḃ":"b","ḅ":"b","ɓ":"b","ḇ":"b","ᵬ":"b","ᶀ":"b","ƀ":"b","ƃ":"b","ɵ":"o","ć":"c","č":"c","ç":"c","ḉ":"c","ĉ":"c","ɕ":"c","ċ":"c","ƈ":"c","ȼ":"c","ď":"d","ḑ":"d","ḓ":"d","ȡ":"d","ḋ":"d","ḍ":"d","ɗ":"d","ᶑ":"d","ḏ":"d","ᵭ":"d","ᶁ":"d","đ":"d","ɖ":"d","ƌ":"d","ı":"i","ȷ":"j","ɟ":"j","ʄ":"j","dz":"dz","dž":"dz","é":"e","ĕ":"e","ě":"e","ȩ":"e","ḝ":"e","ê":"e","ế":"e","ệ":"e","ề":"e","ể":"e","ễ":"e","ḙ":"e","ë":"e","ė":"e","ẹ":"e","ȅ":"e","è":"e","ẻ":"e","ȇ":"e","ē":"e","ḗ":"e","ḕ":"e","ⱸ":"e","ę":"e","ᶒ":"e","ɇ":"e","ẽ":"e","ḛ":"e","ꝫ":"et","ḟ":"f","ƒ":"f","ᵮ":"f","ᶂ":"f","ǵ":"g","ğ":"g","ǧ":"g","ģ":"g","ĝ":"g","ġ":"g","ɠ":"g","ḡ":"g","ᶃ":"g","ǥ":"g","ḫ":"h","ȟ":"h","ḩ":"h","ĥ":"h","ⱨ":"h","ḧ":"h","ḣ":"h","ḥ":"h","ɦ":"h","ẖ":"h","ħ":"h","ƕ":"hv","í":"i","ĭ":"i","ǐ":"i","î":"i","ï":"i","ḯ":"i","ị":"i","ȉ":"i","ì":"i","ỉ":"i","ȋ":"i","ī":"i","į":"i","ᶖ":"i","ɨ":"i","ĩ":"i","ḭ":"i","ꝺ":"d","ꝼ":"f","ᵹ":"g","ꞃ":"r","ꞅ":"s","ꞇ":"t","ꝭ":"is","ǰ":"j","ĵ":"j","ʝ":"j","ɉ":"j","ḱ":"k","ǩ":"k","ķ":"k","ⱪ":"k","ꝃ":"k","ḳ":"k","ƙ":"k","ḵ":"k","ᶄ":"k","ꝁ":"k","ꝅ":"k","ĺ":"l","ƚ":"l","ɬ":"l","ľ":"l","ļ":"l","ḽ":"l","ȴ":"l","ḷ":"l","ḹ":"l","ⱡ":"l","ꝉ":"l","ḻ":"l","ŀ":"l","ɫ":"l","ᶅ":"l","ɭ":"l","ł":"l","lj":"lj","ſ":"s","ẜ":"s","ẛ":"s","ẝ":"s","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ᵯ":"m","ᶆ":"m","ń":"n","ň":"n","ņ":"n","ṋ":"n","ȵ":"n","ṅ":"n","ṇ":"n","ǹ":"n","ɲ":"n","ṉ":"n","ƞ":"n","ᵰ":"n","ᶇ":"n","ɳ":"n","ñ":"n","nj":"nj","ó":"o","ŏ":"o","ǒ":"o","ô":"o","ố":"o","ộ":"o","ồ":"o","ổ":"o","ỗ":"o","ö":"o","ȫ":"o","ȯ":"o","ȱ":"o","ọ":"o","ő":"o","ȍ":"o","ò":"o","ỏ":"o","ơ":"o","ớ":"o","ợ":"o","ờ":"o","ở":"o","ỡ":"o","ȏ":"o","ꝋ":"o","ꝍ":"o","ⱺ":"o","ō":"o","ṓ":"o","ṑ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","õ":"o","ṍ":"o","ṏ":"o","ȭ":"o","ƣ":"oi","ꝏ":"oo","ɛ":"e","ᶓ":"e","ɔ":"o","ᶗ":"o","ȣ":"ou","ṕ":"p","ṗ":"p","ꝓ":"p","ƥ":"p","ᵱ":"p","ᶈ":"p","ꝕ":"p","ᵽ":"p","ꝑ":"p","ꝙ":"q","ʠ":"q","ɋ":"q","ꝗ":"q","ŕ":"r","ř":"r","ŗ":"r","ṙ":"r","ṛ":"r","ṝ":"r","ȑ":"r","ɾ":"r","ᵳ":"r","ȓ":"r","ṟ":"r","ɼ":"r","ᵲ":"r","ᶉ":"r","ɍ":"r","ɽ":"r","ↄ":"c","ꜿ":"c","ɘ":"e","ɿ":"r","ś":"s","ṥ":"s","š":"s","ṧ":"s","ş":"s","ŝ":"s","ș":"s","ṡ":"s","ṣ":"s","ṩ":"s","ʂ":"s","ᵴ":"s","ᶊ":"s","ȿ":"s","ɡ":"g","ß":"ss","ᴑ":"o","ᴓ":"o","ᴝ":"u","ť":"t","ţ":"t","ṱ":"t","ț":"t","ȶ":"t","ẗ":"t","ⱦ":"t","ṫ":"t","ṭ":"t","ƭ":"t","ṯ":"t","ᵵ":"t","ƫ":"t","ʈ":"t","ŧ":"t","ᵺ":"th","ɐ":"a","ᴂ":"ae","ǝ":"e","ᵷ":"g","ɥ":"h","ʮ":"h","ʯ":"h","ᴉ":"i","ʞ":"k","ꞁ":"l","ɯ":"m","ɰ":"m","ᴔ":"oe","ɹ":"r","ɻ":"r","ɺ":"r","ⱹ":"r","ʇ":"t","ʌ":"v","ʍ":"w","ʎ":"y","ꜩ":"tz","ú":"u","ŭ":"u","ǔ":"u","û":"u","ṷ":"u","ü":"u","ǘ":"u","ǚ":"u","ǜ":"u","ǖ":"u","ṳ":"u","ụ":"u","ű":"u","ȕ":"u","ù":"u","ủ":"u","ư":"u","ứ":"u","ự":"u","ừ":"u","ử":"u","ữ":"u","ȗ":"u","ū":"u","ṻ":"u","ų":"u","ᶙ":"u","ů":"u","ũ":"u","ṹ":"u","ṵ":"u","ᵫ":"ue","ꝸ":"um","ⱴ":"v","ꝟ":"v","ṿ":"v","ʋ":"v","ᶌ":"v","ⱱ":"v","ṽ":"v","ꝡ":"vy","ẃ":"w","ŵ":"w","ẅ":"w","ẇ":"w","ẉ":"w","ẁ":"w","ⱳ":"w","ẘ":"w","ẍ":"x","ẋ":"x","ᶍ":"x","ý":"y","ŷ":"y","ÿ":"y","ẏ":"y","ỵ":"y","ỳ":"y","ƴ":"y","ỷ":"y","ỿ":"y","ȳ":"y","ẙ":"y","ɏ":"y","ỹ":"y","ź":"z","ž":"z","ẑ":"z","ʑ":"z","ⱬ":"z","ż":"z","ẓ":"z","ȥ":"z","ẕ":"z","ᵶ":"z","ᶎ":"z","ʐ":"z","ƶ":"z","ɀ":"z","ff":"ff","ffi":"ffi","ffl":"ffl","fi":"fi","fl":"fl","ij":"ij","œ":"oe","st":"st","ₐ":"a","ₑ":"e","ᵢ":"i","ⱼ":"j","ₒ":"o","ᵣ":"r","ᵤ":"u","ᵥ":"v","ₓ":"x"}; |
|
40643 |
|
40644 //****************************************************************************** |
|
40645 // Added an initialize function which is essentially the code from the S |
|
40646 // constructor. Now, the S constructor calls this and a new method named |
|
40647 // setValue calls it as well. The setValue function allows constructors for |
|
40648 // modules that extend string.js to set the initial value of an object without |
|
40649 // knowing the internal workings of string.js. |
|
40650 // |
|
40651 // Also, all methods which return a new S object now call: |
|
40652 // |
|
40653 // return new this.constructor(s); |
|
40654 // |
|
40655 // instead of: |
|
40656 // |
|
40657 // return new S(s); |
|
40658 // |
|
40659 // This allows extended objects to keep their proper instanceOf and constructor. |
|
40660 //****************************************************************************** |
|
40661 |
|
40662 function initialize (object, s) { |
|
40663 if (s !== null && s !== undefined) { |
|
40664 if (typeof s === 'string') |
|
40665 object.s = s; |
|
40666 else |
|
40667 object.s = s.toString(); |
|
40668 } else { |
|
40669 object.s = s; //null or undefined |
|
40670 } |
|
40671 |
|
40672 object.orig = s; //original object, currently only used by toCSV() and toBoolean() |
|
40673 |
|
40674 if (s !== null && s !== undefined) { |
|
40675 if (object.__defineGetter__) { |
|
40676 object.__defineGetter__('length', function() { |
|
40677 return object.s.length; |
|
40678 }) |
|
40679 } else { |
|
40680 object.length = s.length; |
|
40681 } |
|
40682 } else { |
|
40683 object.length = -1; |
|
40684 } |
|
40685 } |
|
40686 |
|
40687 function S(s) { |
|
40688 initialize(this, s); |
|
40689 } |
|
40690 |
|
40691 var __nsp = String.prototype; |
|
40692 var __sp = S.prototype = { |
|
40693 |
|
40694 between: function(left, right) { |
|
40695 var s = this.s; |
|
40696 var startPos = s.indexOf(left); |
|
40697 var endPos = s.indexOf(right, startPos + left.length); |
|
40698 if (endPos == -1 && right != null) |
|
40699 return new this.constructor('') |
|
40700 else if (endPos == -1 && right == null) |
|
40701 return new this.constructor(s.substring(startPos + left.length)) |
|
40702 else |
|
40703 return new this.constructor(s.slice(startPos + left.length, endPos)); |
|
40704 }, |
|
40705 |
|
40706 //# modified slightly from https://github.com/epeli/underscore.string |
|
40707 camelize: function() { |
|
40708 var s = this.trim().s.replace(/(\-|_|\s)+(.)?/g, function(mathc, sep, c) { |
|
40709 return (c ? c.toUpperCase() : ''); |
|
40710 }); |
|
40711 return new this.constructor(s); |
|
40712 }, |
|
40713 |
|
40714 capitalize: function() { |
|
40715 return new this.constructor(this.s.substr(0, 1).toUpperCase() + this.s.substring(1).toLowerCase()); |
|
40716 }, |
|
40717 |
|
40718 charAt: function(index) { |
|
40719 return this.s.charAt(index); |
|
40720 }, |
|
40721 |
|
40722 chompLeft: function(prefix) { |
|
40723 var s = this.s; |
|
40724 if (s.indexOf(prefix) === 0) { |
|
40725 s = s.slice(prefix.length); |
|
40726 return new this.constructor(s); |
|
40727 } else { |
|
40728 return this; |
|
40729 } |
|
40730 }, |
|
40731 |
|
40732 chompRight: function(suffix) { |
|
40733 if (this.endsWith(suffix)) { |
|
40734 var s = this.s; |
|
40735 s = s.slice(0, s.length - suffix.length); |
|
40736 return new this.constructor(s); |
|
40737 } else { |
|
40738 return this; |
|
40739 } |
|
40740 }, |
|
40741 |
|
40742 //#thanks Google |
|
40743 collapseWhitespace: function() { |
|
40744 var s = this.s.replace(/[\s\xa0]+/g, ' ').replace(/^\s+|\s+$/g, ''); |
|
40745 return new this.constructor(s); |
|
40746 }, |
|
40747 |
|
40748 contains: function(ss) { |
|
40749 return this.s.indexOf(ss) >= 0; |
|
40750 }, |
|
40751 |
|
40752 count: function(ss) { |
|
40753 var count = 0 |
|
40754 , pos = this.s.indexOf(ss) |
|
40755 |
|
40756 while (pos >= 0) { |
|
40757 count += 1 |
|
40758 pos = this.s.indexOf(ss, pos + 1) |
|
40759 } |
|
40760 |
|
40761 return count |
|
40762 }, |
|
40763 |
|
40764 //#modified from https://github.com/epeli/underscore.string |
|
40765 dasherize: function() { |
|
40766 var s = this.trim().s.replace(/[_\s]+/g, '-').replace(/([A-Z])/g, '-$1').replace(/-+/g, '-').toLowerCase(); |
|
40767 return new this.constructor(s); |
|
40768 }, |
|
40769 |
|
40770 latinise: function() { |
|
40771 var s = this.replace(/[^A-Za-z0-9\[\] ]/g, function(x) { return latin_map[x] || x; }); |
|
40772 return new this.constructor(s); |
|
40773 }, |
|
40774 |
|
40775 decodeHtmlEntities: function() { //https://github.com/substack/node-ent/blob/master/index.js |
|
40776 var s = this.s; |
|
40777 s = s.replace(/&#(\d+);?/g, function (_, code) { |
|
40778 return String.fromCharCode(code); |
|
40779 }) |
|
40780 .replace(/&#[xX]([A-Fa-f0-9]+);?/g, function (_, hex) { |
|
40781 return String.fromCharCode(parseInt(hex, 16)); |
|
40782 }) |
|
40783 .replace(/&([^;\W]+;?)/g, function (m, e) { |
|
40784 var ee = e.replace(/;$/, ''); |
|
40785 var target = ENTITIES[e] || (e.match(/;$/) && ENTITIES[ee]); |
|
40786 |
|
40787 if (typeof target === 'number') { |
|
40788 return String.fromCharCode(target); |
|
40789 } |
|
40790 else if (typeof target === 'string') { |
|
40791 return target; |
|
40792 } |
|
40793 else { |
|
40794 return m; |
|
40795 } |
|
40796 }) |
|
40797 |
|
40798 return new this.constructor(s); |
|
40799 }, |
|
40800 |
|
40801 endsWith: function(suffix) { |
|
40802 var l = this.s.length - suffix.length; |
|
40803 return l >= 0 && this.s.indexOf(suffix, l) === l; |
|
40804 }, |
|
40805 |
|
40806 escapeHTML: function() { //from underscore.string |
|
40807 return new this.constructor(this.s.replace(/[&<>"']/g, function(m){ return '&' + reversedEscapeChars[m] + ';'; })); |
|
40808 }, |
|
40809 |
|
40810 ensureLeft: function(prefix) { |
|
40811 var s = this.s; |
|
40812 if (s.indexOf(prefix) === 0) { |
|
40813 return this; |
|
40814 } else { |
|
40815 return new this.constructor(prefix + s); |
|
40816 } |
|
40817 }, |
|
40818 |
|
40819 ensureRight: function(suffix) { |
|
40820 var s = this.s; |
|
40821 if (this.endsWith(suffix)) { |
|
40822 return this; |
|
40823 } else { |
|
40824 return new this.constructor(s + suffix); |
|
40825 } |
|
40826 }, |
|
40827 |
|
40828 humanize: function() { //modified from underscore.string |
|
40829 if (this.s === null || this.s === undefined) |
|
40830 return new this.constructor('') |
|
40831 var s = this.underscore().replace(/_id$/,'').replace(/_/g, ' ').trim().capitalize() |
|
40832 return new this.constructor(s) |
|
40833 }, |
|
40834 |
|
40835 isAlpha: function() { |
|
40836 return !/[^a-z\xDF-\xFF]|^$/.test(this.s.toLowerCase()); |
|
40837 }, |
|
40838 |
|
40839 isAlphaNumeric: function() { |
|
40840 return !/[^0-9a-z\xDF-\xFF]/.test(this.s.toLowerCase()); |
|
40841 }, |
|
40842 |
|
40843 isEmpty: function() { |
|
40844 return this.s === null || this.s === undefined ? true : /^[\s\xa0]*$/.test(this.s); |
|
40845 }, |
|
40846 |
|
40847 isLower: function() { |
|
40848 return this.isAlpha() && this.s.toLowerCase() === this.s; |
|
40849 }, |
|
40850 |
|
40851 isNumeric: function() { |
|
40852 return !/[^0-9]/.test(this.s); |
|
40853 }, |
|
40854 |
|
40855 isUpper: function() { |
|
40856 return this.isAlpha() && this.s.toUpperCase() === this.s; |
|
40857 }, |
|
40858 |
|
40859 left: function(N) { |
|
40860 if (N >= 0) { |
|
40861 var s = this.s.substr(0, N); |
|
40862 return new this.constructor(s); |
|
40863 } else { |
|
40864 return this.right(-N); |
|
40865 } |
|
40866 }, |
|
40867 |
|
40868 lines: function() { //convert windows newlines to unix newlines then convert to an Array of lines |
|
40869 return this.replaceAll('\r\n', '\n').s.split('\n'); |
|
40870 }, |
|
40871 |
|
40872 pad: function(len, ch) { //https://github.com/component/pad |
|
40873 if (ch == null) ch = ' '; |
|
40874 if (this.s.length >= len) return new this.constructor(this.s); |
|
40875 len = len - this.s.length; |
|
40876 var left = Array(Math.ceil(len / 2) + 1).join(ch); |
|
40877 var right = Array(Math.floor(len / 2) + 1).join(ch); |
|
40878 return new this.constructor(left + this.s + right); |
|
40879 }, |
|
40880 |
|
40881 padLeft: function(len, ch) { //https://github.com/component/pad |
|
40882 if (ch == null) ch = ' '; |
|
40883 if (this.s.length >= len) return new this.constructor(this.s); |
|
40884 return new this.constructor(Array(len - this.s.length + 1).join(ch) + this.s); |
|
40885 }, |
|
40886 |
|
40887 padRight: function(len, ch) { //https://github.com/component/pad |
|
40888 if (ch == null) ch = ' '; |
|
40889 if (this.s.length >= len) return new this.constructor(this.s); |
|
40890 return new this.constructor(this.s + Array(len - this.s.length + 1).join(ch)); |
|
40891 }, |
|
40892 |
|
40893 parseCSV: function(delimiter, qualifier, escape, lineDelimiter) { //try to parse no matter what |
|
40894 delimiter = delimiter || ','; |
|
40895 escape = escape || '\\' |
|
40896 if (typeof qualifier == 'undefined') |
|
40897 qualifier = '"'; |
|
40898 |
|
40899 var i = 0, fieldBuffer = [], fields = [], len = this.s.length, inField = false, inUnqualifiedString = false, self = this; |
|
40900 var ca = function(i){return self.s.charAt(i)}; |
|
40901 if (typeof lineDelimiter !== 'undefined') var rows = []; |
|
40902 |
|
40903 if (!qualifier) |
|
40904 inField = true; |
|
40905 |
|
40906 while (i < len) { |
|
40907 var current = ca(i); |
|
40908 switch (current) { |
|
40909 case escape: |
|
40910 //fix for issues #32 and #35 |
|
40911 if (inField && ((escape !== qualifier) || ca(i+1) === qualifier)) { |
|
40912 i += 1; |
|
40913 fieldBuffer.push(ca(i)); |
|
40914 break; |
|
40915 } |
|
40916 if (escape !== qualifier) break; |
|
40917 case qualifier: |
|
40918 inField = !inField; |
|
40919 break; |
|
40920 case delimiter: |
|
40921 if(inUnqualifiedString) { |
|
40922 inField=false; |
|
40923 inUnqualifiedString=false; |
|
40924 } |
|
40925 if (inField && qualifier) |
|
40926 fieldBuffer.push(current); |
|
40927 else { |
|
40928 fields.push(fieldBuffer.join('')) |
|
40929 fieldBuffer.length = 0; |
|
40930 } |
|
40931 break; |
|
40932 case lineDelimiter: |
|
40933 if(inUnqualifiedString) { |
|
40934 inField=false; |
|
40935 inUnqualifiedString=false; |
|
40936 fields.push(fieldBuffer.join('')) |
|
40937 rows.push(fields); |
|
40938 fields = []; |
|
40939 fieldBuffer.length = 0; |
|
40940 } |
|
40941 else if (inField) { |
|
40942 fieldBuffer.push(current); |
|
40943 } else { |
|
40944 if (rows) { |
|
40945 fields.push(fieldBuffer.join('')) |
|
40946 rows.push(fields); |
|
40947 fields = []; |
|
40948 fieldBuffer.length = 0; |
|
40949 } |
|
40950 } |
|
40951 break; |
|
40952 case ' ': |
|
40953 if (inField) |
|
40954 fieldBuffer.push(current); |
|
40955 break; |
|
40956 default: |
|
40957 if (inField) |
|
40958 fieldBuffer.push(current); |
|
40959 else if(current!==qualifier) { |
|
40960 fieldBuffer.push(current); |
|
40961 inField=true; |
|
40962 inUnqualifiedString=true; |
|
40963 } |
|
40964 break; |
|
40965 } |
|
40966 i += 1; |
|
40967 } |
|
40968 |
|
40969 fields.push(fieldBuffer.join('')); |
|
40970 if (rows) { |
|
40971 rows.push(fields); |
|
40972 return rows; |
|
40973 } |
|
40974 return fields; |
|
40975 }, |
|
40976 |
|
40977 replaceAll: function(ss, r) { |
|
40978 //var s = this.s.replace(new RegExp(ss, 'g'), r); |
|
40979 var s = this.s.split(ss).join(r) |
|
40980 return new this.constructor(s); |
|
40981 }, |
|
40982 |
|
40983 strip: function() { |
|
40984 var ss = this.s; |
|
40985 for(var i= 0, n=arguments.length; i<n; i++) { |
|
40986 ss = ss.split(arguments[i]).join(''); |
|
40987 } |
|
40988 return new this.constructor(ss); |
|
40989 }, |
|
40990 |
|
40991 right: function(N) { |
|
40992 if (N >= 0) { |
|
40993 var s = this.s.substr(this.s.length - N, N); |
|
40994 return new this.constructor(s); |
|
40995 } else { |
|
40996 return this.left(-N); |
|
40997 } |
|
40998 }, |
|
40999 |
|
41000 setValue: function (s) { |
|
41001 initialize(this, s); |
|
41002 return this; |
|
41003 }, |
|
41004 |
|
41005 slugify: function() { |
|
41006 var sl = (new S(new S(this.s).latinise().s.replace(/[^\w\s-]/g, '').toLowerCase())).dasherize().s; |
|
41007 if (sl.charAt(0) === '-') |
|
41008 sl = sl.substr(1); |
|
41009 return new this.constructor(sl); |
|
41010 }, |
|
41011 |
|
41012 startsWith: function(prefix) { |
|
41013 return this.s.lastIndexOf(prefix, 0) === 0; |
|
41014 }, |
|
41015 |
|
41016 stripPunctuation: function() { |
|
41017 //return new this.constructor(this.s.replace(/[\.,-\/#!$%\^&\*;:{}=\-_`~()]/g,"")); |
|
41018 return new this.constructor(this.s.replace(/[^\w\s]|_/g, "").replace(/\s+/g, " ")); |
|
41019 }, |
|
41020 |
|
41021 stripTags: function() { //from sugar.js |
|
41022 var s = this.s, args = arguments.length > 0 ? arguments : ['']; |
|
41023 multiArgs(args, function(tag) { |
|
41024 s = s.replace(RegExp('<\/?' + tag + '[^<>]*>', 'gi'), ''); |
|
41025 }); |
|
41026 return new this.constructor(s); |
|
41027 }, |
|
41028 |
|
41029 template: function(values, opening, closing) { |
|
41030 var s = this.s |
|
41031 var opening = opening || Export.TMPL_OPEN |
|
41032 var closing = closing || Export.TMPL_CLOSE |
|
41033 |
|
41034 var open = opening.replace(/[-[\]()*\s]/g, "\\$&").replace(/\$/g, '\\$') |
|
41035 var close = closing.replace(/[-[\]()*\s]/g, "\\$&").replace(/\$/g, '\\$') |
|
41036 var r = new RegExp(open + '(.+?)' + close, 'g') |
|
41037 //, r = /\{\{(.+?)\}\}/g |
|
41038 var matches = s.match(r) || []; |
|
41039 |
|
41040 matches.forEach(function(match) { |
|
41041 var key = match.substring(opening.length, match.length - closing.length);//chop {{ and }} |
|
41042 if (typeof values[key] != 'undefined') |
|
41043 s = s.replace(match, values[key]); |
|
41044 }); |
|
41045 return new this.constructor(s); |
|
41046 }, |
|
41047 |
|
41048 times: function(n) { |
|
41049 return new this.constructor(new Array(n + 1).join(this.s)); |
|
41050 }, |
|
41051 |
|
41052 toBoolean: function() { |
|
41053 if (typeof this.orig === 'string') { |
|
41054 var s = this.s.toLowerCase(); |
|
41055 return s === 'true' || s === 'yes' || s === 'on' || s === '1'; |
|
41056 } else |
|
41057 return this.orig === true || this.orig === 1; |
|
41058 }, |
|
41059 |
|
41060 toFloat: function(precision) { |
|
41061 var num = parseFloat(this.s) |
|
41062 if (precision) |
|
41063 return parseFloat(num.toFixed(precision)) |
|
41064 else |
|
41065 return num |
|
41066 }, |
|
41067 |
|
41068 toInt: function() { //thanks Google |
|
41069 // If the string starts with '0x' or '-0x', parse as hex. |
|
41070 return /^\s*-?0x/i.test(this.s) ? parseInt(this.s, 16) : parseInt(this.s, 10) |
|
41071 }, |
|
41072 |
|
41073 trim: function() { |
|
41074 var s; |
|
41075 if (typeof __nsp.trim === 'undefined') |
|
41076 s = this.s.replace(/(^\s*|\s*$)/g, '') |
|
41077 else |
|
41078 s = this.s.trim() |
|
41079 return new this.constructor(s); |
|
41080 }, |
|
41081 |
|
41082 trimLeft: function() { |
|
41083 var s; |
|
41084 if (__nsp.trimLeft) |
|
41085 s = this.s.trimLeft(); |
|
41086 else |
|
41087 s = this.s.replace(/(^\s*)/g, ''); |
|
41088 return new this.constructor(s); |
|
41089 }, |
|
41090 |
|
41091 trimRight: function() { |
|
41092 var s; |
|
41093 if (__nsp.trimRight) |
|
41094 s = this.s.trimRight(); |
|
41095 else |
|
41096 s = this.s.replace(/\s+$/, ''); |
|
41097 return new this.constructor(s); |
|
41098 }, |
|
41099 |
|
41100 truncate: function(length, pruneStr) { //from underscore.string, author: github.com/rwz |
|
41101 var str = this.s; |
|
41102 |
|
41103 length = ~~length; |
|
41104 pruneStr = pruneStr || '...'; |
|
41105 |
|
41106 if (str.length <= length) return new this.constructor(str); |
|
41107 |
|
41108 var tmpl = function(c){ return c.toUpperCase() !== c.toLowerCase() ? 'A' : ' '; }, |
|
41109 template = str.slice(0, length+1).replace(/.(?=\W*\w*$)/g, tmpl); // 'Hello, world' -> 'HellAA AAAAA' |
|
41110 |
|
41111 if (template.slice(template.length-2).match(/\w\w/)) |
|
41112 template = template.replace(/\s*\S+$/, ''); |
|
41113 else |
|
41114 template = new S(template.slice(0, template.length-1)).trimRight().s; |
|
41115 |
|
41116 return (template+pruneStr).length > str.length ? new S(str) : new S(str.slice(0, template.length)+pruneStr); |
|
41117 }, |
|
41118 |
|
41119 toCSV: function() { |
|
41120 var delim = ',', qualifier = '"', escape = '\\', encloseNumbers = true, keys = false; |
|
41121 var dataArray = []; |
|
41122 |
|
41123 function hasVal(it) { |
|
41124 return it !== null && it !== ''; |
|
41125 } |
|
41126 |
|
41127 if (typeof arguments[0] === 'object') { |
|
41128 delim = arguments[0].delimiter || delim; |
|
41129 delim = arguments[0].separator || delim; |
|
41130 qualifier = arguments[0].qualifier || qualifier; |
|
41131 encloseNumbers = !!arguments[0].encloseNumbers; |
|
41132 escape = arguments[0].escape || escape; |
|
41133 keys = !!arguments[0].keys; |
|
41134 } else if (typeof arguments[0] === 'string') { |
|
41135 delim = arguments[0]; |
|
41136 } |
|
41137 |
|
41138 if (typeof arguments[1] === 'string') |
|
41139 qualifier = arguments[1]; |
|
41140 |
|
41141 if (arguments[1] === null) |
|
41142 qualifier = null; |
|
41143 |
|
41144 if (this.orig instanceof Array) |
|
41145 dataArray = this.orig; |
|
41146 else { //object |
|
41147 for (var key in this.orig) |
|
41148 if (this.orig.hasOwnProperty(key)) |
|
41149 if (keys) |
|
41150 dataArray.push(key); |
|
41151 else |
|
41152 dataArray.push(this.orig[key]); |
|
41153 } |
|
41154 |
|
41155 var rep = escape + qualifier; |
|
41156 var buildString = []; |
|
41157 for (var i = 0; i < dataArray.length; ++i) { |
|
41158 var shouldQualify = hasVal(qualifier) |
|
41159 if (typeof dataArray[i] == 'number') |
|
41160 shouldQualify &= encloseNumbers; |
|
41161 |
|
41162 if (shouldQualify) |
|
41163 buildString.push(qualifier); |
|
41164 |
|
41165 if (dataArray[i] !== null && dataArray[i] !== undefined) { |
|
41166 var d = new S(dataArray[i]).replaceAll(qualifier, rep).s; |
|
41167 buildString.push(d); |
|
41168 } else |
|
41169 buildString.push('') |
|
41170 |
|
41171 if (shouldQualify) |
|
41172 buildString.push(qualifier); |
|
41173 |
|
41174 if (delim) |
|
41175 buildString.push(delim); |
|
41176 } |
|
41177 |
|
41178 //chop last delim |
|
41179 //console.log(buildString.length) |
|
41180 buildString.length = buildString.length - 1; |
|
41181 return new this.constructor(buildString.join('')); |
|
41182 }, |
|
41183 |
|
41184 toString: function() { |
|
41185 return this.s; |
|
41186 }, |
|
41187 |
|
41188 //#modified from https://github.com/epeli/underscore.string |
|
41189 underscore: function() { |
|
41190 var s = this.trim().s.replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/[-\s]+/g, '_').toLowerCase(); |
|
41191 if ((new S(this.s.charAt(0))).isUpper()) { |
|
41192 s = '_' + s; |
|
41193 } |
|
41194 return new this.constructor(s); |
|
41195 }, |
|
41196 |
|
41197 unescapeHTML: function() { //from underscore.string |
|
41198 return new this.constructor(this.s.replace(/\&([^;]+);/g, function(entity, entityCode){ |
|
41199 var match; |
|
41200 |
|
41201 if (entityCode in escapeChars) { |
|
41202 return escapeChars[entityCode]; |
|
41203 } else if (match = entityCode.match(/^#x([\da-fA-F]+)$/)) { |
|
41204 return String.fromCharCode(parseInt(match[1], 16)); |
|
41205 } else if (match = entityCode.match(/^#(\d+)$/)) { |
|
41206 return String.fromCharCode(~~match[1]); |
|
41207 } else { |
|
41208 return entity; |
|
41209 } |
|
41210 })); |
|
41211 }, |
|
41212 |
|
41213 valueOf: function() { |
|
41214 return this.s.valueOf(); |
|
41215 }, |
|
41216 |
|
41217 //#Added a New Function called wrapHTML. |
|
41218 wrapHTML: function (tagName, tagAttrs) { |
|
41219 var s = this.s, el = (tagName == null) ? 'span' : tagName, elAttr = '', wrapped = ''; |
|
41220 if(typeof tagAttrs == 'object') for(var prop in tagAttrs) elAttr += ' ' + prop + '="' +(new this.constructor(tagAttrs[prop])).escapeHTML() + '"'; |
|
41221 s = wrapped.concat('<', el, elAttr, '>', this, '</', el, '>'); |
|
41222 return new this.constructor(s); |
|
41223 } |
|
41224 } |
|
41225 |
|
41226 var methodsAdded = []; |
|
41227 function extendPrototype() { |
|
41228 for (var name in __sp) { |
|
41229 (function(name){ |
|
41230 var func = __sp[name]; |
|
41231 if (!__nsp.hasOwnProperty(name)) { |
|
41232 methodsAdded.push(name); |
|
41233 __nsp[name] = function() { |
|
41234 String.prototype.s = this; |
|
41235 return func.apply(this, arguments); |
|
41236 } |
|
41237 } |
|
41238 })(name); |
|
41239 } |
|
41240 } |
|
41241 |
|
41242 function restorePrototype() { |
|
41243 for (var i = 0; i < methodsAdded.length; ++i) |
|
41244 delete String.prototype[methodsAdded[i]]; |
|
41245 methodsAdded.length = 0; |
|
41246 } |
|
41247 |
|
41248 |
|
41249 /************************************* |
|
41250 /* Attach Native JavaScript String Properties |
|
41251 /*************************************/ |
|
41252 |
|
41253 var nativeProperties = getNativeStringProperties(); |
|
41254 for (var name in nativeProperties) { |
|
41255 (function(name) { |
|
41256 var stringProp = __nsp[name]; |
|
41257 if (typeof stringProp == 'function') { |
|
41258 //console.log(stringProp) |
|
41259 if (!__sp[name]) { |
|
41260 if (nativeProperties[name] === 'string') { |
|
41261 __sp[name] = function() { |
|
41262 //console.log(name) |
|
41263 return new this.constructor(stringProp.apply(this, arguments)); |
|
41264 } |
|
41265 } else { |
|
41266 __sp[name] = stringProp; |
|
41267 } |
|
41268 } |
|
41269 } |
|
41270 })(name); |
|
41271 } |
|
41272 |
|
41273 |
|
41274 /************************************* |
|
41275 /* Function Aliases |
|
41276 /*************************************/ |
|
41277 |
|
41278 __sp.repeat = __sp.times; |
|
41279 __sp.include = __sp.contains; |
|
41280 __sp.toInteger = __sp.toInt; |
|
41281 __sp.toBool = __sp.toBoolean; |
|
41282 __sp.decodeHTMLEntities = __sp.decodeHtmlEntities //ensure consistent casing scheme of 'HTML' |
|
41283 |
|
41284 |
|
41285 //****************************************************************************** |
|
41286 // Set the constructor. Without this, string.js objects are instances of |
|
41287 // Object instead of S. |
|
41288 //****************************************************************************** |
|
41289 |
|
41290 __sp.constructor = S; |
|
41291 |
|
41292 |
|
41293 /************************************* |
|
41294 /* Private Functions |
|
41295 /*************************************/ |
|
41296 |
|
41297 function getNativeStringProperties() { |
|
41298 var names = getNativeStringPropertyNames(); |
|
41299 var retObj = {}; |
|
41300 |
|
41301 for (var i = 0; i < names.length; ++i) { |
|
41302 var name = names[i]; |
|
41303 var func = __nsp[name]; |
|
41304 try { |
|
41305 var type = typeof func.apply('teststring', []); |
|
41306 retObj[name] = type; |
|
41307 } catch (e) {} |
|
41308 } |
|
41309 return retObj; |
|
41310 } |
|
41311 |
|
41312 function getNativeStringPropertyNames() { |
|
41313 var results = []; |
|
41314 if (Object.getOwnPropertyNames) { |
|
41315 results = Object.getOwnPropertyNames(__nsp); |
|
41316 results.splice(results.indexOf('valueOf'), 1); |
|
41317 results.splice(results.indexOf('toString'), 1); |
|
41318 return results; |
|
41319 } else { //meant for legacy cruft, this could probably be made more efficient |
|
41320 var stringNames = {}; |
|
41321 var objectNames = []; |
|
41322 for (var name in String.prototype) |
|
41323 stringNames[name] = name; |
|
41324 |
|
41325 for (var name in Object.prototype) |
|
41326 delete stringNames[name]; |
|
41327 |
|
41328 //stringNames['toString'] = 'toString'; //this was deleted with the rest of the object names |
|
41329 for (var name in stringNames) { |
|
41330 results.push(name); |
|
41331 } |
|
41332 return results; |
|
41333 } |
|
41334 } |
|
41335 |
|
41336 function Export(str) { |
|
41337 return new S(str); |
|
41338 }; |
|
41339 |
|
41340 //attach exports to StringJSWrapper |
|
41341 Export.extendPrototype = extendPrototype; |
|
41342 Export.restorePrototype = restorePrototype; |
|
41343 Export.VERSION = VERSION; |
|
41344 Export.TMPL_OPEN = '{{'; |
|
41345 Export.TMPL_CLOSE = '}}'; |
|
41346 Export.ENTITIES = ENTITIES; |
|
41347 |
|
41348 |
|
41349 |
|
41350 /************************************* |
|
41351 /* Exports |
|
41352 /*************************************/ |
|
41353 |
|
41354 if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { |
|
41355 module.exports = Export; |
|
41356 |
|
41357 } else { |
|
41358 |
|
41359 if(typeof define === "function" && define.amd) { |
|
41360 define([], function() { |
|
41361 return Export; |
|
41362 }); |
|
41363 } else { |
|
41364 window.S = Export; |
|
41365 } |
|
41366 } |
|
41367 |
|
41368 |
|
41369 /************************************* |
|
41370 /* 3rd Party Private Functions |
|
41371 /*************************************/ |
|
41372 |
|
41373 //from sugar.js |
|
41374 function multiArgs(args, fn) { |
|
41375 var result = [], i; |
|
41376 for(i = 0; i < args.length; i++) { |
|
41377 result.push(args[i]); |
|
41378 if(fn) fn.call(args, args[i], i); |
|
41379 } |
|
41380 return result; |
|
41381 } |
|
41382 |
|
41383 //from underscore.string |
|
41384 var escapeChars = { |
|
41385 lt: '<', |
|
41386 gt: '>', |
|
41387 quot: '"', |
|
41388 apos: "'", |
|
41389 amp: '&' |
|
41390 }; |
|
41391 |
|
41392 //from underscore.string |
|
41393 var reversedEscapeChars = {}; |
|
41394 for(var key in escapeChars){ reversedEscapeChars[escapeChars[key]] = key; } |
|
41395 |
|
41396 ENTITIES = { |
|
41397 "amp" : "&", |
|
41398 "gt" : ">", |
|
41399 "lt" : "<", |
|
41400 "quot" : "\"", |
|
41401 "apos" : "'", |
|
41402 "AElig" : 198, |
|
41403 "Aacute" : 193, |
|
41404 "Acirc" : 194, |
|
41405 "Agrave" : 192, |
|
41406 "Aring" : 197, |
|
41407 "Atilde" : 195, |
|
41408 "Auml" : 196, |
|
41409 "Ccedil" : 199, |
|
41410 "ETH" : 208, |
|
41411 "Eacute" : 201, |
|
41412 "Ecirc" : 202, |
|
41413 "Egrave" : 200, |
|
41414 "Euml" : 203, |
|
41415 "Iacute" : 205, |
|
41416 "Icirc" : 206, |
|
41417 "Igrave" : 204, |
|
41418 "Iuml" : 207, |
|
41419 "Ntilde" : 209, |
|
41420 "Oacute" : 211, |
|
41421 "Ocirc" : 212, |
|
41422 "Ograve" : 210, |
|
41423 "Oslash" : 216, |
|
41424 "Otilde" : 213, |
|
41425 "Ouml" : 214, |
|
41426 "THORN" : 222, |
|
41427 "Uacute" : 218, |
|
41428 "Ucirc" : 219, |
|
41429 "Ugrave" : 217, |
|
41430 "Uuml" : 220, |
|
41431 "Yacute" : 221, |
|
41432 "aacute" : 225, |
|
41433 "acirc" : 226, |
|
41434 "aelig" : 230, |
|
41435 "agrave" : 224, |
|
41436 "aring" : 229, |
|
41437 "atilde" : 227, |
|
41438 "auml" : 228, |
|
41439 "ccedil" : 231, |
|
41440 "eacute" : 233, |
|
41441 "ecirc" : 234, |
|
41442 "egrave" : 232, |
|
41443 "eth" : 240, |
|
41444 "euml" : 235, |
|
41445 "iacute" : 237, |
|
41446 "icirc" : 238, |
|
41447 "igrave" : 236, |
|
41448 "iuml" : 239, |
|
41449 "ntilde" : 241, |
|
41450 "oacute" : 243, |
|
41451 "ocirc" : 244, |
|
41452 "ograve" : 242, |
|
41453 "oslash" : 248, |
|
41454 "otilde" : 245, |
|
41455 "ouml" : 246, |
|
41456 "szlig" : 223, |
|
41457 "thorn" : 254, |
|
41458 "uacute" : 250, |
|
41459 "ucirc" : 251, |
|
41460 "ugrave" : 249, |
|
41461 "uuml" : 252, |
|
41462 "yacute" : 253, |
|
41463 "yuml" : 255, |
|
41464 "copy" : 169, |
|
41465 "reg" : 174, |
|
41466 "nbsp" : 160, |
|
41467 "iexcl" : 161, |
|
41468 "cent" : 162, |
|
41469 "pound" : 163, |
|
41470 "curren" : 164, |
|
41471 "yen" : 165, |
|
41472 "brvbar" : 166, |
|
41473 "sect" : 167, |
|
41474 "uml" : 168, |
|
41475 "ordf" : 170, |
|
41476 "laquo" : 171, |
|
41477 "not" : 172, |
|
41478 "shy" : 173, |
|
41479 "macr" : 175, |
|
41480 "deg" : 176, |
|
41481 "plusmn" : 177, |
|
41482 "sup1" : 185, |
|
41483 "sup2" : 178, |
|
41484 "sup3" : 179, |
|
41485 "acute" : 180, |
|
41486 "micro" : 181, |
|
41487 "para" : 182, |
|
41488 "middot" : 183, |
|
41489 "cedil" : 184, |
|
41490 "ordm" : 186, |
|
41491 "raquo" : 187, |
|
41492 "frac14" : 188, |
|
41493 "frac12" : 189, |
|
41494 "frac34" : 190, |
|
41495 "iquest" : 191, |
|
41496 "times" : 215, |
|
41497 "divide" : 247, |
|
41498 "OElig;" : 338, |
|
41499 "oelig;" : 339, |
|
41500 "Scaron;" : 352, |
|
41501 "scaron;" : 353, |
|
41502 "Yuml;" : 376, |
|
41503 "fnof;" : 402, |
|
41504 "circ;" : 710, |
|
41505 "tilde;" : 732, |
|
41506 "Alpha;" : 913, |
|
41507 "Beta;" : 914, |
|
41508 "Gamma;" : 915, |
|
41509 "Delta;" : 916, |
|
41510 "Epsilon;" : 917, |
|
41511 "Zeta;" : 918, |
|
41512 "Eta;" : 919, |
|
41513 "Theta;" : 920, |
|
41514 "Iota;" : 921, |
|
41515 "Kappa;" : 922, |
|
41516 "Lambda;" : 923, |
|
41517 "Mu;" : 924, |
|
41518 "Nu;" : 925, |
|
41519 "Xi;" : 926, |
|
41520 "Omicron;" : 927, |
|
41521 "Pi;" : 928, |
|
41522 "Rho;" : 929, |
|
41523 "Sigma;" : 931, |
|
41524 "Tau;" : 932, |
|
41525 "Upsilon;" : 933, |
|
41526 "Phi;" : 934, |
|
41527 "Chi;" : 935, |
|
41528 "Psi;" : 936, |
|
41529 "Omega;" : 937, |
|
41530 "alpha;" : 945, |
|
41531 "beta;" : 946, |
|
41532 "gamma;" : 947, |
|
41533 "delta;" : 948, |
|
41534 "epsilon;" : 949, |
|
41535 "zeta;" : 950, |
|
41536 "eta;" : 951, |
|
41537 "theta;" : 952, |
|
41538 "iota;" : 953, |
|
41539 "kappa;" : 954, |
|
41540 "lambda;" : 955, |
|
41541 "mu;" : 956, |
|
41542 "nu;" : 957, |
|
41543 "xi;" : 958, |
|
41544 "omicron;" : 959, |
|
41545 "pi;" : 960, |
|
41546 "rho;" : 961, |
|
41547 "sigmaf;" : 962, |
|
41548 "sigma;" : 963, |
|
41549 "tau;" : 964, |
|
41550 "upsilon;" : 965, |
|
41551 "phi;" : 966, |
|
41552 "chi;" : 967, |
|
41553 "psi;" : 968, |
|
41554 "omega;" : 969, |
|
41555 "thetasym;" : 977, |
|
41556 "upsih;" : 978, |
|
41557 "piv;" : 982, |
|
41558 "ensp;" : 8194, |
|
41559 "emsp;" : 8195, |
|
41560 "thinsp;" : 8201, |
|
41561 "zwnj;" : 8204, |
|
41562 "zwj;" : 8205, |
|
41563 "lrm;" : 8206, |
|
41564 "rlm;" : 8207, |
|
41565 "ndash;" : 8211, |
|
41566 "mdash;" : 8212, |
|
41567 "lsquo;" : 8216, |
|
41568 "rsquo;" : 8217, |
|
41569 "sbquo;" : 8218, |
|
41570 "ldquo;" : 8220, |
|
41571 "rdquo;" : 8221, |
|
41572 "bdquo;" : 8222, |
|
41573 "dagger;" : 8224, |
|
41574 "Dagger;" : 8225, |
|
41575 "bull;" : 8226, |
|
41576 "hellip;" : 8230, |
|
41577 "permil;" : 8240, |
|
41578 "prime;" : 8242, |
|
41579 "Prime;" : 8243, |
|
41580 "lsaquo;" : 8249, |
|
41581 "rsaquo;" : 8250, |
|
41582 "oline;" : 8254, |
|
41583 "frasl;" : 8260, |
|
41584 "euro;" : 8364, |
|
41585 "image;" : 8465, |
|
41586 "weierp;" : 8472, |
|
41587 "real;" : 8476, |
|
41588 "trade;" : 8482, |
|
41589 "alefsym;" : 8501, |
|
41590 "larr;" : 8592, |
|
41591 "uarr;" : 8593, |
|
41592 "rarr;" : 8594, |
|
41593 "darr;" : 8595, |
|
41594 "harr;" : 8596, |
|
41595 "crarr;" : 8629, |
|
41596 "lArr;" : 8656, |
|
41597 "uArr;" : 8657, |
|
41598 "rArr;" : 8658, |
|
41599 "dArr;" : 8659, |
|
41600 "hArr;" : 8660, |
|
41601 "forall;" : 8704, |
|
41602 "part;" : 8706, |
|
41603 "exist;" : 8707, |
|
41604 "empty;" : 8709, |
|
41605 "nabla;" : 8711, |
|
41606 "isin;" : 8712, |
|
41607 "notin;" : 8713, |
|
41608 "ni;" : 8715, |
|
41609 "prod;" : 8719, |
|
41610 "sum;" : 8721, |
|
41611 "minus;" : 8722, |
|
41612 "lowast;" : 8727, |
|
41613 "radic;" : 8730, |
|
41614 "prop;" : 8733, |
|
41615 "infin;" : 8734, |
|
41616 "ang;" : 8736, |
|
41617 "and;" : 8743, |
|
41618 "or;" : 8744, |
|
41619 "cap;" : 8745, |
|
41620 "cup;" : 8746, |
|
41621 "int;" : 8747, |
|
41622 "there4;" : 8756, |
|
41623 "sim;" : 8764, |
|
41624 "cong;" : 8773, |
|
41625 "asymp;" : 8776, |
|
41626 "ne;" : 8800, |
|
41627 "equiv;" : 8801, |
|
41628 "le;" : 8804, |
|
41629 "ge;" : 8805, |
|
41630 "sub;" : 8834, |
|
41631 "sup;" : 8835, |
|
41632 "nsub;" : 8836, |
|
41633 "sube;" : 8838, |
|
41634 "supe;" : 8839, |
|
41635 "oplus;" : 8853, |
|
41636 "otimes;" : 8855, |
|
41637 "perp;" : 8869, |
|
41638 "sdot;" : 8901, |
|
41639 "lceil;" : 8968, |
|
41640 "rceil;" : 8969, |
|
41641 "lfloor;" : 8970, |
|
41642 "rfloor;" : 8971, |
|
41643 "lang;" : 9001, |
|
41644 "rang;" : 9002, |
|
41645 "loz;" : 9674, |
|
41646 "spades;" : 9824, |
|
41647 "clubs;" : 9827, |
|
41648 "hearts;" : 9829, |
|
41649 "diams;" : 9830 |
|
41650 } |
|
41651 |
|
41652 |
|
41653 }).call(this); |
|
41654 |
|
41655 /*! |
|
41656 * Bootstrap v3.1.1 (http://getbootstrap.com) |
|
41657 * Copyright 2011-2014 Twitter, Inc. |
|
41658 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
41659 */ |
|
41660 |
|
41661 if (typeof jQuery === 'undefined') { throw new Error('Bootstrap\'s JavaScript requires jQuery') } |
|
41662 |
|
41663 /* ======================================================================== |
|
41664 * Bootstrap: transition.js v3.1.1 |
|
41665 * http://getbootstrap.com/javascript/#transitions |
|
41666 * ======================================================================== |
|
41667 * Copyright 2011-2014 Twitter, Inc. |
|
41668 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
41669 * ======================================================================== */ |
|
41670 |
|
41671 |
|
41672 +function ($) { |
|
41673 'use strict'; |
|
41674 |
|
41675 // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) |
|
41676 // ============================================================ |
|
41677 |
|
41678 function transitionEnd() { |
|
41679 var el = document.createElement('bootstrap') |
|
41680 |
|
41681 var transEndEventNames = { |
|
41682 'WebkitTransition' : 'webkitTransitionEnd', |
|
41683 'MozTransition' : 'transitionend', |
|
41684 'OTransition' : 'oTransitionEnd otransitionend', |
|
41685 'transition' : 'transitionend' |
|
41686 } |
|
41687 |
|
41688 for (var name in transEndEventNames) { |
|
41689 if (el.style[name] !== undefined) { |
|
41690 return { end: transEndEventNames[name] } |
|
41691 } |
|
41692 } |
|
41693 |
|
41694 return false // explicit for ie8 ( ._.) |
|
41695 } |
|
41696 |
|
41697 // http://blog.alexmaccaw.com/css-transitions |
|
41698 $.fn.emulateTransitionEnd = function (duration) { |
|
41699 var called = false, $el = this |
|
41700 $(this).one($.support.transition.end, function () { called = true }) |
|
41701 var callback = function () { if (!called) $($el).trigger($.support.transition.end) } |
|
41702 setTimeout(callback, duration) |
|
41703 return this |
|
41704 } |
|
41705 |
|
41706 $(function () { |
|
41707 $.support.transition = transitionEnd() |
|
41708 }) |
|
41709 |
|
41710 }(jQuery); |
|
41711 |
|
41712 /* ======================================================================== |
|
41713 * Bootstrap: alert.js v3.1.1 |
|
41714 * http://getbootstrap.com/javascript/#alerts |
|
41715 * ======================================================================== |
|
41716 * Copyright 2011-2014 Twitter, Inc. |
|
41717 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
41718 * ======================================================================== */ |
|
41719 |
|
41720 |
|
41721 +function ($) { |
|
41722 'use strict'; |
|
41723 |
|
41724 // ALERT CLASS DEFINITION |
|
41725 // ====================== |
|
41726 |
|
41727 var dismiss = '[data-dismiss="alert"]' |
|
41728 var Alert = function (el) { |
|
41729 $(el).on('click', dismiss, this.close) |
|
41730 } |
|
41731 |
|
41732 Alert.prototype.close = function (e) { |
|
41733 var $this = $(this) |
|
41734 var selector = $this.attr('data-target') |
|
41735 |
|
41736 if (!selector) { |
|
41737 selector = $this.attr('href') |
|
41738 selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 |
|
41739 } |
|
41740 |
|
41741 var $parent = $(selector) |
|
41742 |
|
41743 if (e) e.preventDefault() |
|
41744 |
|
41745 if (!$parent.length) { |
|
41746 $parent = $this.hasClass('alert') ? $this : $this.parent() |
|
41747 } |
|
41748 |
|
41749 $parent.trigger(e = $.Event('close.bs.alert')) |
|
41750 |
|
41751 if (e.isDefaultPrevented()) return |
|
41752 |
|
41753 $parent.removeClass('in') |
|
41754 |
|
41755 function removeElement() { |
|
41756 $parent.trigger('closed.bs.alert').remove() |
|
41757 } |
|
41758 |
|
41759 $.support.transition && $parent.hasClass('fade') ? |
|
41760 $parent |
|
41761 .one($.support.transition.end, removeElement) |
|
41762 .emulateTransitionEnd(150) : |
|
41763 removeElement() |
|
41764 } |
|
41765 |
|
41766 |
|
41767 // ALERT PLUGIN DEFINITION |
|
41768 // ======================= |
|
41769 |
|
41770 var old = $.fn.alert |
|
41771 |
|
41772 $.fn.alert = function (option) { |
|
41773 return this.each(function () { |
|
41774 var $this = $(this) |
|
41775 var data = $this.data('bs.alert') |
|
41776 |
|
41777 if (!data) $this.data('bs.alert', (data = new Alert(this))) |
|
41778 if (typeof option == 'string') data[option].call($this) |
|
41779 }) |
|
41780 } |
|
41781 |
|
41782 $.fn.alert.Constructor = Alert |
|
41783 |
|
41784 |
|
41785 // ALERT NO CONFLICT |
|
41786 // ================= |
|
41787 |
|
41788 $.fn.alert.noConflict = function () { |
|
41789 $.fn.alert = old |
|
41790 return this |
|
41791 } |
|
41792 |
|
41793 |
|
41794 // ALERT DATA-API |
|
41795 // ============== |
|
41796 |
|
41797 $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) |
|
41798 |
|
41799 }(jQuery); |
|
41800 |
|
41801 /* ======================================================================== |
|
41802 * Bootstrap: button.js v3.1.1 |
|
41803 * http://getbootstrap.com/javascript/#buttons |
|
41804 * ======================================================================== |
|
41805 * Copyright 2011-2014 Twitter, Inc. |
|
41806 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
41807 * ======================================================================== */ |
|
41808 |
|
41809 |
|
41810 +function ($) { |
|
41811 'use strict'; |
|
41812 |
|
41813 // BUTTON PUBLIC CLASS DEFINITION |
|
41814 // ============================== |
|
41815 |
|
41816 var Button = function (element, options) { |
|
41817 this.$element = $(element) |
|
41818 this.options = $.extend({}, Button.DEFAULTS, options) |
|
41819 this.isLoading = false |
|
41820 } |
|
41821 |
|
41822 Button.DEFAULTS = { |
|
41823 loadingText: 'loading...' |
|
41824 } |
|
41825 |
|
41826 Button.prototype.setState = function (state) { |
|
41827 var d = 'disabled' |
|
41828 var $el = this.$element |
|
41829 var val = $el.is('input') ? 'val' : 'html' |
|
41830 var data = $el.data() |
|
41831 |
|
41832 state = state + 'Text' |
|
41833 |
|
41834 if (!data.resetText) $el.data('resetText', $el[val]()) |
|
41835 |
|
41836 $el[val](data[state] || this.options[state]) |
|
41837 |
|
41838 // push to event loop to allow forms to submit |
|
41839 setTimeout($.proxy(function () { |
|
41840 if (state == 'loadingText') { |
|
41841 this.isLoading = true |
|
41842 $el.addClass(d).attr(d, d) |
|
41843 } else if (this.isLoading) { |
|
41844 this.isLoading = false |
|
41845 $el.removeClass(d).removeAttr(d) |
|
41846 } |
|
41847 }, this), 0) |
|
41848 } |
|
41849 |
|
41850 Button.prototype.toggle = function () { |
|
41851 var changed = true |
|
41852 var $parent = this.$element.closest('[data-toggle="buttons"]') |
|
41853 |
|
41854 if ($parent.length) { |
|
41855 var $input = this.$element.find('input') |
|
41856 if ($input.prop('type') == 'radio') { |
|
41857 if ($input.prop('checked') && this.$element.hasClass('active')) changed = false |
|
41858 else $parent.find('.active').removeClass('active') |
|
41859 } |
|
41860 if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') |
|
41861 } |
|
41862 |
|
41863 if (changed) this.$element.toggleClass('active') |
|
41864 } |
|
41865 |
|
41866 |
|
41867 // BUTTON PLUGIN DEFINITION |
|
41868 // ======================== |
|
41869 |
|
41870 var old = $.fn.button |
|
41871 |
|
41872 $.fn.button = function (option) { |
|
41873 return this.each(function () { |
|
41874 var $this = $(this) |
|
41875 var data = $this.data('bs.button') |
|
41876 var options = typeof option == 'object' && option |
|
41877 |
|
41878 if (!data) $this.data('bs.button', (data = new Button(this, options))) |
|
41879 |
|
41880 if (option == 'toggle') data.toggle() |
|
41881 else if (option) data.setState(option) |
|
41882 }) |
|
41883 } |
|
41884 |
|
41885 $.fn.button.Constructor = Button |
|
41886 |
|
41887 |
|
41888 // BUTTON NO CONFLICT |
|
41889 // ================== |
|
41890 |
|
41891 $.fn.button.noConflict = function () { |
|
41892 $.fn.button = old |
|
41893 return this |
|
41894 } |
|
41895 |
|
41896 |
|
41897 // BUTTON DATA-API |
|
41898 // =============== |
|
41899 |
|
41900 $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) { |
|
41901 var $btn = $(e.target) |
|
41902 if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') |
|
41903 $btn.button('toggle') |
|
41904 e.preventDefault() |
|
41905 }) |
|
41906 |
|
41907 }(jQuery); |
|
41908 |
|
41909 /* ======================================================================== |
|
41910 * Bootstrap: carousel.js v3.1.1 |
|
41911 * http://getbootstrap.com/javascript/#carousel |
|
41912 * ======================================================================== |
|
41913 * Copyright 2011-2014 Twitter, Inc. |
|
41914 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
41915 * ======================================================================== */ |
|
41916 |
|
41917 |
|
41918 +function ($) { |
|
41919 'use strict'; |
|
41920 |
|
41921 // CAROUSEL CLASS DEFINITION |
|
41922 // ========================= |
|
41923 |
|
41924 var Carousel = function (element, options) { |
|
41925 this.$element = $(element) |
|
41926 this.$indicators = this.$element.find('.carousel-indicators') |
|
41927 this.options = options |
|
41928 this.paused = |
|
41929 this.sliding = |
|
41930 this.interval = |
|
41931 this.$active = |
|
41932 this.$items = null |
|
41933 |
|
41934 this.options.pause == 'hover' && this.$element |
|
41935 .on('mouseenter', $.proxy(this.pause, this)) |
|
41936 .on('mouseleave', $.proxy(this.cycle, this)) |
|
41937 } |
|
41938 |
|
41939 Carousel.DEFAULTS = { |
|
41940 interval: 5000, |
|
41941 pause: 'hover', |
|
41942 wrap: true |
|
41943 } |
|
41944 |
|
41945 Carousel.prototype.cycle = function (e) { |
|
41946 e || (this.paused = false) |
|
41947 |
|
41948 this.interval && clearInterval(this.interval) |
|
41949 |
|
41950 this.options.interval |
|
41951 && !this.paused |
|
41952 && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) |
|
41953 |
|
41954 return this |
|
41955 } |
|
41956 |
|
41957 Carousel.prototype.getActiveIndex = function () { |
|
41958 this.$active = this.$element.find('.item.active') |
|
41959 this.$items = this.$active.parent().children() |
|
41960 |
|
41961 return this.$items.index(this.$active) |
|
41962 } |
|
41963 |
|
41964 Carousel.prototype.to = function (pos) { |
|
41965 var that = this |
|
41966 var activeIndex = this.getActiveIndex() |
|
41967 |
|
41968 if (pos > (this.$items.length - 1) || pos < 0) return |
|
41969 |
|
41970 if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) |
|
41971 if (activeIndex == pos) return this.pause().cycle() |
|
41972 |
|
41973 return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) |
|
41974 } |
|
41975 |
|
41976 Carousel.prototype.pause = function (e) { |
|
41977 e || (this.paused = true) |
|
41978 |
|
41979 if (this.$element.find('.next, .prev').length && $.support.transition) { |
|
41980 this.$element.trigger($.support.transition.end) |
|
41981 this.cycle(true) |
|
41982 } |
|
41983 |
|
41984 this.interval = clearInterval(this.interval) |
|
41985 |
|
41986 return this |
|
41987 } |
|
41988 |
|
41989 Carousel.prototype.next = function () { |
|
41990 if (this.sliding) return |
|
41991 return this.slide('next') |
|
41992 } |
|
41993 |
|
41994 Carousel.prototype.prev = function () { |
|
41995 if (this.sliding) return |
|
41996 return this.slide('prev') |
|
41997 } |
|
41998 |
|
41999 Carousel.prototype.slide = function (type, next) { |
|
42000 var $active = this.$element.find('.item.active') |
|
42001 var $next = next || $active[type]() |
|
42002 var isCycling = this.interval |
|
42003 var direction = type == 'next' ? 'left' : 'right' |
|
42004 var fallback = type == 'next' ? 'first' : 'last' |
|
42005 var that = this |
|
42006 |
|
42007 if (!$next.length) { |
|
42008 if (!this.options.wrap) return |
|
42009 $next = this.$element.find('.item')[fallback]() |
|
42010 } |
|
42011 |
|
42012 if ($next.hasClass('active')) return this.sliding = false |
|
42013 |
|
42014 var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction }) |
|
42015 this.$element.trigger(e) |
|
42016 if (e.isDefaultPrevented()) return |
|
42017 |
|
42018 this.sliding = true |
|
42019 |
|
42020 isCycling && this.pause() |
|
42021 |
|
42022 if (this.$indicators.length) { |
|
42023 this.$indicators.find('.active').removeClass('active') |
|
42024 this.$element.one('slid.bs.carousel', function () { |
|
42025 var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) |
|
42026 $nextIndicator && $nextIndicator.addClass('active') |
|
42027 }) |
|
42028 } |
|
42029 |
|
42030 if ($.support.transition && this.$element.hasClass('slide')) { |
|
42031 $next.addClass(type) |
|
42032 $next[0].offsetWidth // force reflow |
|
42033 $active.addClass(direction) |
|
42034 $next.addClass(direction) |
|
42035 $active |
|
42036 .one($.support.transition.end, function () { |
|
42037 $next.removeClass([type, direction].join(' ')).addClass('active') |
|
42038 $active.removeClass(['active', direction].join(' ')) |
|
42039 that.sliding = false |
|
42040 setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0) |
|
42041 }) |
|
42042 .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000) |
|
42043 } else { |
|
42044 $active.removeClass('active') |
|
42045 $next.addClass('active') |
|
42046 this.sliding = false |
|
42047 this.$element.trigger('slid.bs.carousel') |
|
42048 } |
|
42049 |
|
42050 isCycling && this.cycle() |
|
42051 |
|
42052 return this |
|
42053 } |
|
42054 |
|
42055 |
|
42056 // CAROUSEL PLUGIN DEFINITION |
|
42057 // ========================== |
|
42058 |
|
42059 var old = $.fn.carousel |
|
42060 |
|
42061 $.fn.carousel = function (option) { |
|
42062 return this.each(function () { |
|
42063 var $this = $(this) |
|
42064 var data = $this.data('bs.carousel') |
|
42065 var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) |
|
42066 var action = typeof option == 'string' ? option : options.slide |
|
42067 |
|
42068 if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) |
|
42069 if (typeof option == 'number') data.to(option) |
|
42070 else if (action) data[action]() |
|
42071 else if (options.interval) data.pause().cycle() |
|
42072 }) |
|
42073 } |
|
42074 |
|
42075 $.fn.carousel.Constructor = Carousel |
|
42076 |
|
42077 |
|
42078 // CAROUSEL NO CONFLICT |
|
42079 // ==================== |
|
42080 |
|
42081 $.fn.carousel.noConflict = function () { |
|
42082 $.fn.carousel = old |
|
42083 return this |
|
42084 } |
|
42085 |
|
42086 |
|
42087 // CAROUSEL DATA-API |
|
42088 // ================= |
|
42089 |
|
42090 $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { |
|
42091 var $this = $(this), href |
|
42092 var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 |
|
42093 var options = $.extend({}, $target.data(), $this.data()) |
|
42094 var slideIndex = $this.attr('data-slide-to') |
|
42095 if (slideIndex) options.interval = false |
|
42096 |
|
42097 $target.carousel(options) |
|
42098 |
|
42099 if (slideIndex = $this.attr('data-slide-to')) { |
|
42100 $target.data('bs.carousel').to(slideIndex) |
|
42101 } |
|
42102 |
|
42103 e.preventDefault() |
|
42104 }) |
|
42105 |
|
42106 $(window).on('load', function () { |
|
42107 $('[data-ride="carousel"]').each(function () { |
|
42108 var $carousel = $(this) |
|
42109 $carousel.carousel($carousel.data()) |
|
42110 }) |
|
42111 }) |
|
42112 |
|
42113 }(jQuery); |
|
42114 |
|
42115 /* ======================================================================== |
|
42116 * Bootstrap: collapse.js v3.1.1 |
|
42117 * http://getbootstrap.com/javascript/#collapse |
|
42118 * ======================================================================== |
|
42119 * Copyright 2011-2014 Twitter, Inc. |
|
42120 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
42121 * ======================================================================== */ |
|
42122 |
|
42123 |
|
42124 +function ($) { |
|
42125 'use strict'; |
|
42126 |
|
42127 // COLLAPSE PUBLIC CLASS DEFINITION |
|
42128 // ================================ |
|
42129 |
|
42130 var Collapse = function (element, options) { |
|
42131 this.$element = $(element) |
|
42132 this.options = $.extend({}, Collapse.DEFAULTS, options) |
|
42133 this.transitioning = null |
|
42134 |
|
42135 if (this.options.parent) this.$parent = $(this.options.parent) |
|
42136 if (this.options.toggle) this.toggle() |
|
42137 } |
|
42138 |
|
42139 Collapse.DEFAULTS = { |
|
42140 toggle: true |
|
42141 } |
|
42142 |
|
42143 Collapse.prototype.dimension = function () { |
|
42144 var hasWidth = this.$element.hasClass('width') |
|
42145 return hasWidth ? 'width' : 'height' |
|
42146 } |
|
42147 |
|
42148 Collapse.prototype.show = function () { |
|
42149 if (this.transitioning || this.$element.hasClass('in')) return |
|
42150 |
|
42151 var startEvent = $.Event('show.bs.collapse') |
|
42152 this.$element.trigger(startEvent) |
|
42153 if (startEvent.isDefaultPrevented()) return |
|
42154 |
|
42155 var actives = this.$parent && this.$parent.find('> .panel > .in') |
|
42156 |
|
42157 if (actives && actives.length) { |
|
42158 var hasData = actives.data('bs.collapse') |
|
42159 if (hasData && hasData.transitioning) return |
|
42160 actives.collapse('hide') |
|
42161 hasData || actives.data('bs.collapse', null) |
|
42162 } |
|
42163 |
|
42164 var dimension = this.dimension() |
|
42165 |
|
42166 this.$element |
|
42167 .removeClass('collapse') |
|
42168 .addClass('collapsing') |
|
42169 [dimension](0) |
|
42170 |
|
42171 this.transitioning = 1 |
|
42172 |
|
42173 var complete = function () { |
|
42174 this.$element |
|
42175 .removeClass('collapsing') |
|
42176 .addClass('collapse in') |
|
42177 [dimension]('auto') |
|
42178 this.transitioning = 0 |
|
42179 this.$element.trigger('shown.bs.collapse') |
|
42180 } |
|
42181 |
|
42182 if (!$.support.transition) return complete.call(this) |
|
42183 |
|
42184 var scrollSize = $.camelCase(['scroll', dimension].join('-')) |
|
42185 |
|
42186 this.$element |
|
42187 .one($.support.transition.end, $.proxy(complete, this)) |
|
42188 .emulateTransitionEnd(350) |
|
42189 [dimension](this.$element[0][scrollSize]) |
|
42190 } |
|
42191 |
|
42192 Collapse.prototype.hide = function () { |
|
42193 if (this.transitioning || !this.$element.hasClass('in')) return |
|
42194 |
|
42195 var startEvent = $.Event('hide.bs.collapse') |
|
42196 this.$element.trigger(startEvent) |
|
42197 if (startEvent.isDefaultPrevented()) return |
|
42198 |
|
42199 var dimension = this.dimension() |
|
42200 |
|
42201 this.$element |
|
42202 [dimension](this.$element[dimension]()) |
|
42203 [0].offsetHeight |
|
42204 |
|
42205 this.$element |
|
42206 .addClass('collapsing') |
|
42207 .removeClass('collapse') |
|
42208 .removeClass('in') |
|
42209 |
|
42210 this.transitioning = 1 |
|
42211 |
|
42212 var complete = function () { |
|
42213 this.transitioning = 0 |
|
42214 this.$element |
|
42215 .trigger('hidden.bs.collapse') |
|
42216 .removeClass('collapsing') |
|
42217 .addClass('collapse') |
|
42218 } |
|
42219 |
|
42220 if (!$.support.transition) return complete.call(this) |
|
42221 |
|
42222 this.$element |
|
42223 [dimension](0) |
|
42224 .one($.support.transition.end, $.proxy(complete, this)) |
|
42225 .emulateTransitionEnd(350) |
|
42226 } |
|
42227 |
|
42228 Collapse.prototype.toggle = function () { |
|
42229 this[this.$element.hasClass('in') ? 'hide' : 'show']() |
|
42230 } |
|
42231 |
|
42232 |
|
42233 // COLLAPSE PLUGIN DEFINITION |
|
42234 // ========================== |
|
42235 |
|
42236 var old = $.fn.collapse |
|
42237 |
|
42238 $.fn.collapse = function (option) { |
|
42239 return this.each(function () { |
|
42240 var $this = $(this) |
|
42241 var data = $this.data('bs.collapse') |
|
42242 var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) |
|
42243 |
|
42244 if (!data && options.toggle && option == 'show') option = !option |
|
42245 if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) |
|
42246 if (typeof option == 'string') data[option]() |
|
42247 }) |
|
42248 } |
|
42249 |
|
42250 $.fn.collapse.Constructor = Collapse |
|
42251 |
|
42252 |
|
42253 // COLLAPSE NO CONFLICT |
|
42254 // ==================== |
|
42255 |
|
42256 $.fn.collapse.noConflict = function () { |
|
42257 $.fn.collapse = old |
|
42258 return this |
|
42259 } |
|
42260 |
|
42261 |
|
42262 // COLLAPSE DATA-API |
|
42263 // ================= |
|
42264 |
|
42265 $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) { |
|
42266 var $this = $(this), href |
|
42267 var target = $this.attr('data-target') |
|
42268 || e.preventDefault() |
|
42269 || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 |
|
42270 var $target = $(target) |
|
42271 var data = $target.data('bs.collapse') |
|
42272 var option = data ? 'toggle' : $this.data() |
|
42273 var parent = $this.attr('data-parent') |
|
42274 var $parent = parent && $(parent) |
|
42275 |
|
42276 if (!data || !data.transitioning) { |
|
42277 if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed') |
|
42278 $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed') |
|
42279 } |
|
42280 |
|
42281 $target.collapse(option) |
|
42282 }) |
|
42283 |
|
42284 }(jQuery); |
|
42285 |
|
42286 /* ======================================================================== |
|
42287 * Bootstrap: dropdown.js v3.1.1 |
|
42288 * http://getbootstrap.com/javascript/#dropdowns |
|
42289 * ======================================================================== |
|
42290 * Copyright 2011-2014 Twitter, Inc. |
|
42291 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
42292 * ======================================================================== */ |
|
42293 |
|
42294 |
|
42295 +function ($) { |
|
42296 'use strict'; |
|
42297 |
|
42298 // DROPDOWN CLASS DEFINITION |
|
42299 // ========================= |
|
42300 |
|
42301 var backdrop = '.dropdown-backdrop' |
|
42302 var toggle = '[data-toggle=dropdown]' |
|
42303 var Dropdown = function (element) { |
|
42304 $(element).on('click.bs.dropdown', this.toggle) |
|
42305 } |
|
42306 |
|
42307 Dropdown.prototype.toggle = function (e) { |
|
42308 var $this = $(this) |
|
42309 |
|
42310 if ($this.is('.disabled, :disabled')) return |
|
42311 |
|
42312 var $parent = getParent($this) |
|
42313 var isActive = $parent.hasClass('open') |
|
42314 |
|
42315 clearMenus() |
|
42316 |
|
42317 if (!isActive) { |
|
42318 if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { |
|
42319 // if mobile we use a backdrop because click events don't delegate |
|
42320 $('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus) |
|
42321 } |
|
42322 |
|
42323 var relatedTarget = { relatedTarget: this } |
|
42324 $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) |
|
42325 |
|
42326 if (e.isDefaultPrevented()) return |
|
42327 |
|
42328 $parent |
|
42329 .toggleClass('open') |
|
42330 .trigger('shown.bs.dropdown', relatedTarget) |
|
42331 |
|
42332 $this.focus() |
|
42333 } |
|
42334 |
|
42335 return false |
|
42336 } |
|
42337 |
|
42338 Dropdown.prototype.keydown = function (e) { |
|
42339 if (!/(38|40|27)/.test(e.keyCode)) return |
|
42340 |
|
42341 var $this = $(this) |
|
42342 |
|
42343 e.preventDefault() |
|
42344 e.stopPropagation() |
|
42345 |
|
42346 if ($this.is('.disabled, :disabled')) return |
|
42347 |
|
42348 var $parent = getParent($this) |
|
42349 var isActive = $parent.hasClass('open') |
|
42350 |
|
42351 if (!isActive || (isActive && e.keyCode == 27)) { |
|
42352 if (e.which == 27) $parent.find(toggle).focus() |
|
42353 return $this.click() |
|
42354 } |
|
42355 |
|
42356 var desc = ' li:not(.divider):visible a' |
|
42357 var $items = $parent.find('[role=menu]' + desc + ', [role=listbox]' + desc) |
|
42358 |
|
42359 if (!$items.length) return |
|
42360 |
|
42361 var index = $items.index($items.filter(':focus')) |
|
42362 |
|
42363 if (e.keyCode == 38 && index > 0) index-- // up |
|
42364 if (e.keyCode == 40 && index < $items.length - 1) index++ // down |
|
42365 if (!~index) index = 0 |
|
42366 |
|
42367 $items.eq(index).focus() |
|
42368 } |
|
42369 |
|
42370 function clearMenus(e) { |
|
42371 $(backdrop).remove() |
|
42372 $(toggle).each(function () { |
|
42373 var $parent = getParent($(this)) |
|
42374 var relatedTarget = { relatedTarget: this } |
|
42375 if (!$parent.hasClass('open')) return |
|
42376 $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) |
|
42377 if (e.isDefaultPrevented()) return |
|
42378 $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) |
|
42379 }) |
|
42380 } |
|
42381 |
|
42382 function getParent($this) { |
|
42383 var selector = $this.attr('data-target') |
|
42384 |
|
42385 if (!selector) { |
|
42386 selector = $this.attr('href') |
|
42387 selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 |
|
42388 } |
|
42389 |
|
42390 var $parent = selector && $(selector) |
|
42391 |
|
42392 return $parent && $parent.length ? $parent : $this.parent() |
|
42393 } |
|
42394 |
|
42395 |
|
42396 // DROPDOWN PLUGIN DEFINITION |
|
42397 // ========================== |
|
42398 |
|
42399 var old = $.fn.dropdown |
|
42400 |
|
42401 $.fn.dropdown = function (option) { |
|
42402 return this.each(function () { |
|
42403 var $this = $(this) |
|
42404 var data = $this.data('bs.dropdown') |
|
42405 |
|
42406 if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) |
|
42407 if (typeof option == 'string') data[option].call($this) |
|
42408 }) |
|
42409 } |
|
42410 |
|
42411 $.fn.dropdown.Constructor = Dropdown |
|
42412 |
|
42413 |
|
42414 // DROPDOWN NO CONFLICT |
|
42415 // ==================== |
|
42416 |
|
42417 $.fn.dropdown.noConflict = function () { |
|
42418 $.fn.dropdown = old |
|
42419 return this |
|
42420 } |
|
42421 |
|
42422 |
|
42423 // APPLY TO STANDARD DROPDOWN ELEMENTS |
|
42424 // =================================== |
|
42425 |
|
42426 $(document) |
|
42427 .on('click.bs.dropdown.data-api', clearMenus) |
|
42428 .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) |
|
42429 .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) |
|
42430 .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu], [role=listbox]', Dropdown.prototype.keydown) |
|
42431 |
|
42432 }(jQuery); |
|
42433 |
|
42434 /* ======================================================================== |
|
42435 * Bootstrap: modal.js v3.1.1 |
|
42436 * http://getbootstrap.com/javascript/#modals |
|
42437 * ======================================================================== |
|
42438 * Copyright 2011-2014 Twitter, Inc. |
|
42439 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
42440 * ======================================================================== */ |
|
42441 |
|
42442 |
|
42443 +function ($) { |
|
42444 'use strict'; |
|
42445 |
|
42446 // MODAL CLASS DEFINITION |
|
42447 // ====================== |
|
42448 |
|
42449 var Modal = function (element, options) { |
|
42450 this.options = options |
|
42451 this.$element = $(element) |
|
42452 this.$backdrop = |
|
42453 this.isShown = null |
|
42454 |
|
42455 if (this.options.remote) { |
|
42456 this.$element |
|
42457 .find('.modal-content') |
|
42458 .load(this.options.remote, $.proxy(function () { |
|
42459 this.$element.trigger('loaded.bs.modal') |
|
42460 }, this)) |
|
42461 } |
|
42462 } |
|
42463 |
|
42464 Modal.DEFAULTS = { |
|
42465 backdrop: true, |
|
42466 keyboard: true, |
|
42467 show: true |
|
42468 } |
|
42469 |
|
42470 Modal.prototype.toggle = function (_relatedTarget) { |
|
42471 return this[!this.isShown ? 'show' : 'hide'](_relatedTarget) |
|
42472 } |
|
42473 |
|
42474 Modal.prototype.show = function (_relatedTarget) { |
|
42475 var that = this |
|
42476 var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) |
|
42477 |
|
42478 this.$element.trigger(e) |
|
42479 |
|
42480 if (this.isShown || e.isDefaultPrevented()) return |
|
42481 |
|
42482 this.isShown = true |
|
42483 |
|
42484 this.escape() |
|
42485 |
|
42486 this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) |
|
42487 |
|
42488 this.backdrop(function () { |
|
42489 var transition = $.support.transition && that.$element.hasClass('fade') |
|
42490 |
|
42491 if (!that.$element.parent().length) { |
|
42492 that.$element.appendTo(document.body) // don't move modals dom position |
|
42493 } |
|
42494 |
|
42495 that.$element |
|
42496 .show() |
|
42497 .scrollTop(0) |
|
42498 |
|
42499 if (transition) { |
|
42500 that.$element[0].offsetWidth // force reflow |
|
42501 } |
|
42502 |
|
42503 that.$element |
|
42504 .addClass('in') |
|
42505 .attr('aria-hidden', false) |
|
42506 |
|
42507 that.enforceFocus() |
|
42508 |
|
42509 var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) |
|
42510 |
|
42511 transition ? |
|
42512 that.$element.find('.modal-dialog') // wait for modal to slide in |
|
42513 .one($.support.transition.end, function () { |
|
42514 that.$element.focus().trigger(e) |
|
42515 }) |
|
42516 .emulateTransitionEnd(300) : |
|
42517 that.$element.focus().trigger(e) |
|
42518 }) |
|
42519 } |
|
42520 |
|
42521 Modal.prototype.hide = function (e) { |
|
42522 if (e) e.preventDefault() |
|
42523 |
|
42524 e = $.Event('hide.bs.modal') |
|
42525 |
|
42526 this.$element.trigger(e) |
|
42527 |
|
42528 if (!this.isShown || e.isDefaultPrevented()) return |
|
42529 |
|
42530 this.isShown = false |
|
42531 |
|
42532 this.escape() |
|
42533 |
|
42534 $(document).off('focusin.bs.modal') |
|
42535 |
|
42536 this.$element |
|
42537 .removeClass('in') |
|
42538 .attr('aria-hidden', true) |
|
42539 .off('click.dismiss.bs.modal') |
|
42540 |
|
42541 $.support.transition && this.$element.hasClass('fade') ? |
|
42542 this.$element |
|
42543 .one($.support.transition.end, $.proxy(this.hideModal, this)) |
|
42544 .emulateTransitionEnd(300) : |
|
42545 this.hideModal() |
|
42546 } |
|
42547 |
|
42548 Modal.prototype.enforceFocus = function () { |
|
42549 $(document) |
|
42550 .off('focusin.bs.modal') // guard against infinite focus loop |
|
42551 .on('focusin.bs.modal', $.proxy(function (e) { |
|
42552 if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { |
|
42553 this.$element.focus() |
|
42554 } |
|
42555 }, this)) |
|
42556 } |
|
42557 |
|
42558 Modal.prototype.escape = function () { |
|
42559 if (this.isShown && this.options.keyboard) { |
|
42560 this.$element.on('keyup.dismiss.bs.modal', $.proxy(function (e) { |
|
42561 e.which == 27 && this.hide() |
|
42562 }, this)) |
|
42563 } else if (!this.isShown) { |
|
42564 this.$element.off('keyup.dismiss.bs.modal') |
|
42565 } |
|
42566 } |
|
42567 |
|
42568 Modal.prototype.hideModal = function () { |
|
42569 var that = this |
|
42570 this.$element.hide() |
|
42571 this.backdrop(function () { |
|
42572 that.removeBackdrop() |
|
42573 that.$element.trigger('hidden.bs.modal') |
|
42574 }) |
|
42575 } |
|
42576 |
|
42577 Modal.prototype.removeBackdrop = function () { |
|
42578 this.$backdrop && this.$backdrop.remove() |
|
42579 this.$backdrop = null |
|
42580 } |
|
42581 |
|
42582 Modal.prototype.backdrop = function (callback) { |
|
42583 var animate = this.$element.hasClass('fade') ? 'fade' : '' |
|
42584 |
|
42585 if (this.isShown && this.options.backdrop) { |
|
42586 var doAnimate = $.support.transition && animate |
|
42587 |
|
42588 this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') |
|
42589 .appendTo(document.body) |
|
42590 |
|
42591 this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { |
|
42592 if (e.target !== e.currentTarget) return |
|
42593 this.options.backdrop == 'static' |
|
42594 ? this.$element[0].focus.call(this.$element[0]) |
|
42595 : this.hide.call(this) |
|
42596 }, this)) |
|
42597 |
|
42598 if (doAnimate) this.$backdrop[0].offsetWidth // force reflow |
|
42599 |
|
42600 this.$backdrop.addClass('in') |
|
42601 |
|
42602 if (!callback) return |
|
42603 |
|
42604 doAnimate ? |
|
42605 this.$backdrop |
|
42606 .one($.support.transition.end, callback) |
|
42607 .emulateTransitionEnd(150) : |
|
42608 callback() |
|
42609 |
|
42610 } else if (!this.isShown && this.$backdrop) { |
|
42611 this.$backdrop.removeClass('in') |
|
42612 |
|
42613 $.support.transition && this.$element.hasClass('fade') ? |
|
42614 this.$backdrop |
|
42615 .one($.support.transition.end, callback) |
|
42616 .emulateTransitionEnd(150) : |
|
42617 callback() |
|
42618 |
|
42619 } else if (callback) { |
|
42620 callback() |
|
42621 } |
|
42622 } |
|
42623 |
|
42624 |
|
42625 // MODAL PLUGIN DEFINITION |
|
42626 // ======================= |
|
42627 |
|
42628 var old = $.fn.modal |
|
42629 |
|
42630 $.fn.modal = function (option, _relatedTarget) { |
|
42631 return this.each(function () { |
|
42632 var $this = $(this) |
|
42633 var data = $this.data('bs.modal') |
|
42634 var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) |
|
42635 |
|
42636 if (!data) $this.data('bs.modal', (data = new Modal(this, options))) |
|
42637 if (typeof option == 'string') data[option](_relatedTarget) |
|
42638 else if (options.show) data.show(_relatedTarget) |
|
42639 }) |
|
42640 } |
|
42641 |
|
42642 $.fn.modal.Constructor = Modal |
|
42643 |
|
42644 |
|
42645 // MODAL NO CONFLICT |
|
42646 // ================= |
|
42647 |
|
42648 $.fn.modal.noConflict = function () { |
|
42649 $.fn.modal = old |
|
42650 return this |
|
42651 } |
|
42652 |
|
42653 |
|
42654 // MODAL DATA-API |
|
42655 // ============== |
|
42656 |
|
42657 $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { |
|
42658 var $this = $(this) |
|
42659 var href = $this.attr('href') |
|
42660 var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7 |
|
42661 var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) |
|
42662 |
|
42663 if ($this.is('a')) e.preventDefault() |
|
42664 |
|
42665 $target |
|
42666 .modal(option, this) |
|
42667 .one('hide', function () { |
|
42668 $this.is(':visible') && $this.focus() |
|
42669 }) |
|
42670 }) |
|
42671 |
|
42672 $(document) |
|
42673 .on('show.bs.modal', '.modal', function () { $(document.body).addClass('modal-open') }) |
|
42674 .on('hidden.bs.modal', '.modal', function () { $(document.body).removeClass('modal-open') }) |
|
42675 |
|
42676 }(jQuery); |
|
42677 |
|
42678 /* ======================================================================== |
|
42679 * Bootstrap: tooltip.js v3.1.1 |
|
42680 * http://getbootstrap.com/javascript/#tooltip |
|
42681 * Inspired by the original jQuery.tipsy by Jason Frame |
|
42682 * ======================================================================== |
|
42683 * Copyright 2011-2014 Twitter, Inc. |
|
42684 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
42685 * ======================================================================== */ |
|
42686 |
|
42687 |
|
42688 +function ($) { |
|
42689 'use strict'; |
|
42690 |
|
42691 // TOOLTIP PUBLIC CLASS DEFINITION |
|
42692 // =============================== |
|
42693 |
|
42694 var Tooltip = function (element, options) { |
|
42695 this.type = |
|
42696 this.options = |
|
42697 this.enabled = |
|
42698 this.timeout = |
|
42699 this.hoverState = |
|
42700 this.$element = null |
|
42701 |
|
42702 this.init('tooltip', element, options) |
|
42703 } |
|
42704 |
|
42705 Tooltip.DEFAULTS = { |
|
42706 animation: true, |
|
42707 placement: 'top', |
|
42708 selector: false, |
|
42709 template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', |
|
42710 trigger: 'hover focus', |
|
42711 title: '', |
|
42712 delay: 0, |
|
42713 html: false, |
|
42714 container: false |
|
42715 } |
|
42716 |
|
42717 Tooltip.prototype.init = function (type, element, options) { |
|
42718 this.enabled = true |
|
42719 this.type = type |
|
42720 this.$element = $(element) |
|
42721 this.options = this.getOptions(options) |
|
42722 |
|
42723 var triggers = this.options.trigger.split(' ') |
|
42724 |
|
42725 for (var i = triggers.length; i--;) { |
|
42726 var trigger = triggers[i] |
|
42727 |
|
42728 if (trigger == 'click') { |
|
42729 this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) |
|
42730 } else if (trigger != 'manual') { |
|
42731 var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' |
|
42732 var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' |
|
42733 |
|
42734 this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) |
|
42735 this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) |
|
42736 } |
|
42737 } |
|
42738 |
|
42739 this.options.selector ? |
|
42740 (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : |
|
42741 this.fixTitle() |
|
42742 } |
|
42743 |
|
42744 Tooltip.prototype.getDefaults = function () { |
|
42745 return Tooltip.DEFAULTS |
|
42746 } |
|
42747 |
|
42748 Tooltip.prototype.getOptions = function (options) { |
|
42749 options = $.extend({}, this.getDefaults(), this.$element.data(), options) |
|
42750 |
|
42751 if (options.delay && typeof options.delay == 'number') { |
|
42752 options.delay = { |
|
42753 show: options.delay, |
|
42754 hide: options.delay |
|
42755 } |
|
42756 } |
|
42757 |
|
42758 return options |
|
42759 } |
|
42760 |
|
42761 Tooltip.prototype.getDelegateOptions = function () { |
|
42762 var options = {} |
|
42763 var defaults = this.getDefaults() |
|
42764 |
|
42765 this._options && $.each(this._options, function (key, value) { |
|
42766 if (defaults[key] != value) options[key] = value |
|
42767 }) |
|
42768 |
|
42769 return options |
|
42770 } |
|
42771 |
|
42772 Tooltip.prototype.enter = function (obj) { |
|
42773 var self = obj instanceof this.constructor ? |
|
42774 obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) |
|
42775 |
|
42776 clearTimeout(self.timeout) |
|
42777 |
|
42778 self.hoverState = 'in' |
|
42779 |
|
42780 if (!self.options.delay || !self.options.delay.show) return self.show() |
|
42781 |
|
42782 self.timeout = setTimeout(function () { |
|
42783 if (self.hoverState == 'in') self.show() |
|
42784 }, self.options.delay.show) |
|
42785 } |
|
42786 |
|
42787 Tooltip.prototype.leave = function (obj) { |
|
42788 var self = obj instanceof this.constructor ? |
|
42789 obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) |
|
42790 |
|
42791 clearTimeout(self.timeout) |
|
42792 |
|
42793 self.hoverState = 'out' |
|
42794 |
|
42795 if (!self.options.delay || !self.options.delay.hide) return self.hide() |
|
42796 |
|
42797 self.timeout = setTimeout(function () { |
|
42798 if (self.hoverState == 'out') self.hide() |
|
42799 }, self.options.delay.hide) |
|
42800 } |
|
42801 |
|
42802 Tooltip.prototype.show = function () { |
|
42803 var e = $.Event('show.bs.' + this.type) |
|
42804 |
|
42805 if (this.hasContent() && this.enabled) { |
|
42806 this.$element.trigger(e) |
|
42807 |
|
42808 if (e.isDefaultPrevented()) return |
|
42809 var that = this; |
|
42810 |
|
42811 var $tip = this.tip() |
|
42812 |
|
42813 this.setContent() |
|
42814 |
|
42815 if (this.options.animation) $tip.addClass('fade') |
|
42816 |
|
42817 var placement = typeof this.options.placement == 'function' ? |
|
42818 this.options.placement.call(this, $tip[0], this.$element[0]) : |
|
42819 this.options.placement |
|
42820 |
|
42821 var autoToken = /\s?auto?\s?/i |
|
42822 var autoPlace = autoToken.test(placement) |
|
42823 if (autoPlace) placement = placement.replace(autoToken, '') || 'top' |
|
42824 |
|
42825 $tip |
|
42826 .detach() |
|
42827 .css({ top: 0, left: 0, display: 'block' }) |
|
42828 .addClass(placement) |
|
42829 |
|
42830 this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) |
|
42831 |
|
42832 var pos = this.getPosition() |
|
42833 var actualWidth = $tip[0].offsetWidth |
|
42834 var actualHeight = $tip[0].offsetHeight |
|
42835 |
|
42836 if (autoPlace) { |
|
42837 var $parent = this.$element.parent() |
|
42838 |
|
42839 var orgPlacement = placement |
|
42840 var docScroll = document.documentElement.scrollTop || document.body.scrollTop |
|
42841 var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth() |
|
42842 var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight() |
|
42843 var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left |
|
42844 |
|
42845 placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' : |
|
42846 placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' : |
|
42847 placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' : |
|
42848 placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' : |
|
42849 placement |
|
42850 |
|
42851 $tip |
|
42852 .removeClass(orgPlacement) |
|
42853 .addClass(placement) |
|
42854 } |
|
42855 |
|
42856 var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) |
|
42857 |
|
42858 this.applyPlacement(calculatedOffset, placement) |
|
42859 this.hoverState = null |
|
42860 |
|
42861 var complete = function() { |
|
42862 that.$element.trigger('shown.bs.' + that.type) |
|
42863 } |
|
42864 |
|
42865 $.support.transition && this.$tip.hasClass('fade') ? |
|
42866 $tip |
|
42867 .one($.support.transition.end, complete) |
|
42868 .emulateTransitionEnd(150) : |
|
42869 complete() |
|
42870 } |
|
42871 } |
|
42872 |
|
42873 Tooltip.prototype.applyPlacement = function (offset, placement) { |
|
42874 var replace |
|
42875 var $tip = this.tip() |
|
42876 var width = $tip[0].offsetWidth |
|
42877 var height = $tip[0].offsetHeight |
|
42878 |
|
42879 // manually read margins because getBoundingClientRect includes difference |
|
42880 var marginTop = parseInt($tip.css('margin-top'), 10) |
|
42881 var marginLeft = parseInt($tip.css('margin-left'), 10) |
|
42882 |
|
42883 // we must check for NaN for ie 8/9 |
|
42884 if (isNaN(marginTop)) marginTop = 0 |
|
42885 if (isNaN(marginLeft)) marginLeft = 0 |
|
42886 |
|
42887 offset.top = offset.top + marginTop |
|
42888 offset.left = offset.left + marginLeft |
|
42889 |
|
42890 // $.fn.offset doesn't round pixel values |
|
42891 // so we use setOffset directly with our own function B-0 |
|
42892 $.offset.setOffset($tip[0], $.extend({ |
|
42893 using: function (props) { |
|
42894 $tip.css({ |
|
42895 top: Math.round(props.top), |
|
42896 left: Math.round(props.left) |
|
42897 }) |
|
42898 } |
|
42899 }, offset), 0) |
|
42900 |
|
42901 $tip.addClass('in') |
|
42902 |
|
42903 // check to see if placing tip in new offset caused the tip to resize itself |
|
42904 var actualWidth = $tip[0].offsetWidth |
|
42905 var actualHeight = $tip[0].offsetHeight |
|
42906 |
|
42907 if (placement == 'top' && actualHeight != height) { |
|
42908 replace = true |
|
42909 offset.top = offset.top + height - actualHeight |
|
42910 } |
|
42911 |
|
42912 if (/bottom|top/.test(placement)) { |
|
42913 var delta = 0 |
|
42914 |
|
42915 if (offset.left < 0) { |
|
42916 delta = offset.left * -2 |
|
42917 offset.left = 0 |
|
42918 |
|
42919 $tip.offset(offset) |
|
42920 |
|
42921 actualWidth = $tip[0].offsetWidth |
|
42922 actualHeight = $tip[0].offsetHeight |
|
42923 } |
|
42924 |
|
42925 this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') |
|
42926 } else { |
|
42927 this.replaceArrow(actualHeight - height, actualHeight, 'top') |
|
42928 } |
|
42929 |
|
42930 if (replace) $tip.offset(offset) |
|
42931 } |
|
42932 |
|
42933 Tooltip.prototype.replaceArrow = function (delta, dimension, position) { |
|
42934 this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '') |
|
42935 } |
|
42936 |
|
42937 Tooltip.prototype.setContent = function () { |
|
42938 var $tip = this.tip() |
|
42939 var title = this.getTitle() |
|
42940 |
|
42941 $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) |
|
42942 $tip.removeClass('fade in top bottom left right') |
|
42943 } |
|
42944 |
|
42945 Tooltip.prototype.hide = function () { |
|
42946 var that = this |
|
42947 var $tip = this.tip() |
|
42948 var e = $.Event('hide.bs.' + this.type) |
|
42949 |
|
42950 function complete() { |
|
42951 if (that.hoverState != 'in') $tip.detach() |
|
42952 that.$element.trigger('hidden.bs.' + that.type) |
|
42953 } |
|
42954 |
|
42955 this.$element.trigger(e) |
|
42956 |
|
42957 if (e.isDefaultPrevented()) return |
|
42958 |
|
42959 $tip.removeClass('in') |
|
42960 |
|
42961 $.support.transition && this.$tip.hasClass('fade') ? |
|
42962 $tip |
|
42963 .one($.support.transition.end, complete) |
|
42964 .emulateTransitionEnd(150) : |
|
42965 complete() |
|
42966 |
|
42967 this.hoverState = null |
|
42968 |
|
42969 return this |
|
42970 } |
|
42971 |
|
42972 Tooltip.prototype.fixTitle = function () { |
|
42973 var $e = this.$element |
|
42974 if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { |
|
42975 $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') |
|
42976 } |
|
42977 } |
|
42978 |
|
42979 Tooltip.prototype.hasContent = function () { |
|
42980 return this.getTitle() |
|
42981 } |
|
42982 |
|
42983 Tooltip.prototype.getPosition = function () { |
|
42984 var el = this.$element[0] |
|
42985 return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { |
|
42986 width: el.offsetWidth, |
|
42987 height: el.offsetHeight |
|
42988 }, this.$element.offset()) |
|
42989 } |
|
42990 |
|
42991 Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { |
|
42992 return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : |
|
42993 placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : |
|
42994 placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : |
|
42995 /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } |
|
42996 } |
|
42997 |
|
42998 Tooltip.prototype.getTitle = function () { |
|
42999 var title |
|
43000 var $e = this.$element |
|
43001 var o = this.options |
|
43002 |
|
43003 title = $e.attr('data-original-title') |
|
43004 || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) |
|
43005 |
|
43006 return title |
|
43007 } |
|
43008 |
|
43009 Tooltip.prototype.tip = function () { |
|
43010 return this.$tip = this.$tip || $(this.options.template) |
|
43011 } |
|
43012 |
|
43013 Tooltip.prototype.arrow = function () { |
|
43014 return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow') |
|
43015 } |
|
43016 |
|
43017 Tooltip.prototype.validate = function () { |
|
43018 if (!this.$element[0].parentNode) { |
|
43019 this.hide() |
|
43020 this.$element = null |
|
43021 this.options = null |
|
43022 } |
|
43023 } |
|
43024 |
|
43025 Tooltip.prototype.enable = function () { |
|
43026 this.enabled = true |
|
43027 } |
|
43028 |
|
43029 Tooltip.prototype.disable = function () { |
|
43030 this.enabled = false |
|
43031 } |
|
43032 |
|
43033 Tooltip.prototype.toggleEnabled = function () { |
|
43034 this.enabled = !this.enabled |
|
43035 } |
|
43036 |
|
43037 Tooltip.prototype.toggle = function (e) { |
|
43038 var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this |
|
43039 self.tip().hasClass('in') ? self.leave(self) : self.enter(self) |
|
43040 } |
|
43041 |
|
43042 Tooltip.prototype.destroy = function () { |
|
43043 clearTimeout(this.timeout) |
|
43044 this.hide().$element.off('.' + this.type).removeData('bs.' + this.type) |
|
43045 } |
|
43046 |
|
43047 |
|
43048 // TOOLTIP PLUGIN DEFINITION |
|
43049 // ========================= |
|
43050 |
|
43051 var old = $.fn.tooltip |
|
43052 |
|
43053 $.fn.tooltip = function (option) { |
|
43054 return this.each(function () { |
|
43055 var $this = $(this) |
|
43056 var data = $this.data('bs.tooltip') |
|
43057 var options = typeof option == 'object' && option |
|
43058 |
|
43059 if (!data && option == 'destroy') return |
|
43060 if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) |
|
43061 if (typeof option == 'string') data[option]() |
|
43062 }) |
|
43063 } |
|
43064 |
|
43065 $.fn.tooltip.Constructor = Tooltip |
|
43066 |
|
43067 |
|
43068 // TOOLTIP NO CONFLICT |
|
43069 // =================== |
|
43070 |
|
43071 $.fn.tooltip.noConflict = function () { |
|
43072 $.fn.tooltip = old |
|
43073 return this |
|
43074 } |
|
43075 |
|
43076 }(jQuery); |
|
43077 |
|
43078 /* ======================================================================== |
|
43079 * Bootstrap: popover.js v3.1.1 |
|
43080 * http://getbootstrap.com/javascript/#popovers |
|
43081 * ======================================================================== |
|
43082 * Copyright 2011-2014 Twitter, Inc. |
|
43083 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
43084 * ======================================================================== */ |
|
43085 |
|
43086 |
|
43087 +function ($) { |
|
43088 'use strict'; |
|
43089 |
|
43090 // POPOVER PUBLIC CLASS DEFINITION |
|
43091 // =============================== |
|
43092 |
|
43093 var Popover = function (element, options) { |
|
43094 this.init('popover', element, options) |
|
43095 } |
|
43096 |
|
43097 if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') |
|
43098 |
|
43099 Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { |
|
43100 placement: 'right', |
|
43101 trigger: 'click', |
|
43102 content: '', |
|
43103 template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' |
|
43104 }) |
|
43105 |
|
43106 |
|
43107 // NOTE: POPOVER EXTENDS tooltip.js |
|
43108 // ================================ |
|
43109 |
|
43110 Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) |
|
43111 |
|
43112 Popover.prototype.constructor = Popover |
|
43113 |
|
43114 Popover.prototype.getDefaults = function () { |
|
43115 return Popover.DEFAULTS |
|
43116 } |
|
43117 |
|
43118 Popover.prototype.setContent = function () { |
|
43119 var $tip = this.tip() |
|
43120 var title = this.getTitle() |
|
43121 var content = this.getContent() |
|
43122 |
|
43123 $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) |
|
43124 $tip.find('.popover-content')[ // we use append for html objects to maintain js events |
|
43125 this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' |
|
43126 ](content) |
|
43127 |
|
43128 $tip.removeClass('fade top bottom left right in') |
|
43129 |
|
43130 // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do |
|
43131 // this manually by checking the contents. |
|
43132 if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() |
|
43133 } |
|
43134 |
|
43135 Popover.prototype.hasContent = function () { |
|
43136 return this.getTitle() || this.getContent() |
|
43137 } |
|
43138 |
|
43139 Popover.prototype.getContent = function () { |
|
43140 var $e = this.$element |
|
43141 var o = this.options |
|
43142 |
|
43143 return $e.attr('data-content') |
|
43144 || (typeof o.content == 'function' ? |
|
43145 o.content.call($e[0]) : |
|
43146 o.content) |
|
43147 } |
|
43148 |
|
43149 Popover.prototype.arrow = function () { |
|
43150 return this.$arrow = this.$arrow || this.tip().find('.arrow') |
|
43151 } |
|
43152 |
|
43153 Popover.prototype.tip = function () { |
|
43154 if (!this.$tip) this.$tip = $(this.options.template) |
|
43155 return this.$tip |
|
43156 } |
|
43157 |
|
43158 |
|
43159 // POPOVER PLUGIN DEFINITION |
|
43160 // ========================= |
|
43161 |
|
43162 var old = $.fn.popover |
|
43163 |
|
43164 $.fn.popover = function (option) { |
|
43165 return this.each(function () { |
|
43166 var $this = $(this) |
|
43167 var data = $this.data('bs.popover') |
|
43168 var options = typeof option == 'object' && option |
|
43169 |
|
43170 if (!data && option == 'destroy') return |
|
43171 if (!data) $this.data('bs.popover', (data = new Popover(this, options))) |
|
43172 if (typeof option == 'string') data[option]() |
|
43173 }) |
|
43174 } |
|
43175 |
|
43176 $.fn.popover.Constructor = Popover |
|
43177 |
|
43178 |
|
43179 // POPOVER NO CONFLICT |
|
43180 // =================== |
|
43181 |
|
43182 $.fn.popover.noConflict = function () { |
|
43183 $.fn.popover = old |
|
43184 return this |
|
43185 } |
|
43186 |
|
43187 }(jQuery); |
|
43188 |
|
43189 /* ======================================================================== |
|
43190 * Bootstrap: scrollspy.js v3.1.1 |
|
43191 * http://getbootstrap.com/javascript/#scrollspy |
|
43192 * ======================================================================== |
|
43193 * Copyright 2011-2014 Twitter, Inc. |
|
43194 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
43195 * ======================================================================== */ |
|
43196 |
|
43197 |
|
43198 +function ($) { |
|
43199 'use strict'; |
|
43200 |
|
43201 // SCROLLSPY CLASS DEFINITION |
|
43202 // ========================== |
|
43203 |
|
43204 function ScrollSpy(element, options) { |
|
43205 var href |
|
43206 var process = $.proxy(this.process, this) |
|
43207 |
|
43208 this.$element = $(element).is('body') ? $(window) : $(element) |
|
43209 this.$body = $('body') |
|
43210 this.$scrollElement = this.$element.on('scroll.bs.scroll-spy.data-api', process) |
|
43211 this.options = $.extend({}, ScrollSpy.DEFAULTS, options) |
|
43212 this.selector = (this.options.target |
|
43213 || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 |
|
43214 || '') + ' .nav li > a' |
|
43215 this.offsets = $([]) |
|
43216 this.targets = $([]) |
|
43217 this.activeTarget = null |
|
43218 |
|
43219 this.refresh() |
|
43220 this.process() |
|
43221 } |
|
43222 |
|
43223 ScrollSpy.DEFAULTS = { |
|
43224 offset: 10 |
|
43225 } |
|
43226 |
|
43227 ScrollSpy.prototype.refresh = function () { |
|
43228 var offsetMethod = this.$element[0] == window ? 'offset' : 'position' |
|
43229 |
|
43230 this.offsets = $([]) |
|
43231 this.targets = $([]) |
|
43232 |
|
43233 var self = this |
|
43234 var $targets = this.$body |
|
43235 .find(this.selector) |
|
43236 .map(function () { |
|
43237 var $el = $(this) |
|
43238 var href = $el.data('target') || $el.attr('href') |
|
43239 var $href = /^#./.test(href) && $(href) |
|
43240 |
|
43241 return ($href |
|
43242 && $href.length |
|
43243 && $href.is(':visible') |
|
43244 && [[ $href[offsetMethod]().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]]) || null |
|
43245 }) |
|
43246 .sort(function (a, b) { return a[0] - b[0] }) |
|
43247 .each(function () { |
|
43248 self.offsets.push(this[0]) |
|
43249 self.targets.push(this[1]) |
|
43250 }) |
|
43251 } |
|
43252 |
|
43253 ScrollSpy.prototype.process = function () { |
|
43254 var scrollTop = this.$scrollElement.scrollTop() + this.options.offset |
|
43255 var scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight |
|
43256 var maxScroll = scrollHeight - this.$scrollElement.height() |
|
43257 var offsets = this.offsets |
|
43258 var targets = this.targets |
|
43259 var activeTarget = this.activeTarget |
|
43260 var i |
|
43261 |
|
43262 if (scrollTop >= maxScroll) { |
|
43263 return activeTarget != (i = targets.last()[0]) && this.activate(i) |
|
43264 } |
|
43265 |
|
43266 if (activeTarget && scrollTop <= offsets[0]) { |
|
43267 return activeTarget != (i = targets[0]) && this.activate(i) |
|
43268 } |
|
43269 |
|
43270 for (i = offsets.length; i--;) { |
|
43271 activeTarget != targets[i] |
|
43272 && scrollTop >= offsets[i] |
|
43273 && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) |
|
43274 && this.activate( targets[i] ) |
|
43275 } |
|
43276 } |
|
43277 |
|
43278 ScrollSpy.prototype.activate = function (target) { |
|
43279 this.activeTarget = target |
|
43280 |
|
43281 $(this.selector) |
|
43282 .parentsUntil(this.options.target, '.active') |
|
43283 .removeClass('active') |
|
43284 |
|
43285 var selector = this.selector + |
|
43286 '[data-target="' + target + '"],' + |
|
43287 this.selector + '[href="' + target + '"]' |
|
43288 |
|
43289 var active = $(selector) |
|
43290 .parents('li') |
|
43291 .addClass('active') |
|
43292 |
|
43293 if (active.parent('.dropdown-menu').length) { |
|
43294 active = active |
|
43295 .closest('li.dropdown') |
|
43296 .addClass('active') |
|
43297 } |
|
43298 |
|
43299 active.trigger('activate.bs.scrollspy') |
|
43300 } |
|
43301 |
|
43302 |
|
43303 // SCROLLSPY PLUGIN DEFINITION |
|
43304 // =========================== |
|
43305 |
|
43306 var old = $.fn.scrollspy |
|
43307 |
|
43308 $.fn.scrollspy = function (option) { |
|
43309 return this.each(function () { |
|
43310 var $this = $(this) |
|
43311 var data = $this.data('bs.scrollspy') |
|
43312 var options = typeof option == 'object' && option |
|
43313 |
|
43314 if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) |
|
43315 if (typeof option == 'string') data[option]() |
|
43316 }) |
|
43317 } |
|
43318 |
|
43319 $.fn.scrollspy.Constructor = ScrollSpy |
|
43320 |
|
43321 |
|
43322 // SCROLLSPY NO CONFLICT |
|
43323 // ===================== |
|
43324 |
|
43325 $.fn.scrollspy.noConflict = function () { |
|
43326 $.fn.scrollspy = old |
|
43327 return this |
|
43328 } |
|
43329 |
|
43330 |
|
43331 // SCROLLSPY DATA-API |
|
43332 // ================== |
|
43333 |
|
43334 $(window).on('load', function () { |
|
43335 $('[data-spy="scroll"]').each(function () { |
|
43336 var $spy = $(this) |
|
43337 $spy.scrollspy($spy.data()) |
|
43338 }) |
|
43339 }) |
|
43340 |
|
43341 }(jQuery); |
|
43342 |
|
43343 /* ======================================================================== |
|
43344 * Bootstrap: tab.js v3.1.1 |
|
43345 * http://getbootstrap.com/javascript/#tabs |
|
43346 * ======================================================================== |
|
43347 * Copyright 2011-2014 Twitter, Inc. |
|
43348 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
43349 * ======================================================================== */ |
|
43350 |
|
43351 |
|
43352 +function ($) { |
|
43353 'use strict'; |
|
43354 |
|
43355 // TAB CLASS DEFINITION |
|
43356 // ==================== |
|
43357 |
|
43358 var Tab = function (element) { |
|
43359 this.element = $(element) |
|
43360 } |
|
43361 |
|
43362 Tab.prototype.show = function () { |
|
43363 var $this = this.element |
|
43364 var $ul = $this.closest('ul:not(.dropdown-menu)') |
|
43365 var selector = $this.data('target') |
|
43366 |
|
43367 if (!selector) { |
|
43368 selector = $this.attr('href') |
|
43369 selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 |
|
43370 } |
|
43371 |
|
43372 if ($this.parent('li').hasClass('active')) return |
|
43373 |
|
43374 var previous = $ul.find('.active:last a')[0] |
|
43375 var e = $.Event('show.bs.tab', { |
|
43376 relatedTarget: previous |
|
43377 }) |
|
43378 |
|
43379 $this.trigger(e) |
|
43380 |
|
43381 if (e.isDefaultPrevented()) return |
|
43382 |
|
43383 var $target = $(selector) |
|
43384 |
|
43385 this.activate($this.parent('li'), $ul) |
|
43386 this.activate($target, $target.parent(), function () { |
|
43387 $this.trigger({ |
|
43388 type: 'shown.bs.tab', |
|
43389 relatedTarget: previous |
|
43390 }) |
|
43391 }) |
|
43392 } |
|
43393 |
|
43394 Tab.prototype.activate = function (element, container, callback) { |
|
43395 var $active = container.find('> .active') |
|
43396 var transition = callback |
|
43397 && $.support.transition |
|
43398 && $active.hasClass('fade') |
|
43399 |
|
43400 function next() { |
|
43401 $active |
|
43402 .removeClass('active') |
|
43403 .find('> .dropdown-menu > .active') |
|
43404 .removeClass('active') |
|
43405 |
|
43406 element.addClass('active') |
|
43407 |
|
43408 if (transition) { |
|
43409 element[0].offsetWidth // reflow for transition |
|
43410 element.addClass('in') |
|
43411 } else { |
|
43412 element.removeClass('fade') |
|
43413 } |
|
43414 |
|
43415 if (element.parent('.dropdown-menu')) { |
|
43416 element.closest('li.dropdown').addClass('active') |
|
43417 } |
|
43418 |
|
43419 callback && callback() |
|
43420 } |
|
43421 |
|
43422 transition ? |
|
43423 $active |
|
43424 .one($.support.transition.end, next) |
|
43425 .emulateTransitionEnd(150) : |
|
43426 next() |
|
43427 |
|
43428 $active.removeClass('in') |
|
43429 } |
|
43430 |
|
43431 |
|
43432 // TAB PLUGIN DEFINITION |
|
43433 // ===================== |
|
43434 |
|
43435 var old = $.fn.tab |
|
43436 |
|
43437 $.fn.tab = function ( option ) { |
|
43438 return this.each(function () { |
|
43439 var $this = $(this) |
|
43440 var data = $this.data('bs.tab') |
|
43441 |
|
43442 if (!data) $this.data('bs.tab', (data = new Tab(this))) |
|
43443 if (typeof option == 'string') data[option]() |
|
43444 }) |
|
43445 } |
|
43446 |
|
43447 $.fn.tab.Constructor = Tab |
|
43448 |
|
43449 |
|
43450 // TAB NO CONFLICT |
|
43451 // =============== |
|
43452 |
|
43453 $.fn.tab.noConflict = function () { |
|
43454 $.fn.tab = old |
|
43455 return this |
|
43456 } |
|
43457 |
|
43458 |
|
43459 // TAB DATA-API |
|
43460 // ============ |
|
43461 |
|
43462 $(document).on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { |
|
43463 e.preventDefault() |
|
43464 $(this).tab('show') |
|
43465 }) |
|
43466 |
|
43467 }(jQuery); |
|
43468 |
|
43469 /* ======================================================================== |
|
43470 * Bootstrap: affix.js v3.1.1 |
|
43471 * http://getbootstrap.com/javascript/#affix |
|
43472 * ======================================================================== |
|
43473 * Copyright 2011-2014 Twitter, Inc. |
|
43474 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) |
|
43475 * ======================================================================== */ |
|
43476 |
|
43477 |
|
43478 +function ($) { |
|
43479 'use strict'; |
|
43480 |
|
43481 // AFFIX CLASS DEFINITION |
|
43482 // ====================== |
|
43483 |
|
43484 var Affix = function (element, options) { |
|
43485 this.options = $.extend({}, Affix.DEFAULTS, options) |
|
43486 this.$window = $(window) |
|
43487 .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) |
|
43488 .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) |
|
43489 |
|
43490 this.$element = $(element) |
|
43491 this.affixed = |
|
43492 this.unpin = |
|
43493 this.pinnedOffset = null |
|
43494 |
|
43495 this.checkPosition() |
|
43496 } |
|
43497 |
|
43498 Affix.RESET = 'affix affix-top affix-bottom' |
|
43499 |
|
43500 Affix.DEFAULTS = { |
|
43501 offset: 0 |
|
43502 } |
|
43503 |
|
43504 Affix.prototype.getPinnedOffset = function () { |
|
43505 if (this.pinnedOffset) return this.pinnedOffset |
|
43506 this.$element.removeClass(Affix.RESET).addClass('affix') |
|
43507 var scrollTop = this.$window.scrollTop() |
|
43508 var position = this.$element.offset() |
|
43509 return (this.pinnedOffset = position.top - scrollTop) |
|
43510 } |
|
43511 |
|
43512 Affix.prototype.checkPositionWithEventLoop = function () { |
|
43513 setTimeout($.proxy(this.checkPosition, this), 1) |
|
43514 } |
|
43515 |
|
43516 Affix.prototype.checkPosition = function () { |
|
43517 if (!this.$element.is(':visible')) return |
|
43518 |
|
43519 var scrollHeight = $(document).height() |
|
43520 var scrollTop = this.$window.scrollTop() |
|
43521 var position = this.$element.offset() |
|
43522 var offset = this.options.offset |
|
43523 var offsetTop = offset.top |
|
43524 var offsetBottom = offset.bottom |
|
43525 |
|
43526 if (this.affixed == 'top') position.top += scrollTop |
|
43527 |
|
43528 if (typeof offset != 'object') offsetBottom = offsetTop = offset |
|
43529 if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) |
|
43530 if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) |
|
43531 |
|
43532 var affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? false : |
|
43533 offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' : |
|
43534 offsetTop != null && (scrollTop <= offsetTop) ? 'top' : false |
|
43535 |
|
43536 if (this.affixed === affix) return |
|
43537 if (this.unpin) this.$element.css('top', '') |
|
43538 |
|
43539 var affixType = 'affix' + (affix ? '-' + affix : '') |
|
43540 var e = $.Event(affixType + '.bs.affix') |
|
43541 |
|
43542 this.$element.trigger(e) |
|
43543 |
|
43544 if (e.isDefaultPrevented()) return |
|
43545 |
|
43546 this.affixed = affix |
|
43547 this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null |
|
43548 |
|
43549 this.$element |
|
43550 .removeClass(Affix.RESET) |
|
43551 .addClass(affixType) |
|
43552 .trigger($.Event(affixType.replace('affix', 'affixed'))) |
|
43553 |
|
43554 if (affix == 'bottom') { |
|
43555 this.$element.offset({ top: scrollHeight - offsetBottom - this.$element.height() }) |
|
43556 } |
|
43557 } |
|
43558 |
|
43559 |
|
43560 // AFFIX PLUGIN DEFINITION |
|
43561 // ======================= |
|
43562 |
|
43563 var old = $.fn.affix |
|
43564 |
|
43565 $.fn.affix = function (option) { |
|
43566 return this.each(function () { |
|
43567 var $this = $(this) |
|
43568 var data = $this.data('bs.affix') |
|
43569 var options = typeof option == 'object' && option |
|
43570 |
|
43571 if (!data) $this.data('bs.affix', (data = new Affix(this, options))) |
|
43572 if (typeof option == 'string') data[option]() |
|
43573 }) |
|
43574 } |
|
43575 |
|
43576 $.fn.affix.Constructor = Affix |
|
43577 |
|
43578 |
|
43579 // AFFIX NO CONFLICT |
|
43580 // ================= |
|
43581 |
|
43582 $.fn.affix.noConflict = function () { |
|
43583 $.fn.affix = old |
|
43584 return this |
|
43585 } |
|
43586 |
|
43587 |
|
43588 // AFFIX DATA-API |
|
43589 // ============== |
|
43590 |
|
43591 $(window).on('load', function () { |
|
43592 $('[data-spy="affix"]').each(function () { |
|
43593 var $spy = $(this) |
|
43594 var data = $spy.data() |
|
43595 |
|
43596 data.offset = data.offset || {} |
|
43597 |
|
43598 if (data.offsetBottom) data.offset.bottom = data.offsetBottom |
|
43599 if (data.offsetTop) data.offset.top = data.offsetTop |
|
43600 |
|
43601 $spy.affix(data) |
|
43602 }) |
|
43603 }) |
|
43604 |
|
43605 }(jQuery); |