master
Raw Download raw file
  1
  2  'use strict';
  3
  4  /***
  5   * @package Core
  6   * @description Internal utility and common methods.
  7   ***/
  8
  9
 10  // A few optimizations for Google Closure Compiler will save us a couple kb in the release script.
 11  var object = Object, array = Array, regexp = RegExp, date = Date, string = String, number = Number, math = Math, Undefined;
 12
 13  // The global context
 14  var globalContext = typeof global !== 'undefined' ? global : this;
 15
 16  // Internal toString
 17  var internalToString = object.prototype.toString;
 18
 19  // Internal hasOwnProperty
 20  var internalHasOwnProperty = object.prototype.hasOwnProperty;
 21
 22  // defineProperty exists in IE8 but will error when trying to define a property on
 23  // native objects. IE8 does not have defineProperies, however, so this check saves a try/catch block.
 24  var definePropertySupport = object.defineProperty && object.defineProperties;
 25
 26  // Are regexes type function?
 27  var regexIsFunction = typeof regexp() === 'function';
 28
 29  // Do strings have no keys?
 30  var noKeysInStringObjects = !('0' in new string('a'));
 31
 32  // Type check methods need a way to be accessed dynamically.
 33  var typeChecks = {};
 34
 35  // Classes that can be matched by value
 36  var matchedByValueReg = /^\[object Date|Array|String|Number|RegExp|Boolean|Arguments\]$/;
 37
 38  // Class initializers and class helpers
 39  var ClassNames = 'Boolean,Number,String,Array,Date,RegExp,Function'.split(',');
 40
 41  var isBoolean  = buildPrimitiveClassCheck('boolean', ClassNames[0]);
 42  var isNumber   = buildPrimitiveClassCheck('number',  ClassNames[1]);
 43  var isString   = buildPrimitiveClassCheck('string',  ClassNames[2]);
 44
 45  var isArray    = buildClassCheck(ClassNames[3]);
 46  var isDate     = buildClassCheck(ClassNames[4]);
 47  var isRegExp   = buildClassCheck(ClassNames[5]);
 48
 49
 50  // Wanted to enhance performance here by using simply "typeof"
 51  // but Firefox has two major issues that make this impossible,
 52  // one fixed, the other not. Despite being typeof "function"
 53  // the objects below still report in as [object Function], so
 54  // we need to perform a full class check here.
 55  //
 56  // 1. Regexes can be typeof "function" in FF < 3
 57  //    https://bugzilla.mozilla.org/show_bug.cgi?id=61911 (fixed)
 58  //
 59  // 2. HTMLEmbedElement and HTMLObjectElement are be typeof "function"
 60  //    https://bugzilla.mozilla.org/show_bug.cgi?id=268945 (won't fix)
 61  //
 62  var isFunction = buildClassCheck(ClassNames[6]);
 63
 64  function isClass(obj, klass, cached) {
 65    var k = cached || className(obj);
 66    return k === '[object '+klass+']';
 67  }
 68
 69  function buildClassCheck(klass) {
 70    var fn = (klass === 'Array' && array.isArray) || function(obj, cached) {
 71      return isClass(obj, klass, cached);
 72    };
 73    typeChecks[klass] = fn;
 74    return fn;
 75  }
 76
 77  function buildPrimitiveClassCheck(type, klass) {
 78    var fn = function(obj) {
 79      if(isObjectType(obj)) {
 80        return isClass(obj, klass);
 81      }
 82      return typeof obj === type;
 83    }
 84    typeChecks[klass] = fn;
 85    return fn;
 86  }
 87
 88  function className(obj) {
 89    return internalToString.call(obj);
 90  }
 91
 92  function initializeClasses() {
 93    initializeClass(object);
 94    iterateOverObject(ClassNames, function(i,name) {
 95      initializeClass(globalContext[name]);
 96    });
 97  }
 98
 99  function initializeClass(klass) {
100    if(klass['SugarMethods']) return;
101    defineProperty(klass, 'SugarMethods', {});
102    extend(klass, false, true, {
103      'extend': function(methods, override, instance) {
104        extend(klass, instance !== false, override, methods);
105      },
106      'sugarRestore': function() {
107        return batchMethodExecute(this, klass, arguments, function(target, name, m) {
108          defineProperty(target, name, m.method);
109        });
110      },
111      'sugarRevert': function() {
112        return batchMethodExecute(this, klass, arguments, function(target, name, m) {
113          if(m['existed']) {
114            defineProperty(target, name, m['original']);
115          } else {
116            delete target[name];
117          }
118        });
119      }
120    });
121  }
122
123  // Class extending methods
124
125  function extend(klass, instance, override, methods) {
126    var extendee = instance ? klass.prototype : klass;
127    initializeClass(klass);
128    iterateOverObject(methods, function(name, extendedFn) {
129      var nativeFn = extendee[name],
130          existed  = hasOwnProperty(extendee, name);
131      if(isFunction(override) && nativeFn) {
132        extendedFn = wrapNative(nativeFn, extendedFn, override);
133      }
134      if(override !== false || !nativeFn) {
135        defineProperty(extendee, name, extendedFn);
136      }
137      // If the method is internal to Sugar, then
138      // store a reference so it can be restored later.
139      klass['SugarMethods'][name] = {
140        'method':   extendedFn,
141        'existed':  existed,
142        'original': nativeFn,
143        'instance': instance
144      };
145    });
146  }
147
148  function extendSimilar(klass, instance, override, set, fn) {
149    var methods = {};
150    set = isString(set) ? set.split(',') : set;
151    set.forEach(function(name, i) {
152      fn(methods, name, i);
153    });
154    extend(klass, instance, override, methods);
155  }
156
157  function batchMethodExecute(target, klass, args, fn) {
158    var all = args.length === 0, methods = multiArgs(args), changed = false;
159    iterateOverObject(klass['SugarMethods'], function(name, m) {
160      if(all || methods.indexOf(name) !== -1) {
161        changed = true;
162        fn(m['instance'] ? target.prototype : target, name, m);
163      }
164    });
165    return changed;
166  }
167
168  function wrapNative(nativeFn, extendedFn, condition) {
169    return function(a) {
170      return condition.apply(this, arguments) ?
171             extendedFn.apply(this, arguments) :
172             nativeFn.apply(this, arguments);
173    }
174  }
175
176  function defineProperty(target, name, method) {
177    if(definePropertySupport) {
178      object.defineProperty(target, name, {
179        'value': method,
180        'configurable': true,
181        'enumerable': false,
182        'writable': true
183      });
184    } else {
185      target[name] = method;
186    }
187  }
188
189
190  // Argument helpers
191
192  function multiArgs(args, fn, from) {
193    var result = [], i = from || 0, len;
194    for(len = args.length; i < len; i++) {
195      result.push(args[i]);
196      if(fn) fn.call(args, args[i], i);
197    }
198    return result;
199  }
200
201  function flattenedArgs(args, fn, from) {
202    var arg = args[from || 0];
203    if(isArray(arg)) {
204      args = arg;
205      from = 0;
206    }
207    return multiArgs(args, fn, from);
208  }
209
210  function checkCallback(fn) {
211    if(!fn || !fn.call) {
212      throw new TypeError('Callback is not callable');
213    }
214  }
215
216
217  // General helpers
218
219  function isDefined(o) {
220    return o !== Undefined;
221  }
222
223  function isUndefined(o) {
224    return o === Undefined;
225  }
226
227
228  // Object helpers
229
230  function hasProperty(obj, prop) {
231    return !isPrimitiveType(obj) && prop in obj;
232  }
233
234  function hasOwnProperty(obj, prop) {
235    return !!obj && internalHasOwnProperty.call(obj, prop);
236  }
237
238  function isObjectType(obj) {
239    // 1. Check for null
240    // 2. Check for regexes in environments where they are "functions".
241    return !!obj && (typeof obj === 'object' || (regexIsFunction && isRegExp(obj)));
242  }
243
244  function isPrimitiveType(obj) {
245    var type = typeof obj;
246    return obj == null || type === 'string' || type === 'number' || type === 'boolean';
247  }
248
249  function isPlainObject(obj, klass) {
250    klass = klass || className(obj);
251    try {
252      // Not own constructor property must be Object
253      // This code was borrowed from jQuery.isPlainObject
254      if (obj && obj.constructor &&
255            !hasOwnProperty(obj, 'constructor') &&
256            !hasOwnProperty(obj.constructor.prototype, 'isPrototypeOf')) {
257        return false;
258      }
259    } catch (e) {
260      // IE8,9 Will throw exceptions on certain host objects.
261      return false;
262    }
263    // === on the constructor is not safe across iframes
264    // 'hasOwnProperty' ensures that the object also inherits
265    // from Object, which is false for DOMElements in IE.
266    return !!obj && klass === '[object Object]' && 'hasOwnProperty' in obj;
267  }
268
269  function iterateOverObject(obj, fn) {
270    var key;
271    for(key in obj) {
272      if(!hasOwnProperty(obj, key)) continue;
273      if(fn.call(obj, key, obj[key], obj) === false) break;
274    }
275  }
276
277  function simpleRepeat(n, fn) {
278    for(var i = 0; i < n; i++) {
279      fn(i);
280    }
281  }
282
283  function simpleMerge(target, source) {
284    iterateOverObject(source, function(key) {
285      target[key] = source[key];
286    });
287    return target;
288  }
289
290   // Make primtives types like strings into objects.
291   function coercePrimitiveToObject(obj) {
292     if(isPrimitiveType(obj)) {
293       obj = object(obj);
294     }
295     if(noKeysInStringObjects && isString(obj)) {
296       forceStringCoercion(obj);
297     }
298     return obj;
299   }
300
301   // Force strings to have their indexes set in
302   // environments that don't do this automatically.
303   function forceStringCoercion(obj) {
304     var i = 0, chr;
305     while(chr = obj.charAt(i)) {
306       obj[i++] = chr;
307     }
308   }
309
310  // Hash definition
311
312  function Hash(obj) {
313    simpleMerge(this, coercePrimitiveToObject(obj));
314  };
315
316  Hash.prototype.constructor = object;
317
318  // Math helpers
319
320  var abs   = math.abs;
321  var pow   = math.pow;
322  var ceil  = math.ceil;
323  var floor = math.floor;
324  var round = math.round;
325  var min   = math.min;
326  var max   = math.max;
327
328  function withPrecision(val, precision, fn) {
329    var multiplier = pow(10, abs(precision || 0));
330    fn = fn || round;
331    if(precision < 0) multiplier = 1 / multiplier;
332    return fn(val * multiplier) / multiplier;
333  }
334
335  // Full width number helpers
336
337  var HalfWidthZeroCode = 0x30;
338  var HalfWidthNineCode = 0x39;
339  var FullWidthZeroCode = 0xff10;
340  var FullWidthNineCode = 0xff19;
341
342  var HalfWidthPeriod = '.';
343  var FullWidthPeriod = '.';
344  var HalfWidthComma  = ',';
345
346  // Used here and later in the Date package.
347  var FullWidthDigits   = '';
348
349  var NumberNormalizeMap = {};
350  var NumberNormalizeReg;
351
352  function codeIsNumeral(code) {
353    return (code >= HalfWidthZeroCode && code <= HalfWidthNineCode) ||
354           (code >= FullWidthZeroCode && code <= FullWidthNineCode);
355  }
356
357  function buildNumberHelpers() {
358    var digit, i;
359    for(i = 0; i <= 9; i++) {
360      digit = chr(i + FullWidthZeroCode);
361      FullWidthDigits += digit;
362      NumberNormalizeMap[digit] = chr(i + HalfWidthZeroCode);
363    }
364    NumberNormalizeMap[HalfWidthComma] = '';
365    NumberNormalizeMap[FullWidthPeriod] = HalfWidthPeriod;
366    // Mapping this to itself to easily be able to easily
367    // capture it in stringToNumber to detect decimals later.
368    NumberNormalizeMap[HalfWidthPeriod] = HalfWidthPeriod;
369    NumberNormalizeReg = regexp('[' + FullWidthDigits + FullWidthPeriod + HalfWidthComma + HalfWidthPeriod + ']', 'g');
370  }
371
372  // String helpers
373
374  function chr(num) {
375    return string.fromCharCode(num);
376  }
377
378  // WhiteSpace/LineTerminator as defined in ES5.1 plus Unicode characters in the Space, Separator category.
379  function getTrimmableCharacters() {
380    return '\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u2028\u2029\u3000\uFEFF';
381  }
382
383  function repeatString(str, num) {
384    var result = '', str = str.toString();
385    while (num > 0) {
386      if (num & 1) {
387        result += str;
388      }
389      if (num >>= 1) {
390        str += str;
391      }
392    }
393    return result;
394  }
395
396  // Returns taking into account full-width characters, commas, and decimals.
397  function stringToNumber(str, base) {
398    var sanitized, isDecimal;
399    sanitized = str.replace(NumberNormalizeReg, function(chr) {
400      var replacement = NumberNormalizeMap[chr];
401      if(replacement === HalfWidthPeriod) {
402        isDecimal = true;
403      }
404      return replacement;
405    });
406    return isDecimal ? parseFloat(sanitized) : parseInt(sanitized, base || 10);
407  }
408
409
410  // Used by Number and Date
411
412  function padNumber(num, place, sign, base) {
413    var str = abs(num).toString(base || 10);
414    str = repeatString('0', place - str.replace(/\.\d+/, '').length) + str;
415    if(sign || num < 0) {
416      str = (num < 0 ? '-' : '+') + str;
417    }
418    return str;
419  }
420
421  function getOrdinalizedSuffix(num) {
422    if(num >= 11 && num <= 13) {
423      return 'th';
424    } else {
425      switch(num % 10) {
426        case 1:  return 'st';
427        case 2:  return 'nd';
428        case 3:  return 'rd';
429        default: return 'th';
430      }
431    }
432  }
433
434
435  // RegExp helpers
436
437  function getRegExpFlags(reg, add) {
438    var flags = '';
439    add = add || '';
440    function checkFlag(prop, flag) {
441      if(prop || add.indexOf(flag) > -1) {
442        flags += flag;
443      }
444    }
445    checkFlag(reg.multiline, 'm');
446    checkFlag(reg.ignoreCase, 'i');
447    checkFlag(reg.global, 'g');
448    checkFlag(reg.sticky, 'y');
449    return flags;
450  }
451
452  function escapeRegExp(str) {
453    if(!isString(str)) str = string(str);
454    return str.replace(/([\\/\'*+?|()\[\]{}.^$])/g,'\\$1');
455  }
456
457
458  // Date helpers
459
460  function callDateGet(d, method) {
461    return d['get' + (d._utc ? 'UTC' : '') + method]();
462  }
463
464  function callDateSet(d, method, value) {
465    return d['set' + (d._utc && method != 'ISOWeek' ? 'UTC' : '') + method](value);
466  }
467
468  // Used by Array#unique and Object.equal
469
470  function stringify(thing, stack) {
471    var type = typeof thing,
472        thingIsObject,
473        thingIsArray,
474        klass, value,
475        arr, key, i, len;
476
477    // Return quickly if string to save cycles
478    if(type === 'string') return thing;
479
480    klass         = internalToString.call(thing)
481    thingIsObject = isPlainObject(thing, klass);
482    thingIsArray  = isArray(thing, klass);
483
484    if(thing != null && thingIsObject || thingIsArray) {
485      // This method for checking for cyclic structures was egregiously stolen from
486      // the ingenious method by @kitcambridge from the Underscore script:
487      // https://github.com/documentcloud/underscore/issues/240
488      if(!stack) stack = [];
489      // Allowing a step into the structure before triggering this
490      // script to save cycles on standard JSON structures and also to
491      // try as hard as possible to catch basic properties that may have
492      // been modified.
493      if(stack.length > 1) {
494        i = stack.length;
495        while (i--) {
496          if (stack[i] === thing) {
497            return 'CYC';
498          }
499        }
500      }
501      stack.push(thing);
502      value = thing.valueOf() + string(thing.constructor);
503      arr = thingIsArray ? thing : object.keys(thing).sort();
504      for(i = 0, len = arr.length; i < len; i++) {
505        key = thingIsArray ? i : arr[i];
506        value += key + stringify(thing[key], stack);
507      }
508      stack.pop();
509    } else if(1 / thing === -Infinity) {
510      value = '-0';
511    } else {
512      value = string(thing && thing.valueOf ? thing.valueOf() : thing);
513    }
514    return type + klass + value;
515  }
516
517  function isEqual(a, b) {
518    if(a === b) {
519      // Return quickly up front when matching by reference,
520      // but be careful about 0 !== -0.
521      return a !== 0 || 1 / a === 1 / b;
522    } else if(objectIsMatchedByValue(a) && objectIsMatchedByValue(b)) {
523      return stringify(a) === stringify(b);
524    }
525    return false;
526  }
527
528  function objectIsMatchedByValue(obj) {
529    // Only known objects are matched by value. This is notably excluding functions, DOM Elements, and instances of
530    // user-created classes. The latter can arguably be matched by value, but distinguishing between these and
531    // host objects -- which should never be compared by value -- is very tricky so not dealing with it here.
532    var klass = className(obj);
533    return matchedByValueReg.test(klass) || isPlainObject(obj, klass);
534  }
535
536
537  // Used by Array#at and String#at
538
539  function getEntriesForIndexes(obj, args, isString) {
540    var result,
541        length    = obj.length,
542        argsLen   = args.length,
543        overshoot = args[argsLen - 1] !== false,
544        multiple  = argsLen > (overshoot ? 1 : 2);
545    if(!multiple) {
546      return entryAtIndex(obj, length, args[0], overshoot, isString);
547    }
548    result = [];
549    multiArgs(args, function(index) {
550      if(isBoolean(index)) return false;
551      result.push(entryAtIndex(obj, length, index, overshoot, isString));
552    });
553    return result;
554  }
555
556  function entryAtIndex(obj, length, index, overshoot, isString) {
557    if(overshoot) {
558      index = index % length;
559      if(index < 0) index = length + index;
560    }
561    return isString ? obj.charAt(index) : obj[index];
562  }
563
564
565  // Object class methods implemented as instance methods
566
567  function buildObjectInstanceMethods(set, target) {
568    extendSimilar(target, true, false, set, function(methods, name) {
569      methods[name + (name === 'equal' ? 's' : '')] = function() {
570        return object[name].apply(null, [this].concat(multiArgs(arguments)));
571      }
572    });
573  }
574
575  initializeClasses();
576  buildNumberHelpers();
577