lang.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. define(["./kernel", "../has", "../sniff"], function(dojo, has){
  2. // module:
  3. // dojo/_base/lang
  4. has.add("bug-for-in-skips-shadowed", function(){
  5. // if true, the for-in iterator skips object properties that exist in Object's prototype (IE 6 - ?)
  6. for(var i in {toString: 1}){
  7. return 0;
  8. }
  9. return 1;
  10. });
  11. // Helper methods
  12. var _extraNames =
  13. has("bug-for-in-skips-shadowed") ?
  14. "hasOwnProperty.valueOf.isPrototypeOf.propertyIsEnumerable.toLocaleString.toString.constructor".split(".") : [],
  15. _extraLen = _extraNames.length,
  16. getProp = function(/*Array*/parts, /*Boolean*/create, /*Object*/context){
  17. if(!context){
  18. if(parts[0] && dojo.scopeMap[parts[0]]) {
  19. // Voodoo code from the old days where "dojo" or "dijit" maps to some special object
  20. // rather than just window.dojo
  21. context = dojo.scopeMap[parts.shift()][1];
  22. }else{
  23. context = dojo.global;
  24. }
  25. }
  26. try{
  27. for(var i = 0; i < parts.length; i++){
  28. var p = parts[i];
  29. if(!(p in context)){
  30. if(create){
  31. context[p] = {};
  32. }else{
  33. return; // return undefined
  34. }
  35. }
  36. context = context[p];
  37. }
  38. return context; // mixed
  39. }catch(e){
  40. // "p in context" throws an exception when context is a number, boolean, etc. rather than an object,
  41. // so in that corner case just return undefined (by having no return statement)
  42. }
  43. },
  44. opts = Object.prototype.toString,
  45. efficient = function(obj, offset, startWith){
  46. return (startWith||[]).concat(Array.prototype.slice.call(obj, offset||0));
  47. },
  48. _pattern = /\{([^\}]+)\}/g;
  49. // Module export
  50. var lang = {
  51. // summary:
  52. // This module defines Javascript language extensions.
  53. // _extraNames: String[]
  54. // Lists property names that must be explicitly processed during for-in iteration
  55. // in environments that have has("bug-for-in-skips-shadowed") true.
  56. _extraNames:_extraNames,
  57. _mixin: function(dest, source, copyFunc){
  58. // summary:
  59. // Copies/adds all properties of source to dest; returns dest.
  60. // dest: Object
  61. // The object to which to copy/add all properties contained in source.
  62. // source: Object
  63. // The object from which to draw all properties to copy into dest.
  64. // copyFunc: Function?
  65. // The process used to copy/add a property in source; defaults to the Javascript assignment operator.
  66. // returns:
  67. // dest, as modified
  68. // description:
  69. // All properties, including functions (sometimes termed "methods"), excluding any non-standard extensions
  70. // found in Object.prototype, are copied/added to dest. Copying/adding each particular property is
  71. // delegated to copyFunc (if any); copyFunc defaults to the Javascript assignment operator if not provided.
  72. // Notice that by default, _mixin executes a so-called "shallow copy" and aggregate types are copied/added by reference.
  73. var name, s, i, empty = {};
  74. for(name in source){
  75. // the (!(name in empty) || empty[name] !== s) condition avoids copying properties in "source"
  76. // inherited from Object.prototype. For example, if dest has a custom toString() method,
  77. // don't overwrite it with the toString() method that source inherited from Object.prototype
  78. s = source[name];
  79. if(!(name in dest) || (dest[name] !== s && (!(name in empty) || empty[name] !== s))){
  80. dest[name] = copyFunc ? copyFunc(s) : s;
  81. }
  82. }
  83. if(has("bug-for-in-skips-shadowed")){
  84. if(source){
  85. for(i = 0; i < _extraLen; ++i){
  86. name = _extraNames[i];
  87. s = source[name];
  88. if(!(name in dest) || (dest[name] !== s && (!(name in empty) || empty[name] !== s))){
  89. dest[name] = copyFunc ? copyFunc(s) : s;
  90. }
  91. }
  92. }
  93. }
  94. return dest; // Object
  95. },
  96. mixin: function(dest, sources){
  97. // summary:
  98. // Copies/adds all properties of one or more sources to dest; returns dest.
  99. // dest: Object
  100. // The object to which to copy/add all properties contained in source. If dest is falsy, then
  101. // a new object is manufactured before copying/adding properties begins.
  102. // sources: Object...
  103. // One of more objects from which to draw all properties to copy into dest. sources are processed
  104. // left-to-right and if more than one of these objects contain the same property name, the right-most
  105. // value "wins".
  106. // returns: Object
  107. // dest, as modified
  108. // description:
  109. // All properties, including functions (sometimes termed "methods"), excluding any non-standard extensions
  110. // found in Object.prototype, are copied/added from sources to dest. sources are processed left to right.
  111. // The Javascript assignment operator is used to copy/add each property; therefore, by default, mixin
  112. // executes a so-called "shallow copy" and aggregate types are copied/added by reference.
  113. // example:
  114. // make a shallow copy of an object
  115. // | var copy = lang.mixin({}, source);
  116. // example:
  117. // many class constructors often take an object which specifies
  118. // values to be configured on the object. In this case, it is
  119. // often simplest to call `lang.mixin` on the `this` object:
  120. // | declare("acme.Base", null, {
  121. // | constructor: function(properties){
  122. // | // property configuration:
  123. // | lang.mixin(this, properties);
  124. // |
  125. // | console.log(this.quip);
  126. // | // ...
  127. // | },
  128. // | quip: "I wasn't born yesterday, you know - I've seen movies.",
  129. // | // ...
  130. // | });
  131. // |
  132. // | // create an instance of the class and configure it
  133. // | var b = new acme.Base({quip: "That's what it does!" });
  134. // example:
  135. // copy in properties from multiple objects
  136. // | var flattened = lang.mixin(
  137. // | {
  138. // | name: "Frylock",
  139. // | braces: true
  140. // | },
  141. // | {
  142. // | name: "Carl Brutanananadilewski"
  143. // | }
  144. // | );
  145. // |
  146. // | // will print "Carl Brutanananadilewski"
  147. // | console.log(flattened.name);
  148. // | // will print "true"
  149. // | console.log(flattened.braces);
  150. if(!dest){ dest = {}; }
  151. for(var i = 1, l = arguments.length; i < l; i++){
  152. lang._mixin(dest, arguments[i]);
  153. }
  154. return dest; // Object
  155. },
  156. setObject: function(name, value, context){
  157. // summary:
  158. // Set a property from a dot-separated string, such as "A.B.C"
  159. // description:
  160. // Useful for longer api chains where you have to test each object in
  161. // the chain, or when you have an object reference in string format.
  162. // Objects are created as needed along `path`. Returns the passed
  163. // value if setting is successful or `undefined` if not.
  164. // name: String
  165. // Path to a property, in the form "A.B.C".
  166. // value: anything
  167. // value or object to place at location given by name
  168. // context: Object?
  169. // Optional. Object to use as root of path. Defaults to
  170. // `dojo.global`.
  171. // example:
  172. // set the value of `foo.bar.baz`, regardless of whether
  173. // intermediate objects already exist:
  174. // | lang.setObject("foo.bar.baz", value);
  175. // example:
  176. // without `lang.setObject`, we often see code like this:
  177. // | // ensure that intermediate objects are available
  178. // | if(!obj["parent"]){ obj.parent = {}; }
  179. // | if(!obj.parent["child"]){ obj.parent.child = {}; }
  180. // | // now we can safely set the property
  181. // | obj.parent.child.prop = "some value";
  182. // whereas with `lang.setObject`, we can shorten that to:
  183. // | lang.setObject("parent.child.prop", "some value", obj);
  184. var parts = name.split("."), p = parts.pop(), obj = getProp(parts, true, context);
  185. return obj && p ? (obj[p] = value) : undefined; // Object
  186. },
  187. getObject: function(name, create, context){
  188. // summary:
  189. // Get a property from a dot-separated string, such as "A.B.C"
  190. // description:
  191. // Useful for longer api chains where you have to test each object in
  192. // the chain, or when you have an object reference in string format.
  193. // name: String
  194. // Path to an property, in the form "A.B.C".
  195. // create: Boolean?
  196. // Optional. Defaults to `false`. If `true`, Objects will be
  197. // created at any point along the 'path' that is undefined.
  198. // context: Object?
  199. // Optional. Object to use as root of path. Defaults to
  200. // 'dojo.global'. Null may be passed.
  201. return getProp(name ? name.split(".") : [], create, context); // Object
  202. },
  203. exists: function(name, obj){
  204. // summary:
  205. // determine if an object supports a given method
  206. // description:
  207. // useful for longer api chains where you have to test each object in
  208. // the chain. Useful for object and method detection.
  209. // name: String
  210. // Path to an object, in the form "A.B.C".
  211. // obj: Object?
  212. // Object to use as root of path. Defaults to
  213. // 'dojo.global'. Null may be passed.
  214. // example:
  215. // | // define an object
  216. // | var foo = {
  217. // | bar: { }
  218. // | };
  219. // |
  220. // | // search the global scope
  221. // | lang.exists("foo.bar"); // true
  222. // | lang.exists("foo.bar.baz"); // false
  223. // |
  224. // | // search from a particular scope
  225. // | lang.exists("bar", foo); // true
  226. // | lang.exists("bar.baz", foo); // false
  227. return lang.getObject(name, false, obj) !== undefined; // Boolean
  228. },
  229. // Crockford (ish) functions
  230. isString: function(it){
  231. // summary:
  232. // Return true if it is a String
  233. // it: anything
  234. // Item to test.
  235. return (typeof it == "string" || it instanceof String); // Boolean
  236. },
  237. isArray: function(it){
  238. // summary:
  239. // Return true if it is an Array.
  240. // Does not work on Arrays created in other windows.
  241. // it: anything
  242. // Item to test.
  243. return it && (it instanceof Array || typeof it == "array"); // Boolean
  244. },
  245. isFunction: function(it){
  246. // summary:
  247. // Return true if it is a Function
  248. // it: anything
  249. // Item to test.
  250. return opts.call(it) === "[object Function]";
  251. },
  252. isObject: function(it){
  253. // summary:
  254. // Returns true if it is a JavaScript object (or an Array, a Function
  255. // or null)
  256. // it: anything
  257. // Item to test.
  258. return it !== undefined &&
  259. (it === null || typeof it == "object" || lang.isArray(it) || lang.isFunction(it)); // Boolean
  260. },
  261. isArrayLike: function(it){
  262. // summary:
  263. // similar to isArray() but more permissive
  264. // it: anything
  265. // Item to test.
  266. // returns:
  267. // If it walks like a duck and quacks like a duck, return `true`
  268. // description:
  269. // Doesn't strongly test for "arrayness". Instead, settles for "isn't
  270. // a string or number and has a length property". Arguments objects
  271. // and DOM collections will return true when passed to
  272. // isArrayLike(), but will return false when passed to
  273. // isArray().
  274. return it && it !== undefined && // Boolean
  275. // keep out built-in constructors (Number, String, ...) which have length
  276. // properties
  277. !lang.isString(it) && !lang.isFunction(it) &&
  278. !(it.tagName && it.tagName.toLowerCase() == 'form') &&
  279. (lang.isArray(it) || isFinite(it.length));
  280. },
  281. isAlien: function(it){
  282. // summary:
  283. // Returns true if it is a built-in function or some other kind of
  284. // oddball that *should* report as a function but doesn't
  285. return it && !lang.isFunction(it) && /\{\s*\[native code\]\s*\}/.test(String(it)); // Boolean
  286. },
  287. extend: function(ctor, props){
  288. // summary:
  289. // Adds all properties and methods of props to constructor's
  290. // prototype, making them available to all instances created with
  291. // constructor.
  292. // ctor: Object
  293. // Target constructor to extend.
  294. // props: Object
  295. // One or more objects to mix into ctor.prototype
  296. for(var i=1, l=arguments.length; i<l; i++){
  297. lang._mixin(ctor.prototype, arguments[i]);
  298. }
  299. return ctor; // Object
  300. },
  301. _hitchArgs: function(scope, method){
  302. var pre = lang._toArray(arguments, 2);
  303. var named = lang.isString(method);
  304. return function(){
  305. // arrayify arguments
  306. var args = lang._toArray(arguments);
  307. // locate our method
  308. var f = named ? (scope||dojo.global)[method] : method;
  309. // invoke with collected args
  310. return f && f.apply(scope || this, pre.concat(args)); // mixed
  311. }; // Function
  312. },
  313. hitch: function(scope, method){
  314. // summary:
  315. // Returns a function that will only ever execute in the given scope.
  316. // This allows for easy use of object member functions
  317. // in callbacks and other places in which the "this" keyword may
  318. // otherwise not reference the expected scope.
  319. // Any number of default positional arguments may be passed as parameters
  320. // beyond "method".
  321. // Each of these values will be used to "placehold" (similar to curry)
  322. // for the hitched function.
  323. // scope: Object
  324. // The scope to use when method executes. If method is a string,
  325. // scope is also the object containing method.
  326. // method: Function|String...
  327. // A function to be hitched to scope, or the name of the method in
  328. // scope to be hitched.
  329. // example:
  330. // | lang.hitch(foo, "bar")();
  331. // runs foo.bar() in the scope of foo
  332. // example:
  333. // | lang.hitch(foo, myFunction);
  334. // returns a function that runs myFunction in the scope of foo
  335. // example:
  336. // Expansion on the default positional arguments passed along from
  337. // hitch. Passed args are mixed first, additional args after.
  338. // | var foo = { bar: function(a, b, c){ console.log(a, b, c); } };
  339. // | var fn = lang.hitch(foo, "bar", 1, 2);
  340. // | fn(3); // logs "1, 2, 3"
  341. // example:
  342. // | var foo = { bar: 2 };
  343. // | lang.hitch(foo, function(){ this.bar = 10; })();
  344. // execute an anonymous function in scope of foo
  345. if(arguments.length > 2){
  346. return lang._hitchArgs.apply(dojo, arguments); // Function
  347. }
  348. if(!method){
  349. method = scope;
  350. scope = null;
  351. }
  352. if(lang.isString(method)){
  353. scope = scope || dojo.global;
  354. if(!scope[method]){ throw(['lang.hitch: scope["', method, '"] is null (scope="', scope, '")'].join('')); }
  355. return function(){ return scope[method].apply(scope, arguments || []); }; // Function
  356. }
  357. return !scope ? method : function(){ return method.apply(scope, arguments || []); }; // Function
  358. },
  359. delegate: (function(){
  360. // boodman/crockford delegation w/ cornford optimization
  361. function TMP(){}
  362. return function(obj, props){
  363. TMP.prototype = obj;
  364. var tmp = new TMP();
  365. TMP.prototype = null;
  366. if(props){
  367. lang._mixin(tmp, props);
  368. }
  369. return tmp; // Object
  370. };
  371. })(),
  372. /*=====
  373. delegate: function(obj, props){
  374. // summary:
  375. // Returns a new object which "looks" to obj for properties which it
  376. // does not have a value for. Optionally takes a bag of properties to
  377. // seed the returned object with initially.
  378. // description:
  379. // This is a small implementation of the Boodman/Crockford delegation
  380. // pattern in JavaScript. An intermediate object constructor mediates
  381. // the prototype chain for the returned object, using it to delegate
  382. // down to obj for property lookup when object-local lookup fails.
  383. // This can be thought of similarly to ES4's "wrap", save that it does
  384. // not act on types but rather on pure objects.
  385. // obj: Object
  386. // The object to delegate to for properties not found directly on the
  387. // return object or in props.
  388. // props: Object...
  389. // an object containing properties to assign to the returned object
  390. // returns:
  391. // an Object of anonymous type
  392. // example:
  393. // | var foo = { bar: "baz" };
  394. // | var thinger = lang.delegate(foo, { thud: "xyzzy"});
  395. // | thinger.bar == "baz"; // delegated to foo
  396. // | foo.thud == undefined; // by definition
  397. // | thinger.thud == "xyzzy"; // mixed in from props
  398. // | foo.bar = "thonk";
  399. // | thinger.bar == "thonk"; // still delegated to foo's bar
  400. },
  401. =====*/
  402. _toArray: has("ie") ?
  403. (function(){
  404. function slow(obj, offset, startWith){
  405. var arr = startWith||[];
  406. for(var x = offset || 0; x < obj.length; x++){
  407. arr.push(obj[x]);
  408. }
  409. return arr;
  410. }
  411. return function(obj){
  412. return ((obj.item) ? slow : efficient).apply(this, arguments);
  413. };
  414. })() : efficient,
  415. /*=====
  416. _toArray: function(obj, offset, startWith){
  417. // summary:
  418. // Converts an array-like object (i.e. arguments, DOMCollection) to an
  419. // array. Returns a new Array with the elements of obj.
  420. // obj: Object
  421. // the object to "arrayify". We expect the object to have, at a
  422. // minimum, a length property which corresponds to integer-indexed
  423. // properties.
  424. // offset: Number?
  425. // the location in obj to start iterating from. Defaults to 0.
  426. // Optional.
  427. // startWith: Array?
  428. // An array to pack with the properties of obj. If provided,
  429. // properties in obj are appended at the end of startWith and
  430. // startWith is the returned array.
  431. },
  432. =====*/
  433. partial: function(/*Function|String*/ method /*, ...*/){
  434. // summary:
  435. // similar to hitch() except that the scope object is left to be
  436. // whatever the execution context eventually becomes.
  437. // description:
  438. // Calling lang.partial is the functional equivalent of calling:
  439. // | lang.hitch(null, funcName, ...);
  440. // method:
  441. // The function to "wrap"
  442. var arr = [ null ];
  443. return lang.hitch.apply(dojo, arr.concat(lang._toArray(arguments))); // Function
  444. },
  445. clone: function(/*anything*/ src){
  446. // summary:
  447. // Clones objects (including DOM nodes) and all children.
  448. // Warning: do not clone cyclic structures.
  449. // src:
  450. // The object to clone
  451. if(!src || typeof src != "object" || lang.isFunction(src)){
  452. // null, undefined, any non-object, or function
  453. return src; // anything
  454. }
  455. if(src.nodeType && "cloneNode" in src){
  456. // DOM Node
  457. return src.cloneNode(true); // Node
  458. }
  459. if(src instanceof Date){
  460. // Date
  461. return new Date(src.getTime()); // Date
  462. }
  463. if(src instanceof RegExp){
  464. // RegExp
  465. return new RegExp(src); // RegExp
  466. }
  467. var r, i, l;
  468. if(lang.isArray(src)){
  469. // array
  470. r = [];
  471. for(i = 0, l = src.length; i < l; ++i){
  472. if(i in src){
  473. r.push(lang.clone(src[i]));
  474. }
  475. }
  476. // we don't clone functions for performance reasons
  477. // }else if(d.isFunction(src)){
  478. // // function
  479. // r = function(){ return src.apply(this, arguments); };
  480. }else{
  481. // generic objects
  482. r = src.constructor ? new src.constructor() : {};
  483. }
  484. return lang._mixin(r, src, lang.clone);
  485. },
  486. trim: String.prototype.trim ?
  487. function(str){ return str.trim(); } :
  488. function(str){ return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); },
  489. /*=====
  490. trim: function(str){
  491. // summary:
  492. // Trims whitespace from both sides of the string
  493. // str: String
  494. // String to be trimmed
  495. // returns: String
  496. // Returns the trimmed string
  497. // description:
  498. // This version of trim() was selected for inclusion into the base due
  499. // to its compact size and relatively good performance
  500. // (see [Steven Levithan's blog](http://blog.stevenlevithan.com/archives/faster-trim-javascript)
  501. // Uses String.prototype.trim instead, if available.
  502. // The fastest but longest version of this function is located at
  503. // lang.string.trim()
  504. },
  505. =====*/
  506. replace: function(tmpl, map, pattern){
  507. // summary:
  508. // Performs parameterized substitutions on a string. Throws an
  509. // exception if any parameter is unmatched.
  510. // tmpl: String
  511. // String to be used as a template.
  512. // map: Object|Function
  513. // If an object, it is used as a dictionary to look up substitutions.
  514. // If a function, it is called for every substitution with following parameters:
  515. // a whole match, a name, an offset, and the whole template
  516. // string (see https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/String/replace
  517. // for more details).
  518. // pattern: RegEx?
  519. // Optional regular expression objects that overrides the default pattern.
  520. // Must be global and match one item. The default is: /\{([^\}]+)\}/g,
  521. // which matches patterns like that: "{xxx}", where "xxx" is any sequence
  522. // of characters, which doesn't include "}".
  523. // returns: String
  524. // Returns the substituted string.
  525. // example:
  526. // | // uses a dictionary for substitutions:
  527. // | lang.replace("Hello, {name.first} {name.last} AKA {nick}!",
  528. // | {
  529. // | nick: "Bob",
  530. // | name: {
  531. // | first: "Robert",
  532. // | middle: "X",
  533. // | last: "Cringely"
  534. // | }
  535. // | });
  536. // | // returns: Hello, Robert Cringely AKA Bob!
  537. // example:
  538. // | // uses an array for substitutions:
  539. // | lang.replace("Hello, {0} {2}!",
  540. // | ["Robert", "X", "Cringely"]);
  541. // | // returns: Hello, Robert Cringely!
  542. // example:
  543. // | // uses a function for substitutions:
  544. // | function sum(a){
  545. // | var t = 0;
  546. // | arrayforEach(a, function(x){ t += x; });
  547. // | return t;
  548. // | }
  549. // | lang.replace(
  550. // | "{count} payments averaging {avg} USD per payment.",
  551. // | lang.hitch(
  552. // | { payments: [11, 16, 12] },
  553. // | function(_, key){
  554. // | switch(key){
  555. // | case "count": return this.payments.length;
  556. // | case "min": return Math.min.apply(Math, this.payments);
  557. // | case "max": return Math.max.apply(Math, this.payments);
  558. // | case "sum": return sum(this.payments);
  559. // | case "avg": return sum(this.payments) / this.payments.length;
  560. // | }
  561. // | }
  562. // | )
  563. // | );
  564. // | // prints: 3 payments averaging 13 USD per payment.
  565. // example:
  566. // | // uses an alternative PHP-like pattern for substitutions:
  567. // | lang.replace("Hello, ${0} ${2}!",
  568. // | ["Robert", "X", "Cringely"], /\$\{([^\}]+)\}/g);
  569. // | // returns: Hello, Robert Cringely!
  570. return tmpl.replace(pattern || _pattern, lang.isFunction(map) ?
  571. map : function(_, k){ return lang.getObject(k, false, map); });
  572. }
  573. };
  574. has("extend-dojo") && lang.mixin(dojo, lang);
  575. return lang;
  576. });