1 /** 2 * @fileOverview lang 3 * @author lifesinger@gmail.com, yiminghe@gmail.com 4 * @description this code can run in any ecmascript compliant environment 5 */ 6 (function (S, undefined) { 7 8 function hasOwnProperty(o, p) { 9 return Object.prototype.hasOwnProperty.call(o, p); 10 } 11 12 var TRUE = true, 13 FALSE = false, 14 OP = Object.prototype, 15 toString = OP.toString, 16 AP = Array.prototype, 17 indexOf = AP.indexOf, 18 lastIndexOf = AP.lastIndexOf, 19 filter = AP.filter, 20 every = AP.every, 21 some = AP.some, 22 //reduce = AP.reduce, 23 trim = String.prototype.trim, 24 map = AP.map, 25 EMPTY = '', 26 HEX_BASE = 16, 27 CLONE_MARKER = '__~ks_cloned', 28 COMPARE_MARKER = '__~ks_compared', 29 STAMP_MARKER = '__~ks_stamped', 30 // IE doesn't include non-breaking-space (0xa0) in their \s character 31 // class (as required by section 7.2 of the ECMAScript spec), we explicitly 32 // include it in the regexp to enforce consistent cross-browser behavior. 33 RE_TRIM = /^[\s\xa0]+|[\s\xa0]+$/g, 34 encode = encodeURIComponent, 35 decode = decodeURIComponent, 36 SEP = '&', 37 EQ = '=', 38 // [[Class]] -> type pairs 39 class2type = {}, 40 // http://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet 41 htmlEntities = { 42 '&':'&', 43 '>':'>', 44 '<':'<', 45 '`':'`', 46 '/':'/', 47 '"':'"', 48 ''':"'" 49 }, 50 reverseEntities = {}, 51 escapeReg, 52 unEscapeReg, 53 // - # $ ^ * ( ) + [ ] { } | \ , . ? 54 escapeRegExp = /[\-#$\^*()+\[\]{}|\\,.?\s]/g; 55 (function () { 56 for (var k in htmlEntities) { 57 if (hasOwnProperty(htmlEntities, k)) { 58 reverseEntities[htmlEntities[k]] = k; 59 } 60 } 61 })(); 62 63 function getEscapeReg() { 64 if (escapeReg) { 65 return escapeReg 66 } 67 var str = EMPTY; 68 S.each(htmlEntities, function (entity) { 69 str += entity + '|'; 70 }); 71 str = str.slice(0, -1); 72 return escapeReg = new RegExp(str, "g"); 73 } 74 75 function getUnEscapeReg() { 76 if (unEscapeReg) { 77 return unEscapeReg 78 } 79 var str = EMPTY; 80 S.each(reverseEntities, function (entity) { 81 str += entity + '|'; 82 }); 83 str += '(\\d{1,5});'; 84 return unEscapeReg = new RegExp(str, "g"); 85 } 86 87 88 function isValidParamValue(val) { 89 var t = typeof val; 90 // If the type of val is null, undefined, number, string, boolean, return true. 91 return val == null || (t !== 'object' && t !== 'function'); 92 } 93 94 S.mix(S, 95 /** 96 * @lends KISSY 97 */ 98 { 99 100 /** 101 * stamp a object by guid 102 * @param {Object} o object needed to be stamped 103 * @param {Boolean} [readOnly] while set marker on o if marker does not exist 104 * @param {String} [marker] the marker will be set on Object 105 * @return guid associated with this object 106 */ 107 stamp:function (o, readOnly, marker) { 108 if (!o) { 109 return o 110 } 111 marker = marker || STAMP_MARKER; 112 var guid = o[marker]; 113 if (guid) { 114 return guid; 115 } else if (!readOnly) { 116 try { 117 guid = o[marker] = S.guid(marker); 118 } 119 catch (e) { 120 guid = undefined; 121 } 122 } 123 return guid; 124 }, 125 126 /** 127 * empty function 128 */ 129 noop:function () { 130 }, 131 132 /** 133 * Determine the internal JavaScript [[Class]] of an object. 134 */ 135 type:function (o) { 136 return o == null ? 137 String(o) : 138 class2type[toString.call(o)] || 'object'; 139 }, 140 141 /** 142 * whether o === null 143 * @param o 144 */ 145 isNull:function (o) { 146 return o === null; 147 }, 148 149 /** 150 * whether o === undefined 151 * @param o 152 */ 153 isUndefined:function (o) { 154 return o === undefined; 155 }, 156 157 /** 158 * Checks to see if an object is empty. 159 */ 160 isEmptyObject:function (o) { 161 for (var p in o) { 162 if (p !== undefined) { 163 return FALSE; 164 } 165 } 166 return TRUE; 167 }, 168 169 /** 170 * Checks to see if an object is a plain object (created using "{}" 171 * or "new Object()" or "new FunctionClass()"). 172 */ 173 isPlainObject:function (o) { 174 /** 175 * note by yiminghe 176 * isPlainObject(node=document.getElementById("xx")) -> false 177 * toString.call(node) : ie678 == '[object Object]',other =='[object HTMLElement]' 178 * 'isPrototypeOf' in node : ie678 === false ,other === true 179 * refer http://lifesinger.org/blog/2010/12/thinking-of-isplainobject/ 180 */ 181 return o && toString.call(o) === '[object Object]' && 'isPrototypeOf' in o; 182 }, 183 184 185 /** 186 * Checks to see whether two object are equals. 187 * @param a 比较目标1 188 * @param b 比较目标2 189 * @returns {Boolean} a.equals(b) 190 */ 191 equals:function (a, b, /*internal use*/mismatchKeys, /*internal use*/mismatchValues) { 192 // inspired by jasmine 193 mismatchKeys = mismatchKeys || []; 194 mismatchValues = mismatchValues || []; 195 196 if (a === b) { 197 return TRUE; 198 } 199 if (a === undefined || a === null || b === undefined || b === null) { 200 // need type coercion 201 return a == null && b == null; 202 } 203 if (a instanceof Date && b instanceof Date) { 204 return a.getTime() == b.getTime(); 205 } 206 if (S.isString(a) && S.isString(b)) { 207 return (a == b); 208 } 209 if (S.isNumber(a) && S.isNumber(b)) { 210 return (a == b); 211 } 212 if (typeof a === "object" && typeof b === "object") { 213 return compareObjects(a, b, mismatchKeys, mismatchValues); 214 } 215 // Straight check 216 return (a === b); 217 }, 218 219 /** 220 * Creates a deep copy of a plain object or array. Others are returned untouched. 221 * @param input 222 * @param {Function} [filter] filter function 223 * @returns the new cloned object 224 * @see http://www.w3.org/TR/html5/common-dom-interfaces.html#safe-passing-of-structured-data 225 */ 226 clone:function (input, filter) { 227 // 稍微改改就和规范一样了 :) 228 // Let memory be an association list of pairs of objects, 229 // initially empty. This is used to handle duplicate references. 230 // In each pair of objects, one is called the source object 231 // and the other the destination object. 232 var memory = {}, 233 ret = cloneInternal(input, filter, memory); 234 S.each(memory, function (v) { 235 // 清理在源对象上做的标记 236 v = v.input; 237 if (v[CLONE_MARKER]) { 238 try { 239 delete v[CLONE_MARKER]; 240 } catch (e) { 241 S.log("delete CLONE_MARKER error : "); 242 v[CLONE_MARKER] = undefined; 243 } 244 } 245 }); 246 memory = null; 247 return ret; 248 }, 249 250 /** 251 * Removes the whitespace from the beginning and end of a string. 252 * @function 253 */ 254 trim:trim ? 255 function (str) { 256 return str == null ? EMPTY : trim.call(str); 257 } : 258 function (str) { 259 return str == null ? EMPTY : str.toString().replace(RE_TRIM, EMPTY); 260 }, 261 262 /** 263 * Substitutes keywords in a string using an object/array. 264 * Removes undefined keywords and ignores escaped keywords. 265 * @param {String} str template string 266 * @param {Object} o json data 267 * @param {RegExp} [regexp] to match a piece of template string 268 */ 269 substitute:function (str, o, regexp) { 270 if (!S.isString(str) 271 || !S.isPlainObject(o)) { 272 return str; 273 } 274 275 return str.replace(regexp || /\\?\{([^{}]+)\}/g, function (match, name) { 276 if (match.charAt(0) === '\\') { 277 return match.slice(1); 278 } 279 return (o[name] === undefined) ? EMPTY : o[name]; 280 }); 281 }, 282 283 /** 284 * Executes the supplied function on each item in the array. 285 * @param object {Object} the object to iterate 286 * @param fn {Function} the function to execute on each item. The function 287 * receives three arguments: the value, the index, the full array. 288 * @param {Object} [context] 289 */ 290 each:function (object, fn, context) { 291 if (object) { 292 var key, 293 val, 294 i = 0, 295 length = object && object.length, 296 isObj = length === undefined || S.type(object) === 'function'; 297 298 context = context || null; 299 300 if (isObj) { 301 for (key in object) { 302 // can not use hasOwnProperty 303 if (fn.call(context, object[key], key, object) === FALSE) { 304 break; 305 } 306 } 307 } else { 308 for (val = object[0]; 309 i < length && fn.call(context, val, i, object) !== FALSE; val = object[++i]) { 310 } 311 } 312 } 313 return object; 314 }, 315 316 /** 317 * Search for a specified value within an array. 318 * @param item individual item to be searched 319 * @function 320 * @param {Array} arr the array of items where item will be search 321 * @returns {number} item's index in array 322 */ 323 indexOf:indexOf ? 324 function (item, arr) { 325 return indexOf.call(arr, item); 326 } : 327 function (item, arr) { 328 for (var i = 0, len = arr.length; i < len; ++i) { 329 if (arr[i] === item) { 330 return i; 331 } 332 } 333 return -1; 334 }, 335 336 /** 337 * Returns the index of the last item in the array 338 * that contains the specified value, -1 if the 339 * value isn't found. 340 * @function 341 * @param item individual item to be searched 342 * @param {Array} arr the array of items where item will be search 343 * @returns {number} item's last index in array 344 */ 345 lastIndexOf:(lastIndexOf) ? 346 function (item, arr) { 347 return lastIndexOf.call(arr, item); 348 } : 349 function (item, arr) { 350 for (var i = arr.length - 1; i >= 0; i--) { 351 if (arr[i] === item) { 352 break; 353 } 354 } 355 return i; 356 }, 357 358 /** 359 * Returns a copy of the array with the duplicate entries removed 360 * @param a {Array} the array to find the subset of unique for 361 * @param [override] {Boolean} 362 * if override is true, S.unique([a, b, a]) => [b, a] 363 * if override is false, S.unique([a, b, a]) => [a, b] 364 * @return {Array} a copy of the array with duplicate entries removed 365 */ 366 unique:function (a, override) { 367 var b = a.slice(); 368 if (override) { 369 b.reverse(); 370 } 371 var i = 0, 372 n, 373 item; 374 375 while (i < b.length) { 376 item = b[i]; 377 while ((n = S.lastIndexOf(item, b)) !== i) { 378 b.splice(n, 1); 379 } 380 i += 1; 381 } 382 383 if (override) { 384 b.reverse(); 385 } 386 return b; 387 }, 388 389 /** 390 * Search for a specified value index within an array. 391 * @param item individual item to be searched 392 * @param {Array} arr the array of items where item will be search 393 * @returns {Boolean} the item exists in arr 394 */ 395 inArray:function (item, arr) { 396 return S.indexOf(item, arr) > -1; 397 }, 398 399 /** 400 * Executes the supplied function on each item in the array. 401 * Returns a new array containing the items that the supplied 402 * function returned true for. 403 * @function 404 * @param arr {Array} the array to iterate 405 * @param fn {Function} the function to execute on each item 406 * @param [context] {Object} optional context object 407 * @return {Array} The items on which the supplied function 408 * returned true. If no items matched an empty array is 409 * returned. 410 */ 411 filter:filter ? 412 function (arr, fn, context) { 413 return filter.call(arr, fn, context || this); 414 } : 415 function (arr, fn, context) { 416 var ret = []; 417 S.each(arr, function (item, i, arr) { 418 if (fn.call(context || this, item, i, arr)) { 419 ret.push(item); 420 } 421 }); 422 return ret; 423 }, 424 425 426 /** 427 * Executes the supplied function on each item in the array. 428 * Returns a new array containing the items that the supplied 429 * function returned for. 430 * @function 431 * @param arr {Array} the array to iterate 432 * @param fn {Function} the function to execute on each item 433 * @param [context] {Object} optional context object 434 * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map 435 * @return {Array} The items on which the supplied function 436 * returned 437 */ 438 map:map ? 439 function (arr, fn, context) { 440 return map.call(arr, fn, context || this); 441 } : 442 function (arr, fn, context) { 443 var len = arr.length, 444 res = new Array(len); 445 for (var i = 0; i < len; i++) { 446 var el = S.isString(arr) ? arr.charAt(i) : arr[i]; 447 if (el 448 || 449 //ie<9 in invalid when typeof arr == string 450 i in arr) { 451 res[i] = fn.call(context || this, el, i, arr); 452 } 453 } 454 return res; 455 }, 456 457 458 /** 459 * Executes the supplied function on each item in the array. 460 * Returns a value which is accumulation of the value that the supplied 461 * function returned. 462 * 463 * @param arr {Array} the array to iterate 464 * @param callback {Function} the function to execute on each item 465 * @param initialValue {number} optional context object 466 * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/reduce 467 * @return {Array} The items on which the supplied function returned 468 */ 469 reduce:/* 470 NaN ? 471 reduce ? function(arr, callback, initialValue) { 472 return arr.reduce(callback, initialValue); 473 } : */function (arr, callback, initialValue) { 474 var len = arr.length; 475 if (typeof callback !== "function") { 476 throw new TypeError("callback is not function!"); 477 } 478 479 // no value to return if no initial value and an empty array 480 if (len === 0 && arguments.length == 2) { 481 throw new TypeError("arguments invalid"); 482 } 483 484 var k = 0; 485 var accumulator; 486 if (arguments.length >= 3) { 487 accumulator = arguments[2]; 488 } 489 else { 490 do { 491 if (k in arr) { 492 accumulator = arr[k++]; 493 break; 494 } 495 496 // if array contains no values, no initial value to return 497 k += 1; 498 if (k >= len) { 499 throw new TypeError(); 500 } 501 } 502 while (TRUE); 503 } 504 505 while (k < len) { 506 if (k in arr) { 507 accumulator = callback.call(undefined, accumulator, arr[k], k, arr); 508 } 509 k++; 510 } 511 512 return accumulator; 513 }, 514 515 /** 516 * Tests whether all elements in the array pass the test implemented by the provided function. 517 * @function 518 * @param arr {Array} the array to iterate 519 * @param callback {Function} the function to execute on each item 520 * @param [context] {Object} optional context object 521 * @returns {Boolean} whether all elements in the array pass the test implemented by the provided function. 522 */ 523 every:every ? 524 function (arr, fn, context) { 525 return every.call(arr, fn, context || this); 526 } : 527 function (arr, fn, context) { 528 var len = arr && arr.length || 0; 529 for (var i = 0; i < len; i++) { 530 if (i in arr && !fn.call(context, arr[i], i, arr)) { 531 return FALSE; 532 } 533 } 534 return TRUE; 535 }, 536 537 /** 538 * Tests whether some element in the array passes the test implemented by the provided function. 539 * @function 540 * @param arr {Array} the array to iterate 541 * @param callback {Function} the function to execute on each item 542 * @param [context] {Object} optional context object 543 * @returns {Boolean} whether some element in the array passes the test implemented by the provided function. 544 */ 545 some:some ? 546 function (arr, fn, context) { 547 return some.call(arr, fn, context || this); 548 } : 549 function (arr, fn, context) { 550 var len = arr && arr.length || 0; 551 for (var i = 0; i < len; i++) { 552 if (i in arr && fn.call(context, arr[i], i, arr)) { 553 return TRUE; 554 } 555 } 556 return FALSE; 557 }, 558 559 /** 560 * Creates a new function that, when called, itself calls this function in the context of the provided this value, 561 * with a given sequence of arguments preceding any provided when the new function was called. 562 * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind 563 * @param {Function} fn internal called function 564 * @param {Object} obj context in which fn runs 565 * @param {...} arg1 extra arguments 566 * @returns {Function} new function with context and arguments 567 */ 568 bind:function (fn, obj, arg1) { 569 var slice = [].slice, 570 args = slice.call(arguments, 2), 571 fNOP = function () { 572 }, 573 bound = function () { 574 return fn.apply(this instanceof fNOP ? this : obj, 575 args.concat(slice.call(arguments))); 576 }; 577 fNOP.prototype = fn.prototype; 578 bound.prototype = new fNOP(); 579 return bound; 580 }, 581 582 /** 583 * Gets current date in milliseconds. 584 * @function 585 * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/now 586 * http://j-query.blogspot.com/2011/02/timing-ecmascript-5-datenow-function.html 587 * http://kangax.github.com/es5-compat-table/ 588 */ 589 now:Date.now || function () { 590 return +new Date(); 591 }, 592 /** 593 * frequently used in taobao cookie about nick 594 */ 595 fromUnicode:function (str) { 596 return str.replace(/\\u([a-f\d]{4})/ig, function (m, u) { 597 return String.fromCharCode(parseInt(u, HEX_BASE)); 598 }); 599 }, 600 601 602 ucfirst:function (s) { 603 s += ""; 604 return s.charAt(0).toUpperCase() + s.substring(1); 605 }, 606 607 /** 608 * get escaped string from html 609 * @see http://yiminghe.javaeye.com/blog/788929 610 * http://wonko.com/post/html-escaping 611 * @param str {string} text2html show 612 */ 613 escapeHTML:function (str) { 614 return str.replace(getEscapeReg(), function (m) { 615 return reverseEntities[m]; 616 }); 617 }, 618 619 /** 620 * get escaped regexp string for construct regexp 621 * @param str 622 */ 623 escapeRegExp:function (str) { 624 return str.replace(escapeRegExp, '\\$&'); 625 }, 626 627 /** 628 * un-escape html to string 629 * @param str {string} html2text 630 */ 631 unEscapeHTML:function (str) { 632 return str.replace(getUnEscapeReg(), function (m, n) { 633 return htmlEntities[m] || String.fromCharCode(+n); 634 }); 635 }, 636 /** 637 * Converts object to a true array. 638 * @param o {object|Array} array like object or array 639 * @return {Array} native Array 640 */ 641 makeArray:function (o) { 642 if (o == null) { 643 return []; 644 } 645 if (S.isArray(o)) { 646 return o; 647 } 648 649 // The strings and functions also have 'length' 650 if (typeof o.length !== 'number' 651 // form.elements in ie78 has nodeName "form" 652 // then caution select 653 // || o.nodeName 654 // window 655 || o.alert 656 || S.isString(o) 657 || S.isFunction(o)) { 658 return [o]; 659 } 660 var ret = []; 661 for (var i = 0, l = o.length; i < l; i++) { 662 ret[i] = o[i]; 663 } 664 return ret; 665 }, 666 /** 667 * Creates a serialized string of an array or object. 668 * @example 669 * <code> 670 * {foo: 1, bar: 2} // -> 'foo=1&bar=2' 671 * {foo: 1, bar: [2, 3]} // -> 'foo=1&bar=2&bar=3' 672 * {foo: '', bar: 2} // -> 'foo=&bar=2' 673 * {foo: undefined, bar: 2} // -> 'foo=undefined&bar=2' 674 * {foo: true, bar: 2} // -> 'foo=true&bar=2' 675 * </code> 676 * @param {Object} o json data 677 * @param {String} [sep='&'] separator between each pair of data 678 * @param {String} [eq='='] separator between key and value of data 679 * @param {Boolean} [arr=true] whether add '[]' to array key of data 680 * @return {String} 681 */ 682 param:function (o, sep, eq, arr) { 683 if (!S.isPlainObject(o)) { 684 return EMPTY; 685 } 686 sep = sep || SEP; 687 eq = eq || EQ; 688 if (S.isUndefined(arr)) { 689 arr = TRUE; 690 } 691 var buf = [], key, val; 692 for (key in o) { 693 if (hasOwnProperty(o, key)) { 694 val = o[key]; 695 key = encode(key); 696 697 // val is valid non-array value 698 if (isValidParamValue(val)) { 699 buf.push(key, eq, encode(val + EMPTY), sep); 700 } 701 // val is not empty array 702 else if (S.isArray(val) && val.length) { 703 for (var i = 0, len = val.length; i < len; ++i) { 704 if (isValidParamValue(val[i])) { 705 buf.push(key, 706 (arr ? encode("[]") : EMPTY), 707 eq, encode(val[i] + EMPTY), sep); 708 } 709 } 710 } 711 // ignore other cases, including empty array, Function, RegExp, Date etc. 712 } 713 } 714 buf.pop(); 715 return buf.join(EMPTY); 716 }, 717 718 /** 719 * Parses a URI-like query string and returns an object composed of parameter/value pairs. 720 * @example 721 * <code> 722 * 'section=blog&id=45' // -> {section: 'blog', id: '45'} 723 * 'section=blog&tag=js&tag=doc' // -> {section: 'blog', tag: ['js', 'doc']} 724 * 'tag=ruby%20on%20rails' // -> {tag: 'ruby on rails'} 725 * 'id=45&raw' // -> {id: '45', raw: ''} 726 * </code> 727 * @param {String} str param string 728 * @param {String} [sep='&'] separator between each pair of data 729 * @param {String} [eq='='] separator between key and value of data 730 * @returns {Object} json data 731 */ 732 unparam:function (str, sep, eq) { 733 if (!S.isString(str) || !(str = S.trim(str))) { 734 return {}; 735 } 736 sep = sep || SEP; 737 eq = eq || EQ; 738 var ret = {}, 739 pairs = str.split(sep), 740 pair, key, val, 741 i = 0, len = pairs.length; 742 743 for (; i < len; ++i) { 744 pair = pairs[i].split(eq); 745 key = decode(pair[0]); 746 try { 747 val = decode(pair[1] || EMPTY); 748 } catch (e) { 749 S.log(e + "decodeURIComponent error : " + pair[1], "error"); 750 val = pair[1] || EMPTY; 751 } 752 if (S.endsWith(key, "[]")) { 753 key = key.substring(0, key.length - 2); 754 } 755 if (hasOwnProperty(ret, key)) { 756 if (S.isArray(ret[key])) { 757 ret[key].push(val); 758 } else { 759 ret[key] = [ret[key], val]; 760 } 761 } else { 762 ret[key] = val; 763 } 764 } 765 return ret; 766 }, 767 /** 768 * Executes the supplied function in the context of the supplied 769 * object 'when' milliseconds later. Executes the function a 770 * single time unless periodic is set to true. 771 * @param fn {Function|String} the function to execute or the name of the method in 772 * the 'o' object to execute. 773 * @param when {Number} the number of milliseconds to wait until the fn is executed. 774 * @param {Boolean} [periodic] if true, executes continuously at supplied interval 775 * until canceled. 776 * @param {Object} [context] the context object. 777 * @param [data] that is provided to the function. This accepts either a single 778 * item or an array. If an array is provided, the function is executed with 779 * one parameter for each array item. If you need to pass a single array 780 * parameter, it needs to be wrapped in an array [myarray]. 781 * @return {Object} a timer object. Call the cancel() method on this object to stop 782 * the timer. 783 */ 784 later:function (fn, when, periodic, context, data) { 785 when = when || 0; 786 var m = fn, 787 d = S.makeArray(data), 788 f, 789 r; 790 791 if (S.isString(fn)) { 792 m = context[fn]; 793 } 794 795 if (!m) { 796 S.error('method undefined'); 797 } 798 799 f = function () { 800 m.apply(context, d); 801 }; 802 803 r = (periodic) ? setInterval(f, when) : setTimeout(f, when); 804 805 return { 806 id:r, 807 interval:periodic, 808 cancel:function () { 809 if (this.interval) { 810 clearInterval(r); 811 } else { 812 clearTimeout(r); 813 } 814 } 815 }; 816 }, 817 818 /** 819 * test whether a string start with a specified substring 820 * @param {String} str the whole string 821 * @param {String} prefix a specified substring 822 * @returns {Boolean} whether str start with prefix 823 */ 824 startsWith:function (str, prefix) { 825 return str.lastIndexOf(prefix, 0) === 0; 826 }, 827 828 /** 829 * test whether a string end with a specified substring 830 * @param {String} str the whole string 831 * @param {String} suffix a specified substring 832 * @returns {Boolean} whether str end with suffix 833 */ 834 endsWith:function (str, suffix) { 835 var ind = str.length - suffix.length; 836 return ind >= 0 && str.indexOf(suffix, ind) == ind; 837 }, 838 839 /** 840 * Throttles a call to a method based on the time between calls. 841 * @param {function} fn The function call to throttle. 842 * @param {object} [context] context fn to run 843 * @param {Number} [ms] The number of milliseconds to throttle the method call. 844 * Passing a -1 will disable the throttle. Defaults to 150. 845 * @return {function} Returns a wrapped function that calls fn throttled. 846 */ 847 throttle:function (fn, ms, context) { 848 ms = ms || 150; 849 850 if (ms === -1) { 851 return (function () { 852 fn.apply(context || this, arguments); 853 }); 854 } 855 856 var last = S.now(); 857 858 return (function () { 859 var now = S.now(); 860 if (now - last > ms) { 861 last = now; 862 fn.apply(context || this, arguments); 863 } 864 }); 865 }, 866 867 /** 868 * buffers a call between a fixed time 869 * @param {function} fn 870 * @param {object} [context] 871 * @param {Number} ms 872 * @return {function} Returns a wrapped function that calls fn buffered. 873 */ 874 buffer:function (fn, ms, context) { 875 ms = ms || 150; 876 877 if (ms === -1) { 878 return function () { 879 fn.apply(context || this, arguments); 880 }; 881 } 882 var bufferTimer = null; 883 884 function f() { 885 f.stop(); 886 bufferTimer = S.later(fn, ms, FALSE, context || this, arguments); 887 } 888 889 f.stop = function () { 890 if (bufferTimer) { 891 bufferTimer.cancel(); 892 bufferTimer = 0; 893 } 894 }; 895 896 return f; 897 } 898 899 }); 900 901 // for idea ..... auto-hint 902 S.mix(S, 903 /** 904 * @lends KISSY 905 */ 906 { 907 /** 908 * test whether o is boolean 909 * @function 910 * @param o 911 * @returns {Boolean} 912 */ 913 isBoolean:isValidParamValue, 914 /** 915 * test whether o is number 916 * @function 917 * @param o 918 * @returns {Boolean} 919 */ 920 isNumber:isValidParamValue, 921 /** 922 * test whether o is String 923 * @function 924 * @param o 925 * @returns {Boolean} 926 */ 927 isString:isValidParamValue, 928 /** 929 * test whether o is function 930 * @function 931 * @param o 932 * @returns {Boolean} 933 */ 934 isFunction:isValidParamValue, 935 /** 936 * test whether o is Array 937 * @function 938 * @param o 939 * @returns {Boolean} 940 */ 941 isArray:isValidParamValue, 942 /** 943 * test whether o is Date 944 * @function 945 * @param o 946 * @returns {Boolean} 947 */ 948 isDate:isValidParamValue, 949 /** 950 * test whether o is RegExp 951 * @function 952 * @param o 953 * @returns {Boolean} 954 */ 955 isRegExp:isValidParamValue, 956 /** 957 * test whether o is Object 958 * @function 959 * @param o 960 * @returns {Boolean} 961 */ 962 isObject:isValidParamValue 963 }); 964 965 S.each('Boolean Number String Function Array Date RegExp Object'.split(' '), 966 function (name, lc) { 967 // populate the class2type map 968 class2type['[object ' + name + ']'] = (lc = name.toLowerCase()); 969 970 // add isBoolean/isNumber/... 971 S['is' + name] = function (o) { 972 return S.type(o) == lc; 973 } 974 }); 975 976 function cloneInternal(input, f, memory) { 977 var destination = input, 978 isArray, 979 isPlainObject, 980 k, 981 stamp; 982 if (!input) { 983 return destination; 984 } 985 986 // If input is the source object of a pair of objects in memory, 987 // then return the destination object in that pair of objects . 988 // and abort these steps. 989 if (input[CLONE_MARKER]) { 990 // 对应的克隆后对象 991 return memory[input[CLONE_MARKER]].destination; 992 } else if (typeof input === "object") { 993 // 引用类型要先记录 994 var constructor = input.constructor; 995 if (S.inArray(constructor, [Boolean, String, Number, Date, RegExp])) { 996 destination = new constructor(input.valueOf()); 997 } 998 // ImageData , File, Blob , FileList .. etc 999 else if (isArray = S.isArray(input)) { 1000 destination = f ? S.filter(input, f) : input.concat(); 1001 } else if (isPlainObject = S.isPlainObject(input)) { 1002 destination = {}; 1003 } 1004 // Add a mapping from input (the source object) 1005 // to output (the destination object) to memory. 1006 // 做标记 1007 input[CLONE_MARKER] = (stamp = S.guid()); 1008 // 存储源对象以及克隆后的对象 1009 memory[stamp] = {destination:destination, input:input}; 1010 } 1011 // If input is an Array object or an Object object, 1012 // then, for each enumerable property in input, 1013 // add a new property to output having the same name, 1014 // and having a value created from invoking the internal structured cloning algorithm recursively 1015 // with the value of the property as the "input" argument and memory as the "memory" argument. 1016 // The order of the properties in the input and output objects must be the same. 1017 1018 // clone it 1019 if (isArray) { 1020 for (var i = 0; i < destination.length; i++) { 1021 destination[i] = cloneInternal(destination[i], f, memory); 1022 } 1023 } else if (isPlainObject) { 1024 for (k in input) { 1025 if (hasOwnProperty(input, k)) { 1026 if (k !== CLONE_MARKER && 1027 (!f || (f.call(input, input[k], k, input) !== FALSE))) { 1028 destination[k] = cloneInternal(input[k], f, memory); 1029 } 1030 } 1031 } 1032 } 1033 1034 return destination; 1035 } 1036 1037 function compareObjects(a, b, mismatchKeys, mismatchValues) { 1038 // 两个比较过了,无需再比较,防止循环比较 1039 if (a[COMPARE_MARKER] === b && b[COMPARE_MARKER] === a) { 1040 return TRUE; 1041 } 1042 a[COMPARE_MARKER] = b; 1043 b[COMPARE_MARKER] = a; 1044 var hasKey = function (obj, keyName) { 1045 return (obj !== null && obj !== undefined) && obj[keyName] !== undefined; 1046 }; 1047 for (var property in b) { 1048 if (hasOwnProperty(b, property)) { 1049 if (!hasKey(a, property) && hasKey(b, property)) { 1050 mismatchKeys.push("expected has key '" + property + "', but missing from actual."); 1051 } 1052 } 1053 } 1054 for (property in a) { 1055 if (hasOwnProperty(a, property)) { 1056 if (!hasKey(b, property) && hasKey(a, property)) { 1057 mismatchKeys.push("expected missing key '" + property + "', but present in actual."); 1058 } 1059 } 1060 } 1061 for (property in b) { 1062 if (hasOwnProperty(b, property)) { 1063 if (property == COMPARE_MARKER) { 1064 continue; 1065 } 1066 if (!S.equals(a[property], b[property], mismatchKeys, mismatchValues)) { 1067 mismatchValues.push("'" + property + "' was '" + (b[property] ? (b[property].toString()) : b[property]) 1068 + "' in expected, but was '" + 1069 (a[property] ? (a[property].toString()) : a[property]) + "' in actual."); 1070 } 1071 } 1072 } 1073 if (S.isArray(a) && S.isArray(b) && a.length != b.length) { 1074 mismatchValues.push("arrays were not the same length"); 1075 } 1076 delete a[COMPARE_MARKER]; 1077 delete b[COMPARE_MARKER]; 1078 return (mismatchKeys.length === 0 && mismatchValues.length === 0); 1079 } 1080 1081 })(KISSY, undefined); 1082