|
/*! |
|
* @overview Ember - JavaScript Application Framework |
|
* @copyright Copyright 2011-2014 Tilde Inc. and contributors |
|
* Portions Copyright 2006-2011 Strobe Inc. |
|
* Portions Copyright 2008-2011 Apple Inc. All rights reserved. |
|
* @license Licensed under MIT license |
|
* See https://raw.github.com/emberjs/ember.js/master/LICENSE |
|
* @version 1.5.0 |
|
*/ |
|
|
|
|
|
(function() { |
|
/*global __fail__*/ |
|
|
|
/** |
|
Ember Debug |
|
|
|
@module ember |
|
@submodule ember-debug |
|
*/ |
|
|
|
/** |
|
@class Ember |
|
*/ |
|
|
|
if ('undefined' === typeof Ember) { |
|
Ember = {}; |
|
|
|
if ('undefined' !== typeof window) { |
|
window.Em = window.Ember = Em = Ember; |
|
} |
|
} |
|
|
|
// This needs to be kept in sync with the logic in |
|
// `packages/ember-metal/lib/core.js`. |
|
// |
|
// This is duplicated here to ensure that `Ember.ENV` |
|
// is setup even if `Ember` is not loaded yet. |
|
if (Ember.ENV) { |
|
// do nothing if Ember.ENV is already setup |
|
} else if ('undefined' !== typeof EmberENV) { |
|
Ember.ENV = EmberENV; |
|
} else if('undefined' !== typeof ENV) { |
|
Ember.ENV = ENV; |
|
} else { |
|
Ember.ENV = {}; |
|
} |
|
|
|
if (!('MANDATORY_SETTER' in Ember.ENV)) { |
|
Ember.ENV.MANDATORY_SETTER = true; // default to true for debug dist |
|
} |
|
|
|
/** |
|
Define an assertion that will throw an exception if the condition is not |
|
met. Ember build tools will remove any calls to `Ember.assert()` when |
|
doing a production build. Example: |
|
|
|
```javascript |
|
// Test for truthiness |
|
Ember.assert('Must pass a valid object', obj); |
|
// Fail unconditionally |
|
Ember.assert('This code path should never be run') |
|
``` |
|
|
|
@method assert |
|
@param {String} desc A description of the assertion. This will become |
|
the text of the Error thrown if the assertion fails. |
|
@param {Boolean} test Must be truthy for the assertion to pass. If |
|
falsy, an exception will be thrown. |
|
*/ |
|
Ember.assert = function(desc, test) { |
|
if (!test) { |
|
throw new Ember.Error("Assertion Failed: " + desc); |
|
} |
|
}; |
|
|
|
|
|
/** |
|
Display a warning with the provided message. Ember build tools will |
|
remove any calls to `Ember.warn()` when doing a production build. |
|
|
|
@method warn |
|
@param {String} message A warning to display. |
|
@param {Boolean} test An optional boolean. If falsy, the warning |
|
will be displayed. |
|
*/ |
|
Ember.warn = function(message, test) { |
|
if (!test) { |
|
Ember.Logger.warn("WARNING: "+message); |
|
if ('trace' in Ember.Logger) Ember.Logger.trace(); |
|
} |
|
}; |
|
|
|
/** |
|
Display a debug notice. Ember build tools will remove any calls to |
|
`Ember.debug()` when doing a production build. |
|
|
|
```javascript |
|
Ember.debug("I'm a debug notice!"); |
|
``` |
|
|
|
@method debug |
|
@param {String} message A debug message to display. |
|
*/ |
|
Ember.debug = function(message) { |
|
Ember.Logger.debug("DEBUG: "+message); |
|
}; |
|
|
|
/** |
|
Display a deprecation warning with the provided message and a stack trace |
|
(Chrome and Firefox only). Ember build tools will remove any calls to |
|
`Ember.deprecate()` when doing a production build. |
|
|
|
@method deprecate |
|
@param {String} message A description of the deprecation. |
|
@param {Boolean} test An optional boolean. If falsy, the deprecation |
|
will be displayed. |
|
*/ |
|
Ember.deprecate = function(message, test) { |
|
if (test) { return; } |
|
|
|
if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Ember.Error(message); } |
|
|
|
var error; |
|
|
|
// When using new Error, we can't do the arguments check for Chrome. Alternatives are welcome |
|
try { __fail__.fail(); } catch (e) { error = e; } |
|
|
|
if (Ember.LOG_STACKTRACE_ON_DEPRECATION && error.stack) { |
|
var stack, stackStr = ''; |
|
if (error['arguments']) { |
|
// Chrome |
|
stack = error.stack.replace(/^\s+at\s+/gm, ''). |
|
replace(/^([^\(]+?)([\n$])/gm, '{anonymous}($1)$2'). |
|
replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}($1)').split('\n'); |
|
stack.shift(); |
|
} else { |
|
// Firefox |
|
stack = error.stack.replace(/(?:\n@:0)?\s+$/m, ''). |
|
replace(/^\(/gm, '{anonymous}(').split('\n'); |
|
} |
|
|
|
stackStr = "\n " + stack.slice(2).join("\n "); |
|
message = message + stackStr; |
|
} |
|
|
|
Ember.Logger.warn("DEPRECATION: "+message); |
|
}; |
|
|
|
|
|
|
|
/** |
|
Alias an old, deprecated method with its new counterpart. |
|
|
|
Display a deprecation warning with the provided message and a stack trace |
|
(Chrome and Firefox only) when the assigned method is called. |
|
|
|
Ember build tools will not remove calls to `Ember.deprecateFunc()`, though |
|
no warnings will be shown in production. |
|
|
|
```javascript |
|
Ember.oldMethod = Ember.deprecateFunc("Please use the new, updated method", Ember.newMethod); |
|
``` |
|
|
|
@method deprecateFunc |
|
@param {String} message A description of the deprecation. |
|
@param {Function} func The new function called to replace its deprecated counterpart. |
|
@return {Function} a new function that wrapped the original function with a deprecation warning |
|
*/ |
|
Ember.deprecateFunc = function(message, func) { |
|
return function() { |
|
Ember.deprecate(message); |
|
return func.apply(this, arguments); |
|
}; |
|
}; |
|
|
|
|
|
/** |
|
Run a function meant for debugging. Ember build tools will remove any calls to |
|
`Ember.runInDebug()` when doing a production build. |
|
|
|
```javascript |
|
Ember.runInDebug( function() { |
|
Ember.Handlebars.EachView.reopen({ |
|
didInsertElement: function() { |
|
console.log("I'm happy"); |
|
} |
|
}); |
|
}); |
|
``` |
|
|
|
@method runInDebug |
|
@param {Function} func The function to be executed. |
|
*/ |
|
Ember.runInDebug = function(func) { |
|
func() |
|
}; |
|
|
|
// Inform the developer about the Ember Inspector if not installed. |
|
if (!Ember.testing) { |
|
var isFirefox = typeof InstallTrigger !== 'undefined'; |
|
var isChrome = !!window.chrome && !window.opera; |
|
|
|
if (typeof window !== 'undefined' && (isFirefox || isChrome) && window.addEventListener) { |
|
window.addEventListener("load", function() { |
|
if (document.documentElement && document.documentElement.dataset && !document.documentElement.dataset.emberExtension) { |
|
var downloadURL; |
|
|
|
if(isChrome) { |
|
downloadURL = 'https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi'; |
|
} else if(isFirefox) { |
|
downloadURL = 'https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/'; |
|
} |
|
|
|
Ember.debug('For more advanced debugging, install the Ember Inspector from ' + downloadURL); |
|
} |
|
}, false); |
|
} |
|
} |
|
|
|
})(); |
|
|
|
/*! |
|
* @overview Ember - JavaScript Application Framework |
|
* @copyright Copyright 2011-2014 Tilde Inc. and contributors |
|
* Portions Copyright 2006-2011 Strobe Inc. |
|
* Portions Copyright 2008-2011 Apple Inc. All rights reserved. |
|
* @license Licensed under MIT license |
|
* See https://raw.github.com/emberjs/ember.js/master/LICENSE |
|
* @version 1.5.0 |
|
*/ |
|
|
|
|
|
(function() { |
|
var define, requireModule, require, requirejs; |
|
|
|
(function() { |
|
var registry = {}, seen = {}; |
|
|
|
define = function(name, deps, callback) { |
|
registry[name] = { deps: deps, callback: callback }; |
|
}; |
|
|
|
requirejs = require = requireModule = function(name) { |
|
requirejs._eak_seen = registry; |
|
|
|
if (seen[name]) { return seen[name]; } |
|
seen[name] = {}; |
|
|
|
if (!registry[name]) { |
|
throw new Error("Could not find module " + name); |
|
} |
|
|
|
var mod = registry[name], |
|
deps = mod.deps, |
|
callback = mod.callback, |
|
reified = [], |
|
exports; |
|
|
|
for (var i=0, l=deps.length; i<l; i++) { |
|
if (deps[i] === 'exports') { |
|
reified.push(exports = {}); |
|
} else { |
|
reified.push(requireModule(resolve(deps[i]))); |
|
} |
|
} |
|
|
|
var value = callback.apply(this, reified); |
|
return seen[name] = exports || value; |
|
|
|
function resolve(child) { |
|
if (child.charAt(0) !== '.') { return child; } |
|
var parts = child.split("/"); |
|
var parentBase = name.split("/").slice(0, -1); |
|
|
|
for (var i=0, l=parts.length; i<l; i++) { |
|
var part = parts[i]; |
|
|
|
if (part === '..') { parentBase.pop(); } |
|
else if (part === '.') { continue; } |
|
else { parentBase.push(part); } |
|
} |
|
|
|
return parentBase.join("/"); |
|
} |
|
}; |
|
})(); |
|
(function() { |
|
/*globals Em:true ENV EmberENV MetamorphENV:true */ |
|
|
|
/** |
|
@module ember |
|
@submodule ember-metal |
|
*/ |
|
|
|
/** |
|
All Ember methods and functions are defined inside of this namespace. You |
|
generally should not add new properties to this namespace as it may be |
|
overwritten by future versions of Ember. |
|
|
|
You can also use the shorthand `Em` instead of `Ember`. |
|
|
|
Ember-Runtime is a framework that provides core functions for Ember including |
|
cross-platform functions, support for property observing and objects. Its |
|
focus is on small size and performance. You can use this in place of or |
|
along-side other cross-platform libraries such as jQuery. |
|
|
|
The core Runtime framework is based on the jQuery API with a number of |
|
performance optimizations. |
|
|
|
@class Ember |
|
@static |
|
@version 1.5.0 |
|
*/ |
|
|
|
if ('undefined' === typeof Ember) { |
|
// Create core object. Make it act like an instance of Ember.Namespace so that |
|
// objects assigned to it are given a sane string representation. |
|
Ember = {}; |
|
} |
|
|
|
// Default imports, exports and lookup to the global object; |
|
var imports = Ember.imports = Ember.imports || this; |
|
var exports = Ember.exports = Ember.exports || this; |
|
var lookup = Ember.lookup = Ember.lookup || this; |
|
|
|
// aliases needed to keep minifiers from removing the global context |
|
exports.Em = exports.Ember = Em = Ember; |
|
|
|
// Make sure these are set whether Ember was already defined or not |
|
|
|
Ember.isNamespace = true; |
|
|
|
Ember.toString = function() { return "Ember"; }; |
|
|
|
|
|
/** |
|
@property VERSION |
|
@type String |
|
@default '1.5.0' |
|
@static |
|
*/ |
|
Ember.VERSION = '1.5.0'; |
|
|
|
/** |
|
Standard environmental variables. You can define these in a global `EmberENV` |
|
variable before loading Ember to control various configuration settings. |
|
|
|
For backwards compatibility with earlier versions of Ember the global `ENV` |
|
variable will be used if `EmberENV` is not defined. |
|
|
|
@property ENV |
|
@type Hash |
|
*/ |
|
|
|
// This needs to be kept in sync with the logic in |
|
// `packages/ember-debug/lib/main.js`. |
|
if (Ember.ENV) { |
|
// do nothing if Ember.ENV is already setup |
|
} else if ('undefined' !== typeof EmberENV) { |
|
Ember.ENV = EmberENV; |
|
} else if('undefined' !== typeof ENV) { |
|
Ember.ENV = ENV; |
|
} else { |
|
Ember.ENV = {}; |
|
} |
|
|
|
Ember.config = Ember.config || {}; |
|
|
|
// We disable the RANGE API by default for performance reasons |
|
if ('undefined' === typeof Ember.ENV.DISABLE_RANGE_API) { |
|
Ember.ENV.DISABLE_RANGE_API = true; |
|
} |
|
|
|
if ("undefined" === typeof MetamorphENV) { |
|
exports.MetamorphENV = {}; |
|
} |
|
|
|
MetamorphENV.DISABLE_RANGE_API = Ember.ENV.DISABLE_RANGE_API; |
|
|
|
/** |
|
Hash of enabled Canary features. Add to before creating your application. |
|
|
|
You can also define `ENV.FEATURES` if you need to enable features flagged at runtime. |
|
|
|
@property FEATURES |
|
@type Hash |
|
*/ |
|
|
|
Ember.FEATURES = Ember.ENV.FEATURES || {}; |
|
|
|
/** |
|
Test that a feature is enabled. Parsed by Ember's build tools to leave |
|
experimental features out of beta/stable builds. |
|
|
|
You can define the following configuration options: |
|
|
|
* `ENV.ENABLE_ALL_FEATURES` - force all features to be enabled. |
|
* `ENV.ENABLE_OPTIONAL_FEATURES` - enable any features that have not been explicitly |
|
enabled/disabled. |
|
|
|
@method isEnabled |
|
@param {string} feature |
|
*/ |
|
|
|
Ember.FEATURES.isEnabled = function(feature) { |
|
var featureValue = Ember.FEATURES[feature]; |
|
|
|
if (Ember.ENV.ENABLE_ALL_FEATURES) { |
|
return true; |
|
} else if (featureValue === true || featureValue === false || featureValue === undefined) { |
|
return featureValue; |
|
} else if (Ember.ENV.ENABLE_OPTIONAL_FEATURES) { |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
}; |
|
|
|
// .......................................................... |
|
// BOOTSTRAP |
|
// |
|
|
|
/** |
|
Determines whether Ember should enhances some built-in object prototypes to |
|
provide a more friendly API. If enabled, a few methods will be added to |
|
`Function`, `String`, and `Array`. `Object.prototype` will not be enhanced, |
|
which is the one that causes most trouble for people. |
|
|
|
In general we recommend leaving this option set to true since it rarely |
|
conflicts with other code. If you need to turn it off however, you can |
|
define an `ENV.EXTEND_PROTOTYPES` config to disable it. |
|
|
|
@property EXTEND_PROTOTYPES |
|
@type Boolean |
|
@default true |
|
*/ |
|
Ember.EXTEND_PROTOTYPES = Ember.ENV.EXTEND_PROTOTYPES; |
|
|
|
if (typeof Ember.EXTEND_PROTOTYPES === 'undefined') { |
|
Ember.EXTEND_PROTOTYPES = true; |
|
} |
|
|
|
/** |
|
Determines whether Ember logs a full stack trace during deprecation warnings |
|
|
|
@property LOG_STACKTRACE_ON_DEPRECATION |
|
@type Boolean |
|
@default true |
|
*/ |
|
Ember.LOG_STACKTRACE_ON_DEPRECATION = (Ember.ENV.LOG_STACKTRACE_ON_DEPRECATION !== false); |
|
|
|
/** |
|
Determines whether Ember should add ECMAScript 5 shims to older browsers. |
|
|
|
@property SHIM_ES5 |
|
@type Boolean |
|
@default Ember.EXTEND_PROTOTYPES |
|
*/ |
|
Ember.SHIM_ES5 = (Ember.ENV.SHIM_ES5 === false) ? false : Ember.EXTEND_PROTOTYPES; |
|
|
|
/** |
|
Determines whether Ember logs info about version of used libraries |
|
|
|
@property LOG_VERSION |
|
@type Boolean |
|
@default true |
|
*/ |
|
Ember.LOG_VERSION = (Ember.ENV.LOG_VERSION === false) ? false : true; |
|
|
|
/** |
|
Empty function. Useful for some operations. Always returns `this`. |
|
|
|
@method K |
|
@private |
|
@return {Object} |
|
*/ |
|
Ember.K = function() { return this; }; |
|
|
|
|
|
// Stub out the methods defined by the ember-debug package in case it's not loaded |
|
|
|
if ('undefined' === typeof Ember.assert) { Ember.assert = Ember.K; } |
|
if ('undefined' === typeof Ember.warn) { Ember.warn = Ember.K; } |
|
if ('undefined' === typeof Ember.debug) { Ember.debug = Ember.K; } |
|
if ('undefined' === typeof Ember.runInDebug) { Ember.runInDebug = Ember.K; } |
|
if ('undefined' === typeof Ember.deprecate) { Ember.deprecate = Ember.K; } |
|
if ('undefined' === typeof Ember.deprecateFunc) { |
|
Ember.deprecateFunc = function(_, func) { return func; }; |
|
} |
|
|
|
/** |
|
Previously we used `Ember.$.uuid`, however `$.uuid` has been removed from |
|
jQuery master. We'll just bootstrap our own uuid now. |
|
|
|
@property uuid |
|
@type Number |
|
@private |
|
*/ |
|
Ember.uuid = 0; |
|
|
|
/** |
|
Merge the contents of two objects together into the first object. |
|
|
|
```javascript |
|
Ember.merge({first: 'Tom'}, {last: 'Dale'}); // {first: 'Tom', last: 'Dale'} |
|
var a = {first: 'Yehuda'}, b = {last: 'Katz'}; |
|
Ember.merge(a, b); // a == {first: 'Yehuda', last: 'Katz'}, b == {last: 'Katz'} |
|
``` |
|
|
|
@method merge |
|
@for Ember |
|
@param {Object} original The object to merge into |
|
@param {Object} updates The object to copy properties from |
|
@return {Object} |
|
*/ |
|
Ember.merge = function(original, updates) { |
|
for (var prop in updates) { |
|
if (!updates.hasOwnProperty(prop)) { continue; } |
|
original[prop] = updates[prop]; |
|
} |
|
return original; |
|
}; |
|
|
|
/** |
|
Returns true if the passed value is null or undefined. This avoids errors |
|
from JSLint complaining about use of ==, which can be technically |
|
confusing. |
|
|
|
```javascript |
|
Ember.isNone(); // true |
|
Ember.isNone(null); // true |
|
Ember.isNone(undefined); // true |
|
Ember.isNone(''); // false |
|
Ember.isNone([]); // false |
|
Ember.isNone(function() {}); // false |
|
``` |
|
|
|
@method isNone |
|
@for Ember |
|
@param {Object} obj Value to test |
|
@return {Boolean} |
|
*/ |
|
Ember.isNone = function(obj) { |
|
return obj === null || obj === undefined; |
|
}; |
|
Ember.none = Ember.deprecateFunc("Ember.none is deprecated. Please use Ember.isNone instead.", Ember.isNone); |
|
|
|
/** |
|
Verifies that a value is `null` or an empty string, empty array, |
|
or empty function. |
|
|
|
Constrains the rules on `Ember.isNone` by returning false for empty |
|
string and empty arrays. |
|
|
|
```javascript |
|
Ember.isEmpty(); // true |
|
Ember.isEmpty(null); // true |
|
Ember.isEmpty(undefined); // true |
|
Ember.isEmpty(''); // true |
|
Ember.isEmpty([]); // true |
|
Ember.isEmpty('Adam Hawkins'); // false |
|
Ember.isEmpty([0,1,2]); // false |
|
``` |
|
|
|
@method isEmpty |
|
@for Ember |
|
@param {Object} obj Value to test |
|
@return {Boolean} |
|
*/ |
|
Ember.isEmpty = function(obj) { |
|
return Ember.isNone(obj) || (obj.length === 0 && typeof obj !== 'function') || (typeof obj === 'object' && Ember.get(obj, 'length') === 0); |
|
}; |
|
Ember.empty = Ember.deprecateFunc("Ember.empty is deprecated. Please use Ember.isEmpty instead.", Ember.isEmpty); |
|
|
|
|
|
/** |
|
A value is blank if it is empty or a whitespace string. |
|
|
|
```javascript |
|
Ember.isBlank(); // true |
|
Ember.isBlank(null); // true |
|
Ember.isBlank(undefined); // true |
|
Ember.isBlank(''); // true |
|
Ember.isBlank([]); // true |
|
Ember.isBlank('\n\t'); // true |
|
Ember.isBlank(' '); // true |
|
Ember.isBlank({}); // false |
|
Ember.isBlank('\n\t Hello'); // false |
|
Ember.isBlank('Hello world'); // false |
|
Ember.isBlank([1,2,3]); // false |
|
``` |
|
|
|
@method isBlank |
|
@for Ember |
|
@param {Object} obj Value to test |
|
@return {Boolean} |
|
*/ |
|
Ember.isBlank = function(obj) { |
|
return Ember.isEmpty(obj) || (typeof obj === 'string' && obj.match(/\S/) === null); |
|
}; |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/*globals Node */ |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
/** |
|
Platform specific methods and feature detectors needed by the framework. |
|
|
|
@class platform |
|
@namespace Ember |
|
@static |
|
*/ |
|
var platform = Ember.platform = {}; |
|
|
|
|
|
/** |
|
Identical to `Object.create()`. Implements if not available natively. |
|
|
|
@method create |
|
@for Ember |
|
*/ |
|
Ember.create = Object.create; |
|
|
|
// IE8 has Object.create but it couldn't treat property descriptors. |
|
if (Ember.create) { |
|
if (Ember.create({a: 1}, {a: {value: 2}}).a !== 2) { |
|
Ember.create = null; |
|
} |
|
} |
|
|
|
// STUB_OBJECT_CREATE allows us to override other libraries that stub |
|
// Object.create different than we would prefer |
|
if (!Ember.create || Ember.ENV.STUB_OBJECT_CREATE) { |
|
var K = function() {}; |
|
|
|
Ember.create = function(obj, props) { |
|
K.prototype = obj; |
|
obj = new K(); |
|
if (props) { |
|
K.prototype = obj; |
|
for (var prop in props) { |
|
K.prototype[prop] = props[prop].value; |
|
} |
|
obj = new K(); |
|
} |
|
K.prototype = null; |
|
|
|
return obj; |
|
}; |
|
|
|
Ember.create.isSimulated = true; |
|
} |
|
|
|
var defineProperty = Object.defineProperty; |
|
var canRedefineProperties, canDefinePropertyOnDOM; |
|
|
|
// Catch IE8 where Object.defineProperty exists but only works on DOM elements |
|
if (defineProperty) { |
|
try { |
|
defineProperty({}, 'a',{get:function() {}}); |
|
} catch (e) { |
|
defineProperty = null; |
|
} |
|
} |
|
|
|
if (defineProperty) { |
|
// Detects a bug in Android <3.2 where you cannot redefine a property using |
|
// Object.defineProperty once accessors have already been set. |
|
canRedefineProperties = (function() { |
|
var obj = {}; |
|
|
|
defineProperty(obj, 'a', { |
|
configurable: true, |
|
enumerable: true, |
|
get: function() { }, |
|
set: function() { } |
|
}); |
|
|
|
defineProperty(obj, 'a', { |
|
configurable: true, |
|
enumerable: true, |
|
writable: true, |
|
value: true |
|
}); |
|
|
|
return obj.a === true; |
|
})(); |
|
|
|
// This is for Safari 5.0, which supports Object.defineProperty, but not |
|
// on DOM nodes. |
|
canDefinePropertyOnDOM = (function() { |
|
try { |
|
defineProperty(document.createElement('div'), 'definePropertyOnDOM', {}); |
|
return true; |
|
} catch(e) { } |
|
|
|
return false; |
|
})(); |
|
|
|
if (!canRedefineProperties) { |
|
defineProperty = null; |
|
} else if (!canDefinePropertyOnDOM) { |
|
defineProperty = function(obj, keyName, desc) { |
|
var isNode; |
|
|
|
if (typeof Node === "object") { |
|
isNode = obj instanceof Node; |
|
} else { |
|
isNode = typeof obj === "object" && typeof obj.nodeType === "number" && typeof obj.nodeName === "string"; |
|
} |
|
|
|
if (isNode) { |
|
// TODO: Should we have a warning here? |
|
return (obj[keyName] = desc.value); |
|
} else { |
|
return Object.defineProperty(obj, keyName, desc); |
|
} |
|
}; |
|
} |
|
} |
|
|
|
/** |
|
@class platform |
|
@namespace Ember |
|
*/ |
|
|
|
/** |
|
Identical to `Object.defineProperty()`. Implements as much functionality |
|
as possible if not available natively. |
|
|
|
@method defineProperty |
|
@param {Object} obj The object to modify |
|
@param {String} keyName property name to modify |
|
@param {Object} desc descriptor hash |
|
@return {void} |
|
*/ |
|
platform.defineProperty = defineProperty; |
|
|
|
/** |
|
Set to true if the platform supports native getters and setters. |
|
|
|
@property hasPropertyAccessors |
|
@final |
|
*/ |
|
platform.hasPropertyAccessors = true; |
|
|
|
if (!platform.defineProperty) { |
|
platform.hasPropertyAccessors = false; |
|
|
|
platform.defineProperty = function(obj, keyName, desc) { |
|
if (!desc.get) { obj[keyName] = desc.value; } |
|
}; |
|
|
|
platform.defineProperty.isSimulated = true; |
|
} |
|
|
|
if (Ember.ENV.MANDATORY_SETTER && !platform.hasPropertyAccessors) { |
|
Ember.ENV.MANDATORY_SETTER = false; |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/*jshint newcap:false*/ |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
// NOTE: There is a bug in jshint that doesn't recognize `Object()` without `new` |
|
// as being ok unless both `newcap:false` and not `use strict`. |
|
// https://github.com/jshint/jshint/issues/392 |
|
|
|
// Testing this is not ideal, but we want to use native functions |
|
// if available, but not to use versions created by libraries like Prototype |
|
var isNativeFunc = function(func) { |
|
// This should probably work in all browsers likely to have ES5 array methods |
|
return func && Function.prototype.toString.call(func).indexOf('[native code]') > -1; |
|
}; |
|
|
|
// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/map |
|
var arrayMap = isNativeFunc(Array.prototype.map) ? Array.prototype.map : function(fun /*, thisp */) { |
|
//"use strict"; |
|
|
|
if (this === void 0 || this === null) { |
|
throw new TypeError(); |
|
} |
|
|
|
var t = Object(this); |
|
var len = t.length >>> 0; |
|
if (typeof fun !== "function") { |
|
throw new TypeError(); |
|
} |
|
|
|
var res = new Array(len); |
|
var thisp = arguments[1]; |
|
for (var i = 0; i < len; i++) { |
|
if (i in t) { |
|
res[i] = fun.call(thisp, t[i], i, t); |
|
} |
|
} |
|
|
|
return res; |
|
}; |
|
|
|
// From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/foreach |
|
var arrayForEach = isNativeFunc(Array.prototype.forEach) ? Array.prototype.forEach : function(fun /*, thisp */) { |
|
//"use strict"; |
|
|
|
if (this === void 0 || this === null) { |
|
throw new TypeError(); |
|
} |
|
|
|
var t = Object(this); |
|
var len = t.length >>> 0; |
|
if (typeof fun !== "function") { |
|
throw new TypeError(); |
|
} |
|
|
|
var thisp = arguments[1]; |
|
for (var i = 0; i < len; i++) { |
|
if (i in t) { |
|
fun.call(thisp, t[i], i, t); |
|
} |
|
} |
|
}; |
|
|
|
var arrayIndexOf = isNativeFunc(Array.prototype.indexOf) ? Array.prototype.indexOf : function (obj, fromIndex) { |
|
if (fromIndex === null || fromIndex === undefined) { fromIndex = 0; } |
|
else if (fromIndex < 0) { fromIndex = Math.max(0, this.length + fromIndex); } |
|
for (var i = fromIndex, j = this.length; i < j; i++) { |
|
if (this[i] === obj) { return i; } |
|
} |
|
return -1; |
|
}; |
|
|
|
var arrayFilter = isNativeFunc(Array.prototype.filter) ? Array.prototype.filter : function (fn, context) { |
|
var i, |
|
value, |
|
result = [], |
|
length = this.length; |
|
|
|
for (i = 0; i < length; i++) { |
|
if (this.hasOwnProperty(i)) { |
|
value = this[i]; |
|
if (fn.call(context, value, i, this)) { |
|
result.push(value); |
|
} |
|
} |
|
} |
|
return result; |
|
}; |
|
|
|
/** |
|
Array polyfills to support ES5 features in older browsers. |
|
|
|
@namespace Ember |
|
@property ArrayPolyfills |
|
*/ |
|
Ember.ArrayPolyfills = { |
|
map: arrayMap, |
|
forEach: arrayForEach, |
|
filter: arrayFilter, |
|
indexOf: arrayIndexOf |
|
}; |
|
|
|
if (Ember.SHIM_ES5) { |
|
if (!Array.prototype.map) { |
|
Array.prototype.map = arrayMap; |
|
} |
|
|
|
if (!Array.prototype.forEach) { |
|
Array.prototype.forEach = arrayForEach; |
|
} |
|
|
|
if (!Array.prototype.filter) { |
|
Array.prototype.filter = arrayFilter; |
|
} |
|
|
|
if (!Array.prototype.indexOf) { |
|
Array.prototype.indexOf = arrayIndexOf; |
|
} |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; |
|
|
|
/** |
|
A subclass of the JavaScript Error object for use in Ember. |
|
|
|
@class Error |
|
@namespace Ember |
|
@extends Error |
|
@constructor |
|
*/ |
|
Ember.Error = function() { |
|
var tmp = Error.apply(this, arguments); |
|
|
|
// Adds a `stack` property to the given error object that will yield the |
|
// stack trace at the time captureStackTrace was called. |
|
// When collecting the stack trace all frames above the topmost call |
|
// to this function, including that call, will be left out of the |
|
// stack trace. |
|
// This is useful because we can hide Ember implementation details |
|
// that are not very helpful for the user. |
|
if (Error.captureStackTrace) { |
|
Error.captureStackTrace(this, Ember.Error); |
|
} |
|
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. |
|
for (var idx = 0; idx < errorProps.length; idx++) { |
|
this[errorProps[idx]] = tmp[errorProps[idx]]; |
|
} |
|
}; |
|
|
|
Ember.Error.prototype = Ember.create(Error.prototype); |
|
|
|
// .......................................................... |
|
// ERROR HANDLING |
|
// |
|
|
|
/** |
|
A function may be assigned to `Ember.onerror` to be called when Ember |
|
internals encounter an error. This is useful for specialized error handling |
|
and reporting code. |
|
|
|
```javascript |
|
Ember.onerror = function(error) { |
|
Em.$.ajax('/report-error', 'POST', { |
|
stack: error.stack, |
|
otherInformation: 'whatever app state you want to provide' |
|
}); |
|
}; |
|
``` |
|
|
|
@event onerror |
|
@for Ember |
|
@param {Exception} error the error object |
|
*/ |
|
Ember.onerror = null; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
/** |
|
Prefix used for guids through out Ember. |
|
@private |
|
*/ |
|
Ember.GUID_PREFIX = 'ember'; |
|
|
|
|
|
var o_defineProperty = Ember.platform.defineProperty, |
|
o_create = Ember.create, |
|
// Used for guid generation... |
|
GUID_KEY = '__ember'+ (+ new Date()), |
|
numberCache = [], |
|
stringCache = {}, |
|
uuid = 0; |
|
|
|
var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; |
|
|
|
/** |
|
A unique key used to assign guids and other private metadata to objects. |
|
If you inspect an object in your browser debugger you will often see these. |
|
They can be safely ignored. |
|
|
|
On browsers that support it, these properties are added with enumeration |
|
disabled so they won't show up when you iterate over your properties. |
|
|
|
@private |
|
@property GUID_KEY |
|
@for Ember |
|
@type String |
|
@final |
|
*/ |
|
Ember.GUID_KEY = GUID_KEY; |
|
|
|
var GUID_DESC = { |
|
writable: false, |
|
configurable: false, |
|
enumerable: false, |
|
value: null |
|
}; |
|
|
|
/** |
|
Generates a new guid, optionally saving the guid to the object that you |
|
pass in. You will rarely need to use this method. Instead you should |
|
call `Ember.guidFor(obj)`, which return an existing guid if available. |
|
|
|
@private |
|
@method generateGuid |
|
@for Ember |
|
@param {Object} [obj] Object the guid will be used for. If passed in, the guid will |
|
be saved on the object and reused whenever you pass the same object |
|
again. |
|
|
|
If no object is passed, just generate a new guid. |
|
@param {String} [prefix] Prefix to place in front of the guid. Useful when you want to |
|
separate the guid into separate namespaces. |
|
@return {String} the guid |
|
*/ |
|
Ember.generateGuid = function generateGuid(obj, prefix) { |
|
if (!prefix) prefix = Ember.GUID_PREFIX; |
|
var ret = (prefix + (uuid++)); |
|
if (obj) { |
|
if (obj[GUID_KEY] === null) { |
|
obj[GUID_KEY] = ret; |
|
} else { |
|
GUID_DESC.value = ret; |
|
o_defineProperty(obj, GUID_KEY, GUID_DESC); |
|
} |
|
} |
|
return ret; |
|
}; |
|
|
|
/** |
|
Returns a unique id for the object. If the object does not yet have a guid, |
|
one will be assigned to it. You can call this on any object, |
|
`Ember.Object`-based or not, but be aware that it will add a `_guid` |
|
property. |
|
|
|
You can also use this method on DOM Element objects. |
|
|
|
@private |
|
@method guidFor |
|
@for Ember |
|
@param {Object} obj any object, string, number, Element, or primitive |
|
@return {String} the unique guid for this instance. |
|
*/ |
|
Ember.guidFor = function guidFor(obj) { |
|
|
|
// special cases where we don't want to add a key to object |
|
if (obj === undefined) return "(undefined)"; |
|
if (obj === null) return "(null)"; |
|
|
|
var ret; |
|
var type = typeof obj; |
|
|
|
// Don't allow prototype changes to String etc. to change the guidFor |
|
switch(type) { |
|
case 'number': |
|
ret = numberCache[obj]; |
|
if (!ret) ret = numberCache[obj] = 'nu'+obj; |
|
return ret; |
|
|
|
case 'string': |
|
ret = stringCache[obj]; |
|
if (!ret) ret = stringCache[obj] = 'st'+(uuid++); |
|
return ret; |
|
|
|
case 'boolean': |
|
return obj ? '(true)' : '(false)'; |
|
|
|
default: |
|
if (obj[GUID_KEY]) return obj[GUID_KEY]; |
|
if (obj === Object) return '(Object)'; |
|
if (obj === Array) return '(Array)'; |
|
ret = 'ember' + (uuid++); |
|
|
|
if (obj[GUID_KEY] === null) { |
|
obj[GUID_KEY] = ret; |
|
} else { |
|
GUID_DESC.value = ret; |
|
o_defineProperty(obj, GUID_KEY, GUID_DESC); |
|
} |
|
return ret; |
|
} |
|
}; |
|
|
|
// .......................................................... |
|
// META |
|
// |
|
|
|
var META_DESC = Ember.META_DESC = { |
|
writable: true, |
|
configurable: false, |
|
enumerable: false, |
|
value: null |
|
}; |
|
|
|
var META_KEY = Ember.GUID_KEY+'_meta'; |
|
|
|
/** |
|
The key used to store meta information on object for property observing. |
|
|
|
@property META_KEY |
|
@for Ember |
|
@private |
|
@final |
|
@type String |
|
*/ |
|
Ember.META_KEY = META_KEY; |
|
|
|
var isDefinePropertySimulated = Ember.platform.defineProperty.isSimulated; |
|
|
|
function Meta(obj) { |
|
this.descs = {}; |
|
this.watching = {}; |
|
this.cache = {}; |
|
this.cacheMeta = {}; |
|
this.source = obj; |
|
} |
|
|
|
Meta.prototype = { |
|
descs: null, |
|
deps: null, |
|
watching: null, |
|
listeners: null, |
|
cache: null, |
|
cacheMeta: null, |
|
source: null, |
|
mixins: null, |
|
bindings: null, |
|
chains: null, |
|
chainWatchers: null, |
|
values: null, |
|
proto: null |
|
}; |
|
|
|
if (isDefinePropertySimulated) { |
|
// on platforms that don't support enumerable false |
|
// make meta fail jQuery.isPlainObject() to hide from |
|
// jQuery.extend() by having a property that fails |
|
// hasOwnProperty check. |
|
Meta.prototype.__preventPlainObject__ = true; |
|
|
|
// Without non-enumerable properties, meta objects will be output in JSON |
|
// unless explicitly suppressed |
|
Meta.prototype.toJSON = function () { }; |
|
} |
|
|
|
// Placeholder for non-writable metas. |
|
var EMPTY_META = new Meta(null); |
|
|
|
if (MANDATORY_SETTER) { EMPTY_META.values = {}; } |
|
|
|
Ember.EMPTY_META = EMPTY_META; |
|
|
|
/** |
|
Retrieves the meta hash for an object. If `writable` is true ensures the |
|
hash is writable for this object as well. |
|
|
|
The meta object contains information about computed property descriptors as |
|
well as any watched properties and other information. You generally will |
|
not access this information directly but instead work with higher level |
|
methods that manipulate this hash indirectly. |
|
|
|
@method meta |
|
@for Ember |
|
@private |
|
|
|
@param {Object} obj The object to retrieve meta for |
|
@param {Boolean} [writable=true] Pass `false` if you do not intend to modify |
|
the meta hash, allowing the method to avoid making an unnecessary copy. |
|
@return {Object} the meta hash for an object |
|
*/ |
|
Ember.meta = function meta(obj, writable) { |
|
|
|
var ret = obj[META_KEY]; |
|
if (writable===false) return ret || EMPTY_META; |
|
|
|
if (!ret) { |
|
if (!isDefinePropertySimulated) o_defineProperty(obj, META_KEY, META_DESC); |
|
|
|
ret = new Meta(obj); |
|
|
|
if (MANDATORY_SETTER) { ret.values = {}; } |
|
|
|
obj[META_KEY] = ret; |
|
|
|
// make sure we don't accidentally try to create constructor like desc |
|
ret.descs.constructor = null; |
|
|
|
} else if (ret.source !== obj) { |
|
if (!isDefinePropertySimulated) o_defineProperty(obj, META_KEY, META_DESC); |
|
|
|
ret = o_create(ret); |
|
ret.descs = o_create(ret.descs); |
|
ret.watching = o_create(ret.watching); |
|
ret.cache = {}; |
|
ret.cacheMeta = {}; |
|
ret.source = obj; |
|
|
|
if (MANDATORY_SETTER) { ret.values = o_create(ret.values); } |
|
|
|
obj[META_KEY] = ret; |
|
} |
|
return ret; |
|
}; |
|
|
|
Ember.getMeta = function getMeta(obj, property) { |
|
var meta = Ember.meta(obj, false); |
|
return meta[property]; |
|
}; |
|
|
|
Ember.setMeta = function setMeta(obj, property, value) { |
|
var meta = Ember.meta(obj, true); |
|
meta[property] = value; |
|
return value; |
|
}; |
|
|
|
/** |
|
@deprecated |
|
@private |
|
|
|
In order to store defaults for a class, a prototype may need to create |
|
a default meta object, which will be inherited by any objects instantiated |
|
from the class's constructor. |
|
|
|
However, the properties of that meta object are only shallow-cloned, |
|
so if a property is a hash (like the event system's `listeners` hash), |
|
it will by default be shared across all instances of that class. |
|
|
|
This method allows extensions to deeply clone a series of nested hashes or |
|
other complex objects. For instance, the event system might pass |
|
`['listeners', 'foo:change', 'ember157']` to `prepareMetaPath`, which will |
|
walk down the keys provided. |
|
|
|
For each key, if the key does not exist, it is created. If it already |
|
exists and it was inherited from its constructor, the constructor's |
|
key is cloned. |
|
|
|
You can also pass false for `writable`, which will simply return |
|
undefined if `prepareMetaPath` discovers any part of the path that |
|
shared or undefined. |
|
|
|
@method metaPath |
|
@for Ember |
|
@param {Object} obj The object whose meta we are examining |
|
@param {Array} path An array of keys to walk down |
|
@param {Boolean} writable whether or not to create a new meta |
|
(or meta property) if one does not already exist or if it's |
|
shared with its constructor |
|
*/ |
|
Ember.metaPath = function metaPath(obj, path, writable) { |
|
Ember.deprecate("Ember.metaPath is deprecated and will be removed from future releases."); |
|
var meta = Ember.meta(obj, writable), keyName, value; |
|
|
|
for (var i=0, l=path.length; i<l; i++) { |
|
keyName = path[i]; |
|
value = meta[keyName]; |
|
|
|
if (!value) { |
|
if (!writable) { return undefined; } |
|
value = meta[keyName] = { __ember_source__: obj }; |
|
} else if (value.__ember_source__ !== obj) { |
|
if (!writable) { return undefined; } |
|
value = meta[keyName] = o_create(value); |
|
value.__ember_source__ = obj; |
|
} |
|
|
|
meta = value; |
|
} |
|
|
|
return value; |
|
}; |
|
|
|
/** |
|
Wraps the passed function so that `this._super` will point to the superFunc |
|
when the function is invoked. This is the primitive we use to implement |
|
calls to super. |
|
|
|
@private |
|
@method wrap |
|
@for Ember |
|
@param {Function} func The function to call |
|
@param {Function} superFunc The super function. |
|
@return {Function} wrapped function. |
|
*/ |
|
Ember.wrap = function(func, superFunc) { |
|
function superWrapper() { |
|
var ret, sup = this.__nextSuper; |
|
this.__nextSuper = superFunc; |
|
ret = func.apply(this, arguments); |
|
this.__nextSuper = sup; |
|
return ret; |
|
} |
|
|
|
superWrapper.wrappedFunction = func; |
|
superWrapper.__ember_observes__ = func.__ember_observes__; |
|
superWrapper.__ember_observesBefore__ = func.__ember_observesBefore__; |
|
superWrapper.__ember_listens__ = func.__ember_listens__; |
|
|
|
return superWrapper; |
|
}; |
|
|
|
/** |
|
Returns true if the passed object is an array or Array-like. |
|
|
|
Ember Array Protocol: |
|
|
|
- the object has an objectAt property |
|
- the object is a native Array |
|
- the object is an Object, and has a length property |
|
|
|
Unlike `Ember.typeOf` this method returns true even if the passed object is |
|
not formally array but appears to be array-like (i.e. implements `Ember.Array`) |
|
|
|
```javascript |
|
Ember.isArray(); // false |
|
Ember.isArray([]); // true |
|
Ember.isArray( Ember.ArrayProxy.create({ content: [] }) ); // true |
|
``` |
|
|
|
@method isArray |
|
@for Ember |
|
@param {Object} obj The object to test |
|
@return {Boolean} true if the passed object is an array or Array-like |
|
*/ |
|
Ember.isArray = function(obj) { |
|
if (!obj || obj.setInterval) { return false; } |
|
if (Array.isArray && Array.isArray(obj)) { return true; } |
|
if (Ember.Array && Ember.Array.detect(obj)) { return true; } |
|
if ((obj.length !== undefined) && 'object'===typeof obj) { return true; } |
|
return false; |
|
}; |
|
|
|
/** |
|
Forces the passed object to be part of an array. If the object is already |
|
an array or array-like, returns the object. Otherwise adds the object to |
|
an array. If obj is `null` or `undefined`, returns an empty array. |
|
|
|
```javascript |
|
Ember.makeArray(); // [] |
|
Ember.makeArray(null); // [] |
|
Ember.makeArray(undefined); // [] |
|
Ember.makeArray('lindsay'); // ['lindsay'] |
|
Ember.makeArray([1,2,42]); // [1,2,42] |
|
|
|
var controller = Ember.ArrayProxy.create({ content: [] }); |
|
Ember.makeArray(controller) === controller; // true |
|
``` |
|
|
|
@method makeArray |
|
@for Ember |
|
@param {Object} obj the object |
|
@return {Array} |
|
*/ |
|
Ember.makeArray = function(obj) { |
|
if (obj === null || obj === undefined) { return []; } |
|
return Ember.isArray(obj) ? obj : [obj]; |
|
}; |
|
|
|
function canInvoke(obj, methodName) { |
|
return !!(obj && typeof obj[methodName] === 'function'); |
|
} |
|
|
|
/** |
|
Checks to see if the `methodName` exists on the `obj`. |
|
|
|
```javascript |
|
var foo = {bar: Ember.K, baz: null}; |
|
Ember.canInvoke(foo, 'bar'); // true |
|
Ember.canInvoke(foo, 'baz'); // false |
|
Ember.canInvoke(foo, 'bat'); // false |
|
``` |
|
|
|
@method canInvoke |
|
@for Ember |
|
@param {Object} obj The object to check for the method |
|
@param {String} methodName The method name to check for |
|
@return {Boolean} |
|
*/ |
|
Ember.canInvoke = canInvoke; |
|
|
|
/** |
|
Checks to see if the `methodName` exists on the `obj`, |
|
and if it does, invokes it with the arguments passed. |
|
|
|
```javascript |
|
var d = new Date('03/15/2013'); |
|
Ember.tryInvoke(d, 'getTime'); // 1363320000000 |
|
Ember.tryInvoke(d, 'setFullYear', [2014]); // 1394856000000 |
|
Ember.tryInvoke(d, 'noSuchMethod', [2014]); // undefined |
|
``` |
|
|
|
@method tryInvoke |
|
@for Ember |
|
@param {Object} obj The object to check for the method |
|
@param {String} methodName The method name to check for |
|
@param {Array} [args] The arguments to pass to the method |
|
@return {*} the return value of the invoked method or undefined if it cannot be invoked |
|
*/ |
|
Ember.tryInvoke = function(obj, methodName, args) { |
|
if (canInvoke(obj, methodName)) { |
|
return obj[methodName].apply(obj, args || []); |
|
} |
|
}; |
|
|
|
// https://github.com/emberjs/ember.js/pull/1617 |
|
var needsFinallyFix = (function() { |
|
var count = 0; |
|
try{ |
|
try { } |
|
finally { |
|
count++; |
|
throw new Error('needsFinallyFixTest'); |
|
} |
|
} catch (e) {} |
|
|
|
return count !== 1; |
|
})(); |
|
|
|
/** |
|
Provides try { } finally { } functionality, while working |
|
around Safari's double finally bug. |
|
|
|
```javascript |
|
var tryable = function() { |
|
someResource.lock(); |
|
runCallback(); // May throw error. |
|
}; |
|
var finalizer = function() { |
|
someResource.unlock(); |
|
}; |
|
Ember.tryFinally(tryable, finalizer); |
|
``` |
|
|
|
@method tryFinally |
|
@for Ember |
|
@param {Function} tryable The function to run the try callback |
|
@param {Function} finalizer The function to run the finally callback |
|
@param {Object} [binding] The optional calling object. Defaults to 'this' |
|
@return {*} The return value is the that of the finalizer, |
|
unless that value is undefined, in which case it is the return value |
|
of the tryable |
|
*/ |
|
|
|
if (needsFinallyFix) { |
|
Ember.tryFinally = function(tryable, finalizer, binding) { |
|
var result, finalResult, finalError; |
|
|
|
binding = binding || this; |
|
|
|
try { |
|
result = tryable.call(binding); |
|
} finally { |
|
try { |
|
finalResult = finalizer.call(binding); |
|
} catch (e) { |
|
finalError = e; |
|
} |
|
} |
|
|
|
if (finalError) { throw finalError; } |
|
|
|
return (finalResult === undefined) ? result : finalResult; |
|
}; |
|
} else { |
|
Ember.tryFinally = function(tryable, finalizer, binding) { |
|
var result, finalResult; |
|
|
|
binding = binding || this; |
|
|
|
try { |
|
result = tryable.call(binding); |
|
} finally { |
|
finalResult = finalizer.call(binding); |
|
} |
|
|
|
return (finalResult === undefined) ? result : finalResult; |
|
}; |
|
} |
|
|
|
/** |
|
Provides try { } catch finally { } functionality, while working |
|
around Safari's double finally bug. |
|
|
|
```javascript |
|
var tryable = function() { |
|
for (i=0, l=listeners.length; i<l; i++) { |
|
listener = listeners[i]; |
|
beforeValues[i] = listener.before(name, time(), payload); |
|
} |
|
|
|
return callback.call(binding); |
|
}; |
|
|
|
var catchable = function(e) { |
|
payload = payload || {}; |
|
payload.exception = e; |
|
}; |
|
|
|
var finalizer = function() { |
|
for (i=0, l=listeners.length; i<l; i++) { |
|
listener = listeners[i]; |
|
listener.after(name, time(), payload, beforeValues[i]); |
|
} |
|
}; |
|
Ember.tryCatchFinally(tryable, catchable, finalizer); |
|
``` |
|
|
|
@method tryCatchFinally |
|
@for Ember |
|
@param {Function} tryable The function to run the try callback |
|
@param {Function} catchable The function to run the catchable callback |
|
@param {Function} finalizer The function to run the finally callback |
|
@param {Object} [binding] The optional calling object. Defaults to 'this' |
|
@return {*} The return value is the that of the finalizer, |
|
unless that value is undefined, in which case it is the return value |
|
of the tryable. |
|
*/ |
|
if (needsFinallyFix) { |
|
Ember.tryCatchFinally = function(tryable, catchable, finalizer, binding) { |
|
var result, finalResult, finalError; |
|
|
|
binding = binding || this; |
|
|
|
try { |
|
result = tryable.call(binding); |
|
} catch(error) { |
|
result = catchable.call(binding, error); |
|
} finally { |
|
try { |
|
finalResult = finalizer.call(binding); |
|
} catch (e) { |
|
finalError = e; |
|
} |
|
} |
|
|
|
if (finalError) { throw finalError; } |
|
|
|
return (finalResult === undefined) ? result : finalResult; |
|
}; |
|
} else { |
|
Ember.tryCatchFinally = function(tryable, catchable, finalizer, binding) { |
|
var result, finalResult; |
|
|
|
binding = binding || this; |
|
|
|
try { |
|
result = tryable.call(binding); |
|
} catch(error) { |
|
result = catchable.call(binding, error); |
|
} finally { |
|
finalResult = finalizer.call(binding); |
|
} |
|
|
|
return (finalResult === undefined) ? result : finalResult; |
|
}; |
|
} |
|
|
|
// ........................................ |
|
// TYPING & ARRAY MESSAGING |
|
// |
|
|
|
var TYPE_MAP = {}; |
|
var t = "Boolean Number String Function Array Date RegExp Object".split(" "); |
|
Ember.ArrayPolyfills.forEach.call(t, function(name) { |
|
TYPE_MAP[ "[object " + name + "]" ] = name.toLowerCase(); |
|
}); |
|
|
|
var toString = Object.prototype.toString; |
|
|
|
/** |
|
Returns a consistent type for the passed item. |
|
|
|
Use this instead of the built-in `typeof` to get the type of an item. |
|
It will return the same result across all browsers and includes a bit |
|
more detail. Here is what will be returned: |
|
|
|
| Return Value | Meaning | |
|
|---------------|------------------------------------------------------| |
|
| 'string' | String primitive or String object. | |
|
| 'number' | Number primitive or Number object. | |
|
| 'boolean' | Boolean primitive or Boolean object. | |
|
| 'null' | Null value | |
|
| 'undefined' | Undefined value | |
|
| 'function' | A function | |
|
| 'array' | An instance of Array | |
|
| 'regexp' | An instance of RegExp | |
|
| 'date' | An instance of Date | |
|
| 'class' | An Ember class (created using Ember.Object.extend()) | |
|
| 'instance' | An Ember object instance | |
|
| 'error' | An instance of the Error object | |
|
| 'object' | A JavaScript object not inheriting from Ember.Object | |
|
|
|
Examples: |
|
|
|
```javascript |
|
Ember.typeOf(); // 'undefined' |
|
Ember.typeOf(null); // 'null' |
|
Ember.typeOf(undefined); // 'undefined' |
|
Ember.typeOf('michael'); // 'string' |
|
Ember.typeOf(new String('michael')); // 'string' |
|
Ember.typeOf(101); // 'number' |
|
Ember.typeOf(new Number(101)); // 'number' |
|
Ember.typeOf(true); // 'boolean' |
|
Ember.typeOf(new Boolean(true)); // 'boolean' |
|
Ember.typeOf(Ember.makeArray); // 'function' |
|
Ember.typeOf([1,2,90]); // 'array' |
|
Ember.typeOf(/abc/); // 'regexp' |
|
Ember.typeOf(new Date()); // 'date' |
|
Ember.typeOf(Ember.Object.extend()); // 'class' |
|
Ember.typeOf(Ember.Object.create()); // 'instance' |
|
Ember.typeOf(new Error('teamocil')); // 'error' |
|
|
|
// "normal" JavaScript object |
|
Ember.typeOf({a: 'b'}); // 'object' |
|
``` |
|
|
|
@method typeOf |
|
@for Ember |
|
@param {Object} item the item to check |
|
@return {String} the type |
|
*/ |
|
Ember.typeOf = function(item) { |
|
var ret; |
|
|
|
ret = (item === null || item === undefined) ? String(item) : TYPE_MAP[toString.call(item)] || 'object'; |
|
|
|
if (ret === 'function') { |
|
if (Ember.Object && Ember.Object.detect(item)) ret = 'class'; |
|
} else if (ret === 'object') { |
|
if (item instanceof Error) ret = 'error'; |
|
else if (Ember.Object && item instanceof Ember.Object) ret = 'instance'; |
|
else if (item instanceof Date) ret = 'date'; |
|
} |
|
|
|
return ret; |
|
}; |
|
|
|
/** |
|
Convenience method to inspect an object. This method will attempt to |
|
convert the object into a useful string description. |
|
|
|
It is a pretty simple implementation. If you want something more robust, |
|
use something like JSDump: https://github.com/NV/jsDump |
|
|
|
@method inspect |
|
@for Ember |
|
@param {Object} obj The object you want to inspect. |
|
@return {String} A description of the object |
|
*/ |
|
Ember.inspect = function(obj) { |
|
var type = Ember.typeOf(obj); |
|
if (type === 'array') { |
|
return '[' + obj + ']'; |
|
} |
|
if (type !== 'object') { |
|
return obj + ''; |
|
} |
|
|
|
var v, ret = []; |
|
for(var key in obj) { |
|
if (obj.hasOwnProperty(key)) { |
|
v = obj[key]; |
|
if (v === 'toString') { continue; } // ignore useless items |
|
if (Ember.typeOf(v) === 'function') { v = "function() { ... }"; } |
|
ret.push(key + ": " + v); |
|
} |
|
} |
|
return "{" + ret.join(", ") + "}"; |
|
}; |
|
|
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
// Ember.tryCatchFinally |
|
|
|
/** |
|
The purpose of the Ember Instrumentation module is |
|
to provide efficient, general-purpose instrumentation |
|
for Ember. |
|
|
|
Subscribe to a listener by using `Ember.subscribe`: |
|
|
|
```javascript |
|
Ember.subscribe("render", { |
|
before: function(name, timestamp, payload) { |
|
|
|
}, |
|
|
|
after: function(name, timestamp, payload) { |
|
|
|
} |
|
}); |
|
``` |
|
|
|
If you return a value from the `before` callback, that same |
|
value will be passed as a fourth parameter to the `after` |
|
callback. |
|
|
|
Instrument a block of code by using `Ember.instrument`: |
|
|
|
```javascript |
|
Ember.instrument("render.handlebars", payload, function() { |
|
// rendering logic |
|
}, binding); |
|
``` |
|
|
|
Event names passed to `Ember.instrument` are namespaced |
|
by periods, from more general to more specific. Subscribers |
|
can listen for events by whatever level of granularity they |
|
are interested in. |
|
|
|
In the above example, the event is `render.handlebars`, |
|
and the subscriber listened for all events beginning with |
|
`render`. It would receive callbacks for events named |
|
`render`, `render.handlebars`, `render.container`, or |
|
even `render.handlebars.layout`. |
|
|
|
@class Instrumentation |
|
@namespace Ember |
|
@static |
|
*/ |
|
Ember.Instrumentation = {}; |
|
|
|
var subscribers = [], cache = {}; |
|
|
|
var populateListeners = function(name) { |
|
var listeners = [], subscriber; |
|
|
|
for (var i=0, l=subscribers.length; i<l; i++) { |
|
subscriber = subscribers[i]; |
|
if (subscriber.regex.test(name)) { |
|
listeners.push(subscriber.object); |
|
} |
|
} |
|
|
|
cache[name] = listeners; |
|
return listeners; |
|
}; |
|
|
|
var time = (function() { |
|
var perf = 'undefined' !== typeof window ? window.performance || {} : {}; |
|
var fn = perf.now || perf.mozNow || perf.webkitNow || perf.msNow || perf.oNow; |
|
// fn.bind will be available in all the browsers that support the advanced window.performance... ;-) |
|
return fn ? fn.bind(perf) : function() { return +new Date(); }; |
|
})(); |
|
|
|
/** |
|
Notifies event's subscribers, calls `before` and `after` hooks. |
|
|
|
@method instrument |
|
@namespace Ember.Instrumentation |
|
|
|
@param {String} [name] Namespaced event name. |
|
@param {Object} payload |
|
@param {Function} callback Function that you're instrumenting. |
|
@param {Object} binding Context that instrument function is called with. |
|
*/ |
|
Ember.Instrumentation.instrument = function(name, payload, callback, binding) { |
|
var listeners = cache[name], timeName, ret; |
|
|
|
if (Ember.STRUCTURED_PROFILE) { |
|
timeName = name + ": " + payload.object; |
|
console.time(timeName); |
|
} |
|
|
|
if (!listeners) { |
|
listeners = populateListeners(name); |
|
} |
|
|
|
if (listeners.length === 0) { |
|
ret = callback.call(binding); |
|
if (Ember.STRUCTURED_PROFILE) { console.timeEnd(timeName); } |
|
return ret; |
|
} |
|
|
|
var beforeValues = [], listener, i, l; |
|
|
|
function tryable() { |
|
for (i=0, l=listeners.length; i<l; i++) { |
|
listener = listeners[i]; |
|
beforeValues[i] = listener.before(name, time(), payload); |
|
} |
|
|
|
return callback.call(binding); |
|
} |
|
|
|
function catchable(e) { |
|
payload = payload || {}; |
|
payload.exception = e; |
|
} |
|
|
|
function finalizer() { |
|
for (i=0, l=listeners.length; i<l; i++) { |
|
listener = listeners[i]; |
|
listener.after(name, time(), payload, beforeValues[i]); |
|
} |
|
|
|
if (Ember.STRUCTURED_PROFILE) { |
|
console.timeEnd(timeName); |
|
} |
|
} |
|
|
|
return Ember.tryCatchFinally(tryable, catchable, finalizer); |
|
}; |
|
|
|
/** |
|
Subscribes to a particular event or instrumented block of code. |
|
|
|
@method subscribe |
|
@namespace Ember.Instrumentation |
|
|
|
@param {String} [pattern] Namespaced event name. |
|
@param {Object} [object] Before and After hooks. |
|
|
|
@return {Subscriber} |
|
*/ |
|
Ember.Instrumentation.subscribe = function(pattern, object) { |
|
var paths = pattern.split("."), path, regex = []; |
|
|
|
for (var i=0, l=paths.length; i<l; i++) { |
|
path = paths[i]; |
|
if (path === "*") { |
|
regex.push("[^\\.]*"); |
|
} else { |
|
regex.push(path); |
|
} |
|
} |
|
|
|
regex = regex.join("\\."); |
|
regex = regex + "(\\..*)?"; |
|
|
|
var subscriber = { |
|
pattern: pattern, |
|
regex: new RegExp("^" + regex + "$"), |
|
object: object |
|
}; |
|
|
|
subscribers.push(subscriber); |
|
cache = {}; |
|
|
|
return subscriber; |
|
}; |
|
|
|
/** |
|
Unsubscribes from a particular event or instrumented block of code. |
|
|
|
@method unsubscribe |
|
@namespace Ember.Instrumentation |
|
|
|
@param {Object} [subscriber] |
|
*/ |
|
Ember.Instrumentation.unsubscribe = function(subscriber) { |
|
var index; |
|
|
|
for (var i=0, l=subscribers.length; i<l; i++) { |
|
if (subscribers[i] === subscriber) { |
|
index = i; |
|
} |
|
} |
|
|
|
subscribers.splice(index, 1); |
|
cache = {}; |
|
}; |
|
|
|
/** |
|
Resets `Ember.Instrumentation` by flushing list of subscribers. |
|
|
|
@method reset |
|
@namespace Ember.Instrumentation |
|
*/ |
|
Ember.Instrumentation.reset = function() { |
|
subscribers = []; |
|
cache = {}; |
|
}; |
|
|
|
Ember.instrument = Ember.Instrumentation.instrument; |
|
Ember.subscribe = Ember.Instrumentation.subscribe; |
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var map, forEach, indexOf, splice, filter; |
|
map = Array.prototype.map || Ember.ArrayPolyfills.map; |
|
forEach = Array.prototype.forEach || Ember.ArrayPolyfills.forEach; |
|
indexOf = Array.prototype.indexOf || Ember.ArrayPolyfills.indexOf; |
|
filter = Array.prototype.filter || Ember.ArrayPolyfills.filter; |
|
splice = Array.prototype.splice; |
|
|
|
/** |
|
* Defines some convenience methods for working with Enumerables. |
|
* `Ember.EnumerableUtils` uses `Ember.ArrayPolyfills` when necessary. |
|
* |
|
* @class EnumerableUtils |
|
* @namespace Ember |
|
* @static |
|
* */ |
|
var utils = Ember.EnumerableUtils = { |
|
/** |
|
* Calls the map function on the passed object with a specified callback. This |
|
* uses `Ember.ArrayPolyfill`'s-map method when necessary. |
|
* |
|
* @method map |
|
* @param {Object} obj The object that should be mapped |
|
* @param {Function} callback The callback to execute |
|
* @param {Object} thisArg Value to use as this when executing *callback* |
|
* |
|
* @return {Array} An array of mapped values. |
|
*/ |
|
map: function(obj, callback, thisArg) { |
|
return obj.map ? obj.map.call(obj, callback, thisArg) : map.call(obj, callback, thisArg); |
|
}, |
|
|
|
/** |
|
* Calls the forEach function on the passed object with a specified callback. This |
|
* uses `Ember.ArrayPolyfill`'s-forEach method when necessary. |
|
* |
|
* @method forEach |
|
* @param {Object} obj The object to call forEach on |
|
* @param {Function} callback The callback to execute |
|
* @param {Object} thisArg Value to use as this when executing *callback* |
|
* |
|
*/ |
|
forEach: function(obj, callback, thisArg) { |
|
return obj.forEach ? obj.forEach.call(obj, callback, thisArg) : forEach.call(obj, callback, thisArg); |
|
}, |
|
|
|
/** |
|
* Calls the filter function on the passed object with a specified callback. This |
|
* uses `Ember.ArrayPolyfill`'s-filter method when necessary. |
|
* |
|
* @method filter |
|
* @param {Object} obj The object to call filter on |
|
* @param {Function} callback The callback to execute |
|
* @param {Object} thisArg Value to use as this when executing *callback* |
|
* |
|
* @return {Array} An array containing the filtered values |
|
*/ |
|
filter: function(obj, callback, thisArg) { |
|
return obj.filter ? obj.filter.call(obj, callback, thisArg) : filter.call(obj, callback, thisArg); |
|
}, |
|
|
|
/** |
|
* Calls the indexOf function on the passed object with a specified callback. This |
|
* uses `Ember.ArrayPolyfill`'s-indexOf method when necessary. |
|
* |
|
* @method indexOf |
|
* @param {Object} obj The object to call indexOn on |
|
* @param {Function} callback The callback to execute |
|
* @param {Object} index The index to start searching from |
|
* |
|
*/ |
|
indexOf: function(obj, element, index) { |
|
return obj.indexOf ? obj.indexOf.call(obj, element, index) : indexOf.call(obj, element, index); |
|
}, |
|
|
|
/** |
|
* Returns an array of indexes of the first occurrences of the passed elements |
|
* on the passed object. |
|
* |
|
* ```javascript |
|
* var array = [1, 2, 3, 4, 5]; |
|
* Ember.EnumerableUtils.indexesOf(array, [2, 5]); // [1, 4] |
|
* |
|
* var fubar = "Fubarr"; |
|
* Ember.EnumerableUtils.indexesOf(fubar, ['b', 'r']); // [2, 4] |
|
* ``` |
|
* |
|
* @method indexesOf |
|
* @param {Object} obj The object to check for element indexes |
|
* @param {Array} elements The elements to search for on *obj* |
|
* |
|
* @return {Array} An array of indexes. |
|
* |
|
*/ |
|
indexesOf: function(obj, elements) { |
|
return elements === undefined ? [] : utils.map(elements, function(item) { |
|
return utils.indexOf(obj, item); |
|
}); |
|
}, |
|
|
|
/** |
|
* Adds an object to an array. If the array already includes the object this |
|
* method has no effect. |
|
* |
|
* @method addObject |
|
* @param {Array} array The array the passed item should be added to |
|
* @param {Object} item The item to add to the passed array |
|
* |
|
* @return 'undefined' |
|
*/ |
|
addObject: function(array, item) { |
|
var index = utils.indexOf(array, item); |
|
if (index === -1) { array.push(item); } |
|
}, |
|
|
|
/** |
|
* Removes an object from an array. If the array does not contain the passed |
|
* object this method has no effect. |
|
* |
|
* @method removeObject |
|
* @param {Array} array The array to remove the item from. |
|
* @param {Object} item The item to remove from the passed array. |
|
* |
|
* @return 'undefined' |
|
*/ |
|
removeObject: function(array, item) { |
|
var index = utils.indexOf(array, item); |
|
if (index !== -1) { array.splice(index, 1); } |
|
}, |
|
|
|
_replace: function(array, idx, amt, objects) { |
|
var args = [].concat(objects), chunk, ret = [], |
|
// https://code.google.com/p/chromium/issues/detail?id=56588 |
|
size = 60000, start = idx, ends = amt, count; |
|
|
|
while (args.length) { |
|
count = ends > size ? size : ends; |
|
if (count <= 0) { count = 0; } |
|
|
|
chunk = args.splice(0, size); |
|
chunk = [start, count].concat(chunk); |
|
|
|
start += size; |
|
ends -= count; |
|
|
|
ret = ret.concat(splice.apply(array, chunk)); |
|
} |
|
return ret; |
|
}, |
|
|
|
/** |
|
* Replaces objects in an array with the passed objects. |
|
* |
|
* ```javascript |
|
* var array = [1,2,3]; |
|
* Ember.EnumerableUtils.replace(array, 1, 2, [4, 5]); // [1, 4, 5] |
|
* |
|
* var array = [1,2,3]; |
|
* Ember.EnumerableUtils.replace(array, 1, 1, [4, 5]); // [1, 4, 5, 3] |
|
* |
|
* var array = [1,2,3]; |
|
* Ember.EnumerableUtils.replace(array, 10, 1, [4, 5]); // [1, 2, 3, 4, 5] |
|
* ``` |
|
* |
|
* @method replace |
|
* @param {Array} array The array the objects should be inserted into. |
|
* @param {Number} idx Starting index in the array to replace. If *idx* >= |
|
* length, then append to the end of the array. |
|
* @param {Number} amt Number of elements that should be removed from the array, |
|
* starting at *idx* |
|
* @param {Array} objects An array of zero or more objects that should be |
|
* inserted into the array at *idx* |
|
* |
|
* @return {Array} The modified array. |
|
*/ |
|
replace: function(array, idx, amt, objects) { |
|
if (array.replace) { |
|
return array.replace(idx, amt, objects); |
|
} else { |
|
return utils._replace(array, idx, amt, objects); |
|
} |
|
}, |
|
|
|
/** |
|
* Calculates the intersection of two arrays. This method returns a new array |
|
* filled with the records that the two passed arrays share with each other. |
|
* If there is no intersection, an empty array will be returned. |
|
* |
|
* ```javascript |
|
* var array1 = [1, 2, 3, 4, 5]; |
|
* var array2 = [1, 3, 5, 6, 7]; |
|
* |
|
* Ember.EnumerableUtils.intersection(array1, array2); // [1, 3, 5] |
|
* |
|
* var array1 = [1, 2, 3]; |
|
* var array2 = [4, 5, 6]; |
|
* |
|
* Ember.EnumerableUtils.intersection(array1, array2); // [] |
|
* ``` |
|
* |
|
* @method intersection |
|
* @param {Array} array1 The first array |
|
* @param {Array} array2 The second array |
|
* |
|
* @return {Array} The intersection of the two passed arrays. |
|
*/ |
|
intersection: function(array1, array2) { |
|
var intersection = []; |
|
|
|
utils.forEach(array1, function(element) { |
|
if (utils.indexOf(array2, element) >= 0) { |
|
intersection.push(element); |
|
} |
|
}); |
|
|
|
return intersection; |
|
} |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
var META_KEY = Ember.META_KEY, get; |
|
|
|
var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; |
|
|
|
var IS_GLOBAL_PATH = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]/; |
|
var HAS_THIS = /^this[\.\*]/; |
|
var FIRST_KEY = /^([^\.\*]+)/; |
|
|
|
// .......................................................... |
|
// GET AND SET |
|
// |
|
// If we are on a platform that supports accessors we can use those. |
|
// Otherwise simulate accessors by looking up the property directly on the |
|
// object. |
|
|
|
/** |
|
Gets the value of a property on an object. If the property is computed, |
|
the function will be invoked. If the property is not defined but the |
|
object implements the `unknownProperty` method then that will be invoked. |
|
|
|
If you plan to run on IE8 and older browsers then you should use this |
|
method anytime you want to retrieve a property on an object that you don't |
|
know for sure is private. (Properties beginning with an underscore '_' |
|
are considered private.) |
|
|
|
On all newer browsers, you only need to use this method to retrieve |
|
properties if the property might not be defined on the object and you want |
|
to respect the `unknownProperty` handler. Otherwise you can ignore this |
|
method. |
|
|
|
Note that if the object itself is `undefined`, this method will throw |
|
an error. |
|
|
|
@method get |
|
@for Ember |
|
@param {Object} obj The object to retrieve from. |
|
@param {String} keyName The property key to retrieve |
|
@return {Object} the property value or `null`. |
|
*/ |
|
get = function get(obj, keyName) { |
|
// Helpers that operate with 'this' within an #each |
|
if (keyName === '') { |
|
return obj; |
|
} |
|
|
|
if (!keyName && 'string'===typeof obj) { |
|
keyName = obj; |
|
obj = null; |
|
} |
|
|
|
Ember.assert("Cannot call get with "+ keyName +" key.", !!keyName); |
|
Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); |
|
|
|
if (obj === null || keyName.indexOf('.') !== -1) { |
|
return getPath(obj, keyName); |
|
} |
|
|
|
var meta = obj[META_KEY], desc = meta && meta.descs[keyName], ret; |
|
if (desc) { |
|
return desc.get(obj, keyName); |
|
} else { |
|
if (MANDATORY_SETTER && meta && meta.watching[keyName] > 0) { |
|
ret = meta.values[keyName]; |
|
} else { |
|
ret = obj[keyName]; |
|
} |
|
|
|
if (ret === undefined && |
|
'object' === typeof obj && !(keyName in obj) && 'function' === typeof obj.unknownProperty) { |
|
return obj.unknownProperty(keyName); |
|
} |
|
|
|
return ret; |
|
} |
|
}; |
|
|
|
// Currently used only by Ember Data tests |
|
if (Ember.config.overrideAccessors) { |
|
Ember.get = get; |
|
Ember.config.overrideAccessors(); |
|
get = Ember.get; |
|
} |
|
|
|
/** |
|
Normalizes a target/path pair to reflect that actual target/path that should |
|
be observed, etc. This takes into account passing in global property |
|
paths (i.e. a path beginning with a captial letter not defined on the |
|
target) and * separators. |
|
|
|
@private |
|
@method normalizeTuple |
|
@for Ember |
|
@param {Object} target The current target. May be `null`. |
|
@param {String} path A path on the target or a global property path. |
|
@return {Array} a temporary array with the normalized target/path pair. |
|
*/ |
|
var normalizeTuple = Ember.normalizeTuple = function(target, path) { |
|
var hasThis = HAS_THIS.test(path), |
|
isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), |
|
key; |
|
|
|
if (!target || isGlobal) target = Ember.lookup; |
|
if (hasThis) path = path.slice(5); |
|
|
|
if (target === Ember.lookup) { |
|
key = path.match(FIRST_KEY)[0]; |
|
target = get(target, key); |
|
path = path.slice(key.length+1); |
|
} |
|
|
|
// must return some kind of path to be valid else other things will break. |
|
if (!path || path.length===0) throw new Ember.Error('Path cannot be empty'); |
|
|
|
return [ target, path ]; |
|
}; |
|
|
|
var getPath = Ember._getPath = function(root, path) { |
|
var hasThis, parts, tuple, idx, len; |
|
|
|
// If there is no root and path is a key name, return that |
|
// property from the global object. |
|
// E.g. get('Ember') -> Ember |
|
if (root === null && path.indexOf('.') === -1) { return get(Ember.lookup, path); } |
|
|
|
// detect complicated paths and normalize them |
|
hasThis = HAS_THIS.test(path); |
|
|
|
if (!root || hasThis) { |
|
tuple = normalizeTuple(root, path); |
|
root = tuple[0]; |
|
path = tuple[1]; |
|
tuple.length = 0; |
|
} |
|
|
|
parts = path.split("."); |
|
len = parts.length; |
|
for (idx = 0; root != null && idx < len; idx++) { |
|
root = get(root, parts[idx], true); |
|
if (root && root.isDestroyed) { return undefined; } |
|
} |
|
return root; |
|
}; |
|
|
|
Ember.getWithDefault = function(root, key, defaultValue) { |
|
var value = get(root, key); |
|
|
|
if (value === undefined) { return defaultValue; } |
|
return value; |
|
}; |
|
|
|
|
|
Ember.get = get; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
var o_create = Ember.create, |
|
metaFor = Ember.meta, |
|
META_KEY = Ember.META_KEY, |
|
a_slice = [].slice, |
|
/* listener flags */ |
|
ONCE = 1, SUSPENDED = 2; |
|
|
|
/* |
|
The event system uses a series of nested hashes to store listeners on an |
|
object. When a listener is registered, or when an event arrives, these |
|
hashes are consulted to determine which target and action pair to invoke. |
|
|
|
The hashes are stored in the object's meta hash, and look like this: |
|
|
|
// Object's meta hash |
|
{ |
|
listeners: { // variable name: `listenerSet` |
|
"foo:changed": [ // variable name: `actions` |
|
target, method, flags |
|
] |
|
} |
|
} |
|
|
|
*/ |
|
|
|
function indexOf(array, target, method) { |
|
var index = -1; |
|
// hashes are added to the end of the event array |
|
// so it makes sense to start searching at the end |
|
// of the array and search in reverse |
|
for (var i = array.length - 3 ; i >=0; i -= 3) { |
|
if (target === array[i] && method === array[i + 1]) { |
|
index = i; break; |
|
} |
|
} |
|
return index; |
|
} |
|
|
|
function actionsFor(obj, eventName) { |
|
var meta = metaFor(obj, true), |
|
actions; |
|
|
|
if (!meta.listeners) { meta.listeners = {}; } |
|
|
|
if (!meta.hasOwnProperty('listeners')) { |
|
// setup inherited copy of the listeners object |
|
meta.listeners = o_create(meta.listeners); |
|
} |
|
|
|
actions = meta.listeners[eventName]; |
|
|
|
// if there are actions, but the eventName doesn't exist in our listeners, then copy them from the prototype |
|
if (actions && !meta.listeners.hasOwnProperty(eventName)) { |
|
actions = meta.listeners[eventName] = meta.listeners[eventName].slice(); |
|
} else if (!actions) { |
|
actions = meta.listeners[eventName] = []; |
|
} |
|
|
|
return actions; |
|
} |
|
|
|
function actionsUnion(obj, eventName, otherActions) { |
|
var meta = obj[META_KEY], |
|
actions = meta && meta.listeners && meta.listeners[eventName]; |
|
|
|
if (!actions) { return; } |
|
for (var i = actions.length - 3; i >= 0; i -= 3) { |
|
var target = actions[i], |
|
method = actions[i+1], |
|
flags = actions[i+2], |
|
actionIndex = indexOf(otherActions, target, method); |
|
|
|
if (actionIndex === -1) { |
|
otherActions.push(target, method, flags); |
|
} |
|
} |
|
} |
|
|
|
function actionsDiff(obj, eventName, otherActions) { |
|
var meta = obj[META_KEY], |
|
actions = meta && meta.listeners && meta.listeners[eventName], |
|
diffActions = []; |
|
|
|
if (!actions) { return; } |
|
for (var i = actions.length - 3; i >= 0; i -= 3) { |
|
var target = actions[i], |
|
method = actions[i+1], |
|
flags = actions[i+2], |
|
actionIndex = indexOf(otherActions, target, method); |
|
|
|
if (actionIndex !== -1) { continue; } |
|
|
|
otherActions.push(target, method, flags); |
|
diffActions.push(target, method, flags); |
|
} |
|
|
|
return diffActions; |
|
} |
|
|
|
/** |
|
Add an event listener |
|
|
|
@method addListener |
|
@for Ember |
|
@param obj |
|
@param {String} eventName |
|
@param {Object|Function} targetOrMethod A target object or a function |
|
@param {Function|String} method A function or the name of a function to be called on `target` |
|
@param {Boolean} once A flag whether a function should only be called once |
|
*/ |
|
function addListener(obj, eventName, target, method, once) { |
|
Ember.assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); |
|
|
|
if (!method && 'function' === typeof target) { |
|
method = target; |
|
target = null; |
|
} |
|
|
|
var actions = actionsFor(obj, eventName), |
|
actionIndex = indexOf(actions, target, method), |
|
flags = 0; |
|
|
|
if (once) flags |= ONCE; |
|
|
|
if (actionIndex !== -1) { return; } |
|
|
|
actions.push(target, method, flags); |
|
|
|
if ('function' === typeof obj.didAddListener) { |
|
obj.didAddListener(eventName, target, method); |
|
} |
|
} |
|
|
|
/** |
|
Remove an event listener |
|
|
|
Arguments should match those passed to `Ember.addListener`. |
|
|
|
@method removeListener |
|
@for Ember |
|
@param obj |
|
@param {String} eventName |
|
@param {Object|Function} targetOrMethod A target object or a function |
|
@param {Function|String} method A function or the name of a function to be called on `target` |
|
*/ |
|
function removeListener(obj, eventName, target, method) { |
|
Ember.assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); |
|
|
|
if (!method && 'function' === typeof target) { |
|
method = target; |
|
target = null; |
|
} |
|
|
|
function _removeListener(target, method) { |
|
var actions = actionsFor(obj, eventName), |
|
actionIndex = indexOf(actions, target, method); |
|
|
|
// action doesn't exist, give up silently |
|
if (actionIndex === -1) { return; } |
|
|
|
actions.splice(actionIndex, 3); |
|
|
|
if ('function' === typeof obj.didRemoveListener) { |
|
obj.didRemoveListener(eventName, target, method); |
|
} |
|
} |
|
|
|
if (method) { |
|
_removeListener(target, method); |
|
} else { |
|
var meta = obj[META_KEY], |
|
actions = meta && meta.listeners && meta.listeners[eventName]; |
|
|
|
if (!actions) { return; } |
|
for (var i = actions.length - 3; i >= 0; i -= 3) { |
|
_removeListener(actions[i], actions[i+1]); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
Suspend listener during callback. |
|
|
|
This should only be used by the target of the event listener |
|
when it is taking an action that would cause the event, e.g. |
|
an object might suspend its property change listener while it is |
|
setting that property. |
|
|
|
@method suspendListener |
|
@for Ember |
|
|
|
@private |
|
@param obj |
|
@param {String} eventName |
|
@param {Object|Function} targetOrMethod A target object or a function |
|
@param {Function|String} method A function or the name of a function to be called on `target` |
|
@param {Function} callback |
|
*/ |
|
function suspendListener(obj, eventName, target, method, callback) { |
|
if (!method && 'function' === typeof target) { |
|
method = target; |
|
target = null; |
|
} |
|
|
|
var actions = actionsFor(obj, eventName), |
|
actionIndex = indexOf(actions, target, method); |
|
|
|
if (actionIndex !== -1) { |
|
actions[actionIndex+2] |= SUSPENDED; // mark the action as suspended |
|
} |
|
|
|
function tryable() { return callback.call(target); } |
|
function finalizer() { if (actionIndex !== -1) { actions[actionIndex+2] &= ~SUSPENDED; } } |
|
|
|
return Ember.tryFinally(tryable, finalizer); |
|
} |
|
|
|
/** |
|
Suspends multiple listeners during a callback. |
|
|
|
@method suspendListeners |
|
@for Ember |
|
|
|
@private |
|
@param obj |
|
@param {Array} eventName Array of event names |
|
@param {Object|Function} targetOrMethod A target object or a function |
|
@param {Function|String} method A function or the name of a function to be called on `target` |
|
@param {Function} callback |
|
*/ |
|
function suspendListeners(obj, eventNames, target, method, callback) { |
|
if (!method && 'function' === typeof target) { |
|
method = target; |
|
target = null; |
|
} |
|
|
|
var suspendedActions = [], |
|
actionsList = [], |
|
eventName, actions, i, l; |
|
|
|
for (i=0, l=eventNames.length; i<l; i++) { |
|
eventName = eventNames[i]; |
|
actions = actionsFor(obj, eventName); |
|
var actionIndex = indexOf(actions, target, method); |
|
|
|
if (actionIndex !== -1) { |
|
actions[actionIndex+2] |= SUSPENDED; |
|
suspendedActions.push(actionIndex); |
|
actionsList.push(actions); |
|
} |
|
} |
|
|
|
function tryable() { return callback.call(target); } |
|
|
|
function finalizer() { |
|
for (var i = 0, l = suspendedActions.length; i < l; i++) { |
|
var actionIndex = suspendedActions[i]; |
|
actionsList[i][actionIndex+2] &= ~SUSPENDED; |
|
} |
|
} |
|
|
|
return Ember.tryFinally(tryable, finalizer); |
|
} |
|
|
|
/** |
|
Return a list of currently watched events |
|
|
|
@private |
|
@method watchedEvents |
|
@for Ember |
|
@param obj |
|
*/ |
|
function watchedEvents(obj) { |
|
var listeners = obj[META_KEY].listeners, ret = []; |
|
|
|
if (listeners) { |
|
for(var eventName in listeners) { |
|
if (listeners[eventName]) { ret.push(eventName); } |
|
} |
|
} |
|
return ret; |
|
} |
|
|
|
/** |
|
Send an event. The execution of suspended listeners |
|
is skipped, and once listeners are removed. A listener without |
|
a target is executed on the passed object. If an array of actions |
|
is not passed, the actions stored on the passed object are invoked. |
|
|
|
@method sendEvent |
|
@for Ember |
|
@param obj |
|
@param {String} eventName |
|
@param {Array} params Optional parameters for each listener. |
|
@param {Array} actions Optional array of actions (listeners). |
|
@return true |
|
*/ |
|
function sendEvent(obj, eventName, params, actions) { |
|
// first give object a chance to handle it |
|
if (obj !== Ember && 'function' === typeof obj.sendEvent) { |
|
obj.sendEvent(eventName, params); |
|
} |
|
|
|
if (!actions) { |
|
var meta = obj[META_KEY]; |
|
actions = meta && meta.listeners && meta.listeners[eventName]; |
|
} |
|
|
|
if (!actions) { return; } |
|
|
|
for (var i = actions.length - 3; i >= 0; i -= 3) { // looping in reverse for once listeners |
|
var target = actions[i], method = actions[i+1], flags = actions[i+2]; |
|
if (!method) { continue; } |
|
if (flags & SUSPENDED) { continue; } |
|
if (flags & ONCE) { removeListener(obj, eventName, target, method); } |
|
if (!target) { target = obj; } |
|
if ('string' === typeof method) { method = target[method]; } |
|
if (params) { |
|
method.apply(target, params); |
|
} else { |
|
method.call(target); |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
/** |
|
@private |
|
@method hasListeners |
|
@for Ember |
|
@param obj |
|
@param {String} eventName |
|
*/ |
|
function hasListeners(obj, eventName) { |
|
var meta = obj[META_KEY], |
|
actions = meta && meta.listeners && meta.listeners[eventName]; |
|
|
|
return !!(actions && actions.length); |
|
} |
|
|
|
/** |
|
@private |
|
@method listenersFor |
|
@for Ember |
|
@param obj |
|
@param {String} eventName |
|
*/ |
|
function listenersFor(obj, eventName) { |
|
var ret = []; |
|
var meta = obj[META_KEY], |
|
actions = meta && meta.listeners && meta.listeners[eventName]; |
|
|
|
if (!actions) { return ret; } |
|
|
|
for (var i = 0, l = actions.length; i < l; i += 3) { |
|
var target = actions[i], |
|
method = actions[i+1]; |
|
ret.push([target, method]); |
|
} |
|
|
|
return ret; |
|
} |
|
|
|
/** |
|
Define a property as a function that should be executed when |
|
a specified event or events are triggered. |
|
|
|
|
|
``` javascript |
|
var Job = Ember.Object.extend({ |
|
logCompleted: Ember.on('completed', function(){ |
|
console.log('Job completed!'); |
|
}) |
|
}); |
|
var job = Job.create(); |
|
Ember.sendEvent(job, 'completed'); // Logs "Job completed!" |
|
``` |
|
|
|
@method on |
|
@for Ember |
|
@param {String} eventNames* |
|
@param {Function} func |
|
@return func |
|
*/ |
|
Ember.on = function(){ |
|
var func = a_slice.call(arguments, -1)[0], |
|
events = a_slice.call(arguments, 0, -1); |
|
func.__ember_listens__ = events; |
|
return func; |
|
}; |
|
|
|
Ember.addListener = addListener; |
|
Ember.removeListener = removeListener; |
|
Ember._suspendListener = suspendListener; |
|
Ember._suspendListeners = suspendListeners; |
|
Ember.sendEvent = sendEvent; |
|
Ember.hasListeners = hasListeners; |
|
Ember.watchedEvents = watchedEvents; |
|
Ember.listenersFor = listenersFor; |
|
Ember.listenersDiff = actionsDiff; |
|
Ember.listenersUnion = actionsUnion; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var guidFor = Ember.guidFor, |
|
sendEvent = Ember.sendEvent; |
|
|
|
/* |
|
this.observerSet = { |
|
[senderGuid]: { // variable name: `keySet` |
|
[keyName]: listIndex |
|
} |
|
}, |
|
this.observers = [ |
|
{ |
|
sender: obj, |
|
keyName: keyName, |
|
eventName: eventName, |
|
listeners: [ |
|
[target, method, flags] |
|
] |
|
}, |
|
... |
|
] |
|
*/ |
|
var ObserverSet = Ember._ObserverSet = function() { |
|
this.clear(); |
|
}; |
|
|
|
ObserverSet.prototype.add = function(sender, keyName, eventName) { |
|
var observerSet = this.observerSet, |
|
observers = this.observers, |
|
senderGuid = guidFor(sender), |
|
keySet = observerSet[senderGuid], |
|
index; |
|
|
|
if (!keySet) { |
|
observerSet[senderGuid] = keySet = {}; |
|
} |
|
index = keySet[keyName]; |
|
if (index === undefined) { |
|
index = observers.push({ |
|
sender: sender, |
|
keyName: keyName, |
|
eventName: eventName, |
|
listeners: [] |
|
}) - 1; |
|
keySet[keyName] = index; |
|
} |
|
return observers[index].listeners; |
|
}; |
|
|
|
ObserverSet.prototype.flush = function() { |
|
var observers = this.observers, i, len, observer, sender; |
|
this.clear(); |
|
for (i=0, len=observers.length; i < len; ++i) { |
|
observer = observers[i]; |
|
sender = observer.sender; |
|
if (sender.isDestroying || sender.isDestroyed) { continue; } |
|
sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); |
|
} |
|
}; |
|
|
|
ObserverSet.prototype.clear = function() { |
|
this.observerSet = {}; |
|
this.observers = []; |
|
}; |
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var META_KEY = Ember.META_KEY, |
|
guidFor = Ember.guidFor, |
|
tryFinally = Ember.tryFinally, |
|
sendEvent = Ember.sendEvent, |
|
listenersUnion = Ember.listenersUnion, |
|
listenersDiff = Ember.listenersDiff, |
|
ObserverSet = Ember._ObserverSet, |
|
beforeObserverSet = new ObserverSet(), |
|
observerSet = new ObserverSet(), |
|
deferred = 0; |
|
|
|
// .......................................................... |
|
// PROPERTY CHANGES |
|
// |
|
|
|
/** |
|
This function is called just before an object property is about to change. |
|
It will notify any before observers and prepare caches among other things. |
|
|
|
Normally you will not need to call this method directly but if for some |
|
reason you can't directly watch a property you can invoke this method |
|
manually along with `Ember.propertyDidChange()` which you should call just |
|
after the property value changes. |
|
|
|
@method propertyWillChange |
|
@for Ember |
|
@param {Object} obj The object with the property that will change |
|
@param {String} keyName The property key (or path) that will change. |
|
@return {void} |
|
*/ |
|
function propertyWillChange(obj, keyName) { |
|
var m = obj[META_KEY], |
|
watching = (m && m.watching[keyName] > 0) || keyName === 'length', |
|
proto = m && m.proto, |
|
desc = m && m.descs[keyName]; |
|
|
|
if (!watching) { return; } |
|
if (proto === obj) { return; } |
|
if (desc && desc.willChange) { desc.willChange(obj, keyName); } |
|
dependentKeysWillChange(obj, keyName, m); |
|
chainsWillChange(obj, keyName, m); |
|
notifyBeforeObservers(obj, keyName); |
|
} |
|
Ember.propertyWillChange = propertyWillChange; |
|
|
|
/** |
|
This function is called just after an object property has changed. |
|
It will notify any observers and clear caches among other things. |
|
|
|
Normally you will not need to call this method directly but if for some |
|
reason you can't directly watch a property you can invoke this method |
|
manually along with `Ember.propertyWillChange()` which you should call just |
|
before the property value changes. |
|
|
|
@method propertyDidChange |
|
@for Ember |
|
@param {Object} obj The object with the property that will change |
|
@param {String} keyName The property key (or path) that will change. |
|
@return {void} |
|
*/ |
|
function propertyDidChange(obj, keyName) { |
|
var m = obj[META_KEY], |
|
watching = (m && m.watching[keyName] > 0) || keyName === 'length', |
|
proto = m && m.proto, |
|
desc = m && m.descs[keyName]; |
|
|
|
if (proto === obj) { return; } |
|
|
|
// shouldn't this mean that we're watching this key? |
|
if (desc && desc.didChange) { desc.didChange(obj, keyName); } |
|
if (!watching && keyName !== 'length') { return; } |
|
|
|
dependentKeysDidChange(obj, keyName, m); |
|
chainsDidChange(obj, keyName, m, false); |
|
notifyObservers(obj, keyName); |
|
} |
|
Ember.propertyDidChange = propertyDidChange; |
|
|
|
var WILL_SEEN, DID_SEEN; |
|
|
|
// called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) |
|
function dependentKeysWillChange(obj, depKey, meta) { |
|
if (obj.isDestroying) { return; } |
|
|
|
var seen = WILL_SEEN, top = !seen; |
|
if (top) { seen = WILL_SEEN = {}; } |
|
iterDeps(propertyWillChange, obj, depKey, seen, meta); |
|
if (top) { WILL_SEEN = null; } |
|
} |
|
|
|
// called whenever a property has just changed to update dependent keys |
|
function dependentKeysDidChange(obj, depKey, meta) { |
|
if (obj.isDestroying) { return; } |
|
|
|
var seen = DID_SEEN, top = !seen; |
|
if (top) { seen = DID_SEEN = {}; } |
|
iterDeps(propertyDidChange, obj, depKey, seen, meta); |
|
if (top) { DID_SEEN = null; } |
|
} |
|
|
|
function iterDeps(method, obj, depKey, seen, meta) { |
|
var guid = guidFor(obj); |
|
if (!seen[guid]) seen[guid] = {}; |
|
if (seen[guid][depKey]) return; |
|
seen[guid][depKey] = true; |
|
|
|
var deps = meta.deps; |
|
deps = deps && deps[depKey]; |
|
if (deps) { |
|
for(var key in deps) { |
|
var desc = meta.descs[key]; |
|
if (desc && desc._suspended === obj) continue; |
|
method(obj, key); |
|
} |
|
} |
|
} |
|
|
|
function chainsWillChange(obj, keyName, m) { |
|
if (!(m.hasOwnProperty('chainWatchers') && |
|
m.chainWatchers[keyName])) { |
|
return; |
|
} |
|
|
|
var nodes = m.chainWatchers[keyName], |
|
events = [], |
|
i, l; |
|
|
|
for(i = 0, l = nodes.length; i < l; i++) { |
|
nodes[i].willChange(events); |
|
} |
|
|
|
for (i = 0, l = events.length; i < l; i += 2) { |
|
propertyWillChange(events[i], events[i+1]); |
|
} |
|
} |
|
|
|
function chainsDidChange(obj, keyName, m, suppressEvents) { |
|
if (!(m && m.hasOwnProperty('chainWatchers') && |
|
m.chainWatchers[keyName])) { |
|
return; |
|
} |
|
|
|
var nodes = m.chainWatchers[keyName], |
|
events = suppressEvents ? null : [], |
|
i, l; |
|
|
|
for(i = 0, l = nodes.length; i < l; i++) { |
|
nodes[i].didChange(events); |
|
} |
|
|
|
if (suppressEvents) { |
|
return; |
|
} |
|
|
|
for (i = 0, l = events.length; i < l; i += 2) { |
|
propertyDidChange(events[i], events[i+1]); |
|
} |
|
} |
|
|
|
Ember.overrideChains = function(obj, keyName, m) { |
|
chainsDidChange(obj, keyName, m, true); |
|
}; |
|
|
|
/** |
|
@method beginPropertyChanges |
|
@chainable |
|
@private |
|
*/ |
|
function beginPropertyChanges() { |
|
deferred++; |
|
} |
|
|
|
Ember.beginPropertyChanges = beginPropertyChanges; |
|
|
|
/** |
|
@method endPropertyChanges |
|
@private |
|
*/ |
|
function endPropertyChanges() { |
|
deferred--; |
|
if (deferred<=0) { |
|
beforeObserverSet.clear(); |
|
observerSet.flush(); |
|
} |
|
} |
|
|
|
Ember.endPropertyChanges = endPropertyChanges; |
|
|
|
/** |
|
Make a series of property changes together in an |
|
exception-safe way. |
|
|
|
```javascript |
|
Ember.changeProperties(function() { |
|
obj1.set('foo', mayBlowUpWhenSet); |
|
obj2.set('bar', baz); |
|
}); |
|
``` |
|
|
|
@method changeProperties |
|
@param {Function} callback |
|
@param [binding] |
|
*/ |
|
Ember.changeProperties = function(cb, binding) { |
|
beginPropertyChanges(); |
|
tryFinally(cb, endPropertyChanges, binding); |
|
}; |
|
|
|
function notifyBeforeObservers(obj, keyName) { |
|
if (obj.isDestroying) { return; } |
|
|
|
var eventName = keyName + ':before', listeners, diff; |
|
if (deferred) { |
|
listeners = beforeObserverSet.add(obj, keyName, eventName); |
|
diff = listenersDiff(obj, eventName, listeners); |
|
sendEvent(obj, eventName, [obj, keyName], diff); |
|
} else { |
|
sendEvent(obj, eventName, [obj, keyName]); |
|
} |
|
} |
|
|
|
function notifyObservers(obj, keyName) { |
|
if (obj.isDestroying) { return; } |
|
|
|
var eventName = keyName + ':change', listeners; |
|
if (deferred) { |
|
listeners = observerSet.add(obj, keyName, eventName); |
|
listenersUnion(obj, eventName, listeners); |
|
} else { |
|
sendEvent(obj, eventName, [obj, keyName]); |
|
} |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
// META_KEY |
|
// _getPath |
|
// propertyWillChange, propertyDidChange |
|
|
|
var META_KEY = Ember.META_KEY, |
|
MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, |
|
IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/, |
|
getPath = Ember._getPath; |
|
|
|
/** |
|
Sets the value of a property on an object, respecting computed properties |
|
and notifying observers and other listeners of the change. If the |
|
property is not defined but the object implements the `setUnknownProperty` |
|
method then that will be invoked as well. |
|
|
|
@method set |
|
@for Ember |
|
@param {Object} obj The object to modify. |
|
@param {String} keyName The property key to set |
|
@param {Object} value The value to set |
|
@return {Object} the passed value. |
|
*/ |
|
var set = function set(obj, keyName, value, tolerant) { |
|
if (typeof obj === 'string') { |
|
Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj)); |
|
value = keyName; |
|
keyName = obj; |
|
obj = null; |
|
} |
|
|
|
Ember.assert("Cannot call set with "+ keyName +" key.", !!keyName); |
|
|
|
if (!obj || keyName.indexOf('.') !== -1) { |
|
return setPath(obj, keyName, value, tolerant); |
|
} |
|
|
|
Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); |
|
Ember.assert('calling set on destroyed object', !obj.isDestroyed); |
|
|
|
var meta = obj[META_KEY], desc = meta && meta.descs[keyName], |
|
isUnknown, currentValue; |
|
if (desc) { |
|
desc.set(obj, keyName, value); |
|
} else { |
|
isUnknown = 'object' === typeof obj && !(keyName in obj); |
|
|
|
// setUnknownProperty is called if `obj` is an object, |
|
// the property does not already exist, and the |
|
// `setUnknownProperty` method exists on the object |
|
if (isUnknown && 'function' === typeof obj.setUnknownProperty) { |
|
obj.setUnknownProperty(keyName, value); |
|
} else if (meta && meta.watching[keyName] > 0) { |
|
if (MANDATORY_SETTER) { |
|
currentValue = meta.values[keyName]; |
|
} else { |
|
currentValue = obj[keyName]; |
|
} |
|
// only trigger a change if the value has changed |
|
if (value !== currentValue) { |
|
Ember.propertyWillChange(obj, keyName); |
|
if (MANDATORY_SETTER) { |
|
if ((currentValue === undefined && !(keyName in obj)) || !obj.propertyIsEnumerable(keyName)) { |
|
Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter |
|
} else { |
|
meta.values[keyName] = value; |
|
} |
|
} else { |
|
obj[keyName] = value; |
|
} |
|
Ember.propertyDidChange(obj, keyName); |
|
} |
|
} else { |
|
obj[keyName] = value; |
|
} |
|
} |
|
return value; |
|
}; |
|
|
|
// Currently used only by Ember Data tests |
|
if (Ember.config.overrideAccessors) { |
|
Ember.set = set; |
|
Ember.config.overrideAccessors(); |
|
set = Ember.set; |
|
} |
|
|
|
function setPath(root, path, value, tolerant) { |
|
var keyName; |
|
|
|
// get the last part of the path |
|
keyName = path.slice(path.lastIndexOf('.') + 1); |
|
|
|
// get the first part of the part |
|
path = (path === keyName) ? keyName : path.slice(0, path.length-(keyName.length+1)); |
|
|
|
// unless the path is this, look up the first part to |
|
// get the root |
|
if (path !== 'this') { |
|
root = getPath(root, path); |
|
} |
|
|
|
if (!keyName || keyName.length === 0) { |
|
throw new Ember.Error('Property set failed: You passed an empty path'); |
|
} |
|
|
|
if (!root) { |
|
if (tolerant) { return; } |
|
else { throw new Ember.Error('Property set failed: object in path "'+path+'" could not be found or was destroyed.'); } |
|
} |
|
|
|
return set(root, keyName, value); |
|
} |
|
|
|
Ember.set = set; |
|
|
|
/** |
|
Error-tolerant form of `Ember.set`. Will not blow up if any part of the |
|
chain is `undefined`, `null`, or destroyed. |
|
|
|
This is primarily used when syncing bindings, which may try to update after |
|
an object has been destroyed. |
|
|
|
@method trySet |
|
@for Ember |
|
@param {Object} obj The object to modify. |
|
@param {String} path The property path to set |
|
@param {Object} value The value to set |
|
*/ |
|
Ember.trySet = function(root, path, value) { |
|
return set(root, path, value, true); |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
/* |
|
JavaScript (before ES6) does not have a Map implementation. Objects, |
|
which are often used as dictionaries, may only have Strings as keys. |
|
|
|
Because Ember has a way to get a unique identifier for every object |
|
via `Ember.guidFor`, we can implement a performant Map with arbitrary |
|
keys. Because it is commonly used in low-level bookkeeping, Map is |
|
implemented as a pure JavaScript object for performance. |
|
|
|
This implementation follows the current iteration of the ES6 proposal for |
|
maps (http://wiki.ecmascript.org/doku.php?id=harmony:simple_maps_and_sets), |
|
with two exceptions. First, because we need our implementation to be pleasant |
|
on older browsers, we do not use the `delete` name (using `remove` instead). |
|
Second, as we do not have the luxury of in-VM iteration, we implement a |
|
forEach method for iteration. |
|
|
|
Map is mocked out to look like an Ember object, so you can do |
|
`Ember.Map.create()` for symmetry with other Ember classes. |
|
*/ |
|
var set = Ember.set, |
|
guidFor = Ember.guidFor, |
|
indexOf = Ember.ArrayPolyfills.indexOf; |
|
|
|
var copy = function(obj) { |
|
var output = {}; |
|
|
|
for (var prop in obj) { |
|
if (obj.hasOwnProperty(prop)) { output[prop] = obj[prop]; } |
|
} |
|
|
|
return output; |
|
}; |
|
|
|
var copyMap = function(original, newObject) { |
|
var keys = original.keys.copy(), |
|
values = copy(original.values); |
|
|
|
newObject.keys = keys; |
|
newObject.values = values; |
|
newObject.length = original.length; |
|
|
|
return newObject; |
|
}; |
|
|
|
/** |
|
This class is used internally by Ember and Ember Data. |
|
Please do not use it at this time. We plan to clean it up |
|
and add many tests soon. |
|
|
|
@class OrderedSet |
|
@namespace Ember |
|
@constructor |
|
@private |
|
*/ |
|
var OrderedSet = Ember.OrderedSet = function() { |
|
this.clear(); |
|
}; |
|
|
|
/** |
|
@method create |
|
@static |
|
@return {Ember.OrderedSet} |
|
*/ |
|
OrderedSet.create = function() { |
|
return new OrderedSet(); |
|
}; |
|
|
|
|
|
OrderedSet.prototype = { |
|
/** |
|
@method clear |
|
*/ |
|
clear: function() { |
|
this.presenceSet = {}; |
|
this.list = []; |
|
}, |
|
|
|
/** |
|
@method add |
|
@param obj |
|
*/ |
|
add: function(obj) { |
|
var guid = guidFor(obj), |
|
presenceSet = this.presenceSet, |
|
list = this.list; |
|
|
|
if (guid in presenceSet) { return; } |
|
|
|
presenceSet[guid] = true; |
|
list.push(obj); |
|
}, |
|
|
|
/** |
|
@method remove |
|
@param obj |
|
*/ |
|
remove: function(obj) { |
|
var guid = guidFor(obj), |
|
presenceSet = this.presenceSet, |
|
list = this.list; |
|
|
|
delete presenceSet[guid]; |
|
|
|
var index = indexOf.call(list, obj); |
|
if (index > -1) { |
|
list.splice(index, 1); |
|
} |
|
}, |
|
|
|
/** |
|
@method isEmpty |
|
@return {Boolean} |
|
*/ |
|
isEmpty: function() { |
|
return this.list.length === 0; |
|
}, |
|
|
|
/** |
|
@method has |
|
@param obj |
|
@return {Boolean} |
|
*/ |
|
has: function(obj) { |
|
var guid = guidFor(obj), |
|
presenceSet = this.presenceSet; |
|
|
|
return guid in presenceSet; |
|
}, |
|
|
|
/** |
|
@method forEach |
|
@param {Function} fn |
|
@param self |
|
*/ |
|
forEach: function(fn, self) { |
|
// allow mutation during iteration |
|
var list = this.toArray(); |
|
|
|
for (var i = 0, j = list.length; i < j; i++) { |
|
fn.call(self, list[i]); |
|
} |
|
}, |
|
|
|
/** |
|
@method toArray |
|
@return {Array} |
|
*/ |
|
toArray: function() { |
|
return this.list.slice(); |
|
}, |
|
|
|
/** |
|
@method copy |
|
@return {Ember.OrderedSet} |
|
*/ |
|
copy: function() { |
|
var set = new OrderedSet(); |
|
|
|
set.presenceSet = copy(this.presenceSet); |
|
set.list = this.toArray(); |
|
|
|
return set; |
|
} |
|
}; |
|
|
|
/** |
|
A Map stores values indexed by keys. Unlike JavaScript's |
|
default Objects, the keys of a Map can be any JavaScript |
|
object. |
|
|
|
Internally, a Map has two data structures: |
|
|
|
1. `keys`: an OrderedSet of all of the existing keys |
|
2. `values`: a JavaScript Object indexed by the `Ember.guidFor(key)` |
|
|
|
When a key/value pair is added for the first time, we |
|
add the key to the `keys` OrderedSet, and create or |
|
replace an entry in `values`. When an entry is deleted, |
|
we delete its entry in `keys` and `values`. |
|
|
|
@class Map |
|
@namespace Ember |
|
@private |
|
@constructor |
|
*/ |
|
var Map = Ember.Map = function() { |
|
this.keys = Ember.OrderedSet.create(); |
|
this.values = {}; |
|
}; |
|
|
|
/** |
|
@method create |
|
@static |
|
*/ |
|
Map.create = function() { |
|
return new Map(); |
|
}; |
|
|
|
Map.prototype = { |
|
/** |
|
This property will change as the number of objects in the map changes. |
|
|
|
@property length |
|
@type number |
|
@default 0 |
|
*/ |
|
length: 0, |
|
|
|
|
|
/** |
|
Retrieve the value associated with a given key. |
|
|
|
@method get |
|
@param {*} key |
|
@return {*} the value associated with the key, or `undefined` |
|
*/ |
|
get: function(key) { |
|
var values = this.values, |
|
guid = guidFor(key); |
|
|
|
return values[guid]; |
|
}, |
|
|
|
/** |
|
Adds a value to the map. If a value for the given key has already been |
|
provided, the new value will replace the old value. |
|
|
|
@method set |
|
@param {*} key |
|
@param {*} value |
|
*/ |
|
set: function(key, value) { |
|
var keys = this.keys, |
|
values = this.values, |
|
guid = guidFor(key); |
|
|
|
keys.add(key); |
|
values[guid] = value; |
|
set(this, 'length', keys.list.length); |
|
}, |
|
|
|
/** |
|
Removes a value from the map for an associated key. |
|
|
|
@method remove |
|
@param {*} key |
|
@return {Boolean} true if an item was removed, false otherwise |
|
*/ |
|
remove: function(key) { |
|
// don't use ES6 "delete" because it will be annoying |
|
// to use in browsers that are not ES6 friendly; |
|
var keys = this.keys, |
|
values = this.values, |
|
guid = guidFor(key); |
|
|
|
if (values.hasOwnProperty(guid)) { |
|
keys.remove(key); |
|
delete values[guid]; |
|
set(this, 'length', keys.list.length); |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
}, |
|
|
|
/** |
|
Check whether a key is present. |
|
|
|
@method has |
|
@param {*} key |
|
@return {Boolean} true if the item was present, false otherwise |
|
*/ |
|
has: function(key) { |
|
var values = this.values, |
|
guid = guidFor(key); |
|
|
|
return values.hasOwnProperty(guid); |
|
}, |
|
|
|
/** |
|
Iterate over all the keys and values. Calls the function once |
|
for each key, passing in the key and value, in that order. |
|
|
|
The keys are guaranteed to be iterated over in insertion order. |
|
|
|
@method forEach |
|
@param {Function} callback |
|
@param {*} self if passed, the `this` value inside the |
|
callback. By default, `this` is the map. |
|
*/ |
|
forEach: function(callback, self) { |
|
var keys = this.keys, |
|
values = this.values; |
|
|
|
keys.forEach(function(key) { |
|
var guid = guidFor(key); |
|
callback.call(self, key, values[guid]); |
|
}); |
|
}, |
|
|
|
/** |
|
@method copy |
|
@return {Ember.Map} |
|
*/ |
|
copy: function() { |
|
return copyMap(this, new Map()); |
|
} |
|
}; |
|
|
|
/** |
|
@class MapWithDefault |
|
@namespace Ember |
|
@extends Ember.Map |
|
@private |
|
@constructor |
|
@param [options] |
|
@param {*} [options.defaultValue] |
|
*/ |
|
var MapWithDefault = Ember.MapWithDefault = function(options) { |
|
Map.call(this); |
|
this.defaultValue = options.defaultValue; |
|
}; |
|
|
|
/** |
|
@method create |
|
@static |
|
@param [options] |
|
@param {*} [options.defaultValue] |
|
@return {Ember.MapWithDefault|Ember.Map} If options are passed, returns |
|
`Ember.MapWithDefault` otherwise returns `Ember.Map` |
|
*/ |
|
MapWithDefault.create = function(options) { |
|
if (options) { |
|
return new MapWithDefault(options); |
|
} else { |
|
return new Map(); |
|
} |
|
}; |
|
|
|
MapWithDefault.prototype = Ember.create(Map.prototype); |
|
|
|
/** |
|
Retrieve the value associated with a given key. |
|
|
|
@method get |
|
@param {*} key |
|
@return {*} the value associated with the key, or the default value |
|
*/ |
|
MapWithDefault.prototype.get = function(key) { |
|
var hasValue = this.has(key); |
|
|
|
if (hasValue) { |
|
return Map.prototype.get.call(this, key); |
|
} else { |
|
var defaultValue = this.defaultValue(key); |
|
this.set(key, defaultValue); |
|
return defaultValue; |
|
} |
|
}; |
|
|
|
/** |
|
@method copy |
|
@return {Ember.MapWithDefault} |
|
*/ |
|
MapWithDefault.prototype.copy = function() { |
|
return copyMap(this, new MapWithDefault({ |
|
defaultValue: this.defaultValue |
|
})); |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
function consoleMethod(name) { |
|
var consoleObj, logToConsole; |
|
if (Ember.imports.console) { |
|
consoleObj = Ember.imports.console; |
|
} else if (typeof console !== 'undefined') { |
|
consoleObj = console; |
|
} |
|
|
|
var method = typeof consoleObj === 'object' ? consoleObj[name] : null; |
|
|
|
if (method) { |
|
// Older IE doesn't support apply, but Chrome needs it |
|
if (typeof method.apply === 'function') { |
|
logToConsole = function() { |
|
method.apply(consoleObj, arguments); |
|
}; |
|
logToConsole.displayName = 'console.' + name; |
|
return logToConsole; |
|
} else { |
|
return function() { |
|
var message = Array.prototype.join.call(arguments, ', '); |
|
method(message); |
|
}; |
|
} |
|
} |
|
} |
|
|
|
function assertPolyfill(test, message) { |
|
if (!test) { |
|
try { |
|
// attempt to preserve the stack |
|
throw new Ember.Error("assertion failed: " + message); |
|
} catch(error) { |
|
setTimeout(function() { |
|
throw error; |
|
}, 0); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
Inside Ember-Metal, simply uses the methods from `imports.console`. |
|
Override this to provide more robust logging functionality. |
|
|
|
@class Logger |
|
@namespace Ember |
|
*/ |
|
Ember.Logger = { |
|
/** |
|
Logs the arguments to the console. |
|
You can pass as many arguments as you want and they will be joined together with a space. |
|
|
|
```javascript |
|
var foo = 1; |
|
Ember.Logger.log('log value of foo:', foo); |
|
// "log value of foo: 1" will be printed to the console |
|
``` |
|
|
|
@method log |
|
@for Ember.Logger |
|
@param {*} arguments |
|
*/ |
|
log: consoleMethod('log') || Ember.K, |
|
|
|
/** |
|
Prints the arguments to the console with a warning icon. |
|
You can pass as many arguments as you want and they will be joined together with a space. |
|
|
|
```javascript |
|
Ember.Logger.warn('Something happened!'); |
|
// "Something happened!" will be printed to the console with a warning icon. |
|
``` |
|
|
|
@method warn |
|
@for Ember.Logger |
|
@param {*} arguments |
|
*/ |
|
warn: consoleMethod('warn') || Ember.K, |
|
|
|
/** |
|
Prints the arguments to the console with an error icon, red text and a stack trace. |
|
You can pass as many arguments as you want and they will be joined together with a space. |
|
|
|
```javascript |
|
Ember.Logger.error('Danger! Danger!'); |
|
// "Danger! Danger!" will be printed to the console in red text. |
|
``` |
|
|
|
@method error |
|
@for Ember.Logger |
|
@param {*} arguments |
|
*/ |
|
error: consoleMethod('error') || Ember.K, |
|
|
|
/** |
|
Logs the arguments to the console. |
|
You can pass as many arguments as you want and they will be joined together with a space. |
|
|
|
```javascript |
|
var foo = 1; |
|
Ember.Logger.info('log value of foo:', foo); |
|
// "log value of foo: 1" will be printed to the console |
|
``` |
|
|
|
@method info |
|
@for Ember.Logger |
|
@param {*} arguments |
|
*/ |
|
info: consoleMethod('info') || Ember.K, |
|
|
|
/** |
|
Logs the arguments to the console in blue text. |
|
You can pass as many arguments as you want and they will be joined together with a space. |
|
|
|
```javascript |
|
var foo = 1; |
|
Ember.Logger.debug('log value of foo:', foo); |
|
// "log value of foo: 1" will be printed to the console |
|
``` |
|
|
|
@method debug |
|
@for Ember.Logger |
|
@param {*} arguments |
|
*/ |
|
debug: consoleMethod('debug') || consoleMethod('info') || Ember.K, |
|
|
|
/** |
|
If the value passed into `Ember.Logger.assert` is not truthy it will throw an error with a stack trace. |
|
|
|
```javascript |
|
Ember.Logger.assert(true); // undefined |
|
Ember.Logger.assert(true === false); // Throws an Assertion failed error. |
|
``` |
|
|
|
@method assert |
|
@for Ember.Logger |
|
@param {Boolean} bool Value to test |
|
*/ |
|
assert: consoleMethod('assert') || assertPolyfill |
|
}; |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
var META_KEY = Ember.META_KEY, |
|
metaFor = Ember.meta, |
|
objectDefineProperty = Ember.platform.defineProperty; |
|
|
|
var MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER; |
|
|
|
// .......................................................... |
|
// DESCRIPTOR |
|
// |
|
|
|
/** |
|
Objects of this type can implement an interface to respond to requests to |
|
get and set. The default implementation handles simple properties. |
|
|
|
You generally won't need to create or subclass this directly. |
|
|
|
@class Descriptor |
|
@namespace Ember |
|
@private |
|
@constructor |
|
*/ |
|
Ember.Descriptor = function() {}; |
|
|
|
// .......................................................... |
|
// DEFINING PROPERTIES API |
|
// |
|
|
|
var MANDATORY_SETTER_FUNCTION = Ember.MANDATORY_SETTER_FUNCTION = function(value) { |
|
Ember.assert("You must use Ember.set() to access this property (of " + this + ")", false); |
|
}; |
|
|
|
var DEFAULT_GETTER_FUNCTION = Ember.DEFAULT_GETTER_FUNCTION = function(name) { |
|
return function() { |
|
var meta = this[META_KEY]; |
|
return meta && meta.values[name]; |
|
}; |
|
}; |
|
|
|
/** |
|
NOTE: This is a low-level method used by other parts of the API. You almost |
|
never want to call this method directly. Instead you should use |
|
`Ember.mixin()` to define new properties. |
|
|
|
Defines a property on an object. This method works much like the ES5 |
|
`Object.defineProperty()` method except that it can also accept computed |
|
properties and other special descriptors. |
|
|
|
Normally this method takes only three parameters. However if you pass an |
|
instance of `Ember.Descriptor` as the third param then you can pass an |
|
optional value as the fourth parameter. This is often more efficient than |
|
creating new descriptor hashes for each property. |
|
|
|
## Examples |
|
|
|
```javascript |
|
// ES5 compatible mode |
|
Ember.defineProperty(contact, 'firstName', { |
|
writable: true, |
|
configurable: false, |
|
enumerable: true, |
|
value: 'Charles' |
|
}); |
|
|
|
// define a simple property |
|
Ember.defineProperty(contact, 'lastName', undefined, 'Jolley'); |
|
|
|
// define a computed property |
|
Ember.defineProperty(contact, 'fullName', Ember.computed(function() { |
|
return this.firstName+' '+this.lastName; |
|
}).property('firstName', 'lastName')); |
|
``` |
|
|
|
@private |
|
@method defineProperty |
|
@for Ember |
|
@param {Object} obj the object to define this property on. This may be a prototype. |
|
@param {String} keyName the name of the property |
|
@param {Ember.Descriptor} [desc] an instance of `Ember.Descriptor` (typically a |
|
computed property) or an ES5 descriptor. |
|
You must provide this or `data` but not both. |
|
@param {*} [data] something other than a descriptor, that will |
|
become the explicit value of this property. |
|
*/ |
|
Ember.defineProperty = function(obj, keyName, desc, data, meta) { |
|
var descs, existingDesc, watching, value; |
|
|
|
if (!meta) meta = metaFor(obj); |
|
descs = meta.descs; |
|
existingDesc = meta.descs[keyName]; |
|
watching = meta.watching[keyName] > 0; |
|
|
|
if (existingDesc instanceof Ember.Descriptor) { |
|
existingDesc.teardown(obj, keyName); |
|
} |
|
|
|
if (desc instanceof Ember.Descriptor) { |
|
value = desc; |
|
|
|
descs[keyName] = desc; |
|
if (MANDATORY_SETTER && watching) { |
|
objectDefineProperty(obj, keyName, { |
|
configurable: true, |
|
enumerable: true, |
|
writable: true, |
|
value: undefined // make enumerable |
|
}); |
|
} else { |
|
obj[keyName] = undefined; // make enumerable |
|
} |
|
|
|
if (Ember.FEATURES.isEnabled('composable-computed-properties')) { |
|
if (desc.func && desc._dependentCPs) { |
|
addImplicitCPs(obj, desc._dependentCPs, meta); |
|
} |
|
} |
|
} else { |
|
descs[keyName] = undefined; // shadow descriptor in proto |
|
if (desc == null) { |
|
value = data; |
|
|
|
if (MANDATORY_SETTER && watching) { |
|
meta.values[keyName] = data; |
|
objectDefineProperty(obj, keyName, { |
|
configurable: true, |
|
enumerable: true, |
|
set: MANDATORY_SETTER_FUNCTION, |
|
get: DEFAULT_GETTER_FUNCTION(keyName) |
|
}); |
|
} else { |
|
obj[keyName] = data; |
|
} |
|
} else { |
|
value = desc; |
|
|
|
// compatibility with ES5 |
|
objectDefineProperty(obj, keyName, desc); |
|
} |
|
} |
|
|
|
// if key is being watched, override chains that |
|
// were initialized with the prototype |
|
if (watching) { Ember.overrideChains(obj, keyName, meta); } |
|
|
|
// The `value` passed to the `didDefineProperty` hook is |
|
// either the descriptor or data, whichever was passed. |
|
if (obj.didDefineProperty) { obj.didDefineProperty(obj, keyName, value); } |
|
|
|
return this; |
|
}; |
|
|
|
if (Ember.FEATURES.isEnabled('composable-computed-properties')) { |
|
var addImplicitCPs = function defineImplicitCPs(obj, implicitCPs, meta) { |
|
var cp, key, length = implicitCPs.length; |
|
|
|
for (var i=0; i<length; ++i) { |
|
cp = implicitCPs[i]; |
|
key = cp.implicitCPKey; |
|
|
|
Ember.defineProperty(obj, key, cp, undefined, meta); |
|
|
|
if (cp._dependentCPs) { |
|
addImplicitCPs(obj, cp._dependentCPs, meta); |
|
} |
|
} |
|
}; |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var get = Ember.get; |
|
|
|
/** |
|
To get multiple properties at once, call `Ember.getProperties` |
|
with an object followed by a list of strings or an array: |
|
|
|
```javascript |
|
Ember.getProperties(record, 'firstName', 'lastName', 'zipCode'); |
|
// { firstName: 'John', lastName: 'Doe', zipCode: '10011' } |
|
``` |
|
|
|
is equivalent to: |
|
|
|
```javascript |
|
Ember.getProperties(record, ['firstName', 'lastName', 'zipCode']); |
|
// { firstName: 'John', lastName: 'Doe', zipCode: '10011' } |
|
``` |
|
|
|
@method getProperties |
|
@param obj |
|
@param {String...|Array} list of keys to get |
|
@return {Hash} |
|
*/ |
|
Ember.getProperties = function(obj) { |
|
var ret = {}, |
|
propertyNames = arguments, |
|
i = 1; |
|
|
|
if (arguments.length === 2 && Ember.typeOf(arguments[1]) === 'array') { |
|
i = 0; |
|
propertyNames = arguments[1]; |
|
} |
|
for(var len = propertyNames.length; i < len; i++) { |
|
ret[propertyNames[i]] = get(obj, propertyNames[i]); |
|
} |
|
return ret; |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var changeProperties = Ember.changeProperties, |
|
set = Ember.set; |
|
|
|
/** |
|
Set a list of properties on an object. These properties are set inside |
|
a single `beginPropertyChanges` and `endPropertyChanges` batch, so |
|
observers will be buffered. |
|
|
|
```javascript |
|
anObject.setProperties({ |
|
firstName: "Stanley", |
|
lastName: "Stuart", |
|
age: "21" |
|
}) |
|
``` |
|
|
|
@method setProperties |
|
@param self |
|
@param {Object} hash |
|
@return self |
|
*/ |
|
Ember.setProperties = function(self, hash) { |
|
changeProperties(function() { |
|
for(var prop in hash) { |
|
if (hash.hasOwnProperty(prop)) { set(self, prop, hash[prop]); } |
|
} |
|
}); |
|
return self; |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var metaFor = Ember.meta, // utils.js |
|
typeOf = Ember.typeOf, // utils.js |
|
MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, |
|
o_defineProperty = Ember.platform.defineProperty; |
|
|
|
Ember.watchKey = function(obj, keyName, meta) { |
|
// can't watch length on Array - it is special... |
|
if (keyName === 'length' && typeOf(obj) === 'array') { return; } |
|
|
|
var m = meta || metaFor(obj), watching = m.watching; |
|
|
|
// activate watching first time |
|
if (!watching[keyName]) { |
|
watching[keyName] = 1; |
|
|
|
if ('function' === typeof obj.willWatchProperty) { |
|
obj.willWatchProperty(keyName); |
|
} |
|
|
|
if (MANDATORY_SETTER && keyName in obj) { |
|
m.values[keyName] = obj[keyName]; |
|
o_defineProperty(obj, keyName, { |
|
configurable: true, |
|
enumerable: obj.propertyIsEnumerable(keyName), |
|
set: Ember.MANDATORY_SETTER_FUNCTION, |
|
get: Ember.DEFAULT_GETTER_FUNCTION(keyName) |
|
}); |
|
} |
|
} else { |
|
watching[keyName] = (watching[keyName] || 0) + 1; |
|
} |
|
}; |
|
|
|
|
|
Ember.unwatchKey = function(obj, keyName, meta) { |
|
var m = meta || metaFor(obj), watching = m.watching; |
|
|
|
if (watching[keyName] === 1) { |
|
watching[keyName] = 0; |
|
|
|
if ('function' === typeof obj.didUnwatchProperty) { |
|
obj.didUnwatchProperty(keyName); |
|
} |
|
|
|
if (MANDATORY_SETTER && keyName in obj) { |
|
o_defineProperty(obj, keyName, { |
|
configurable: true, |
|
enumerable: obj.propertyIsEnumerable(keyName), |
|
set: function(val) { |
|
// redefine to set as enumerable |
|
o_defineProperty(obj, keyName, { |
|
configurable: true, |
|
writable: true, |
|
enumerable: true, |
|
value: val |
|
}); |
|
delete m.values[keyName]; |
|
}, |
|
get: Ember.DEFAULT_GETTER_FUNCTION(keyName) |
|
}); |
|
} |
|
} else if (watching[keyName] > 1) { |
|
watching[keyName]--; |
|
} |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var metaFor = Ember.meta, // utils.js |
|
get = Ember.get, // property_get.js |
|
normalizeTuple = Ember.normalizeTuple, // property_get.js |
|
forEach = Ember.ArrayPolyfills.forEach, // array.js |
|
warn = Ember.warn, |
|
watchKey = Ember.watchKey, |
|
unwatchKey = Ember.unwatchKey, |
|
FIRST_KEY = /^([^\.\*]+)/, |
|
META_KEY = Ember.META_KEY; |
|
|
|
function firstKey(path) { |
|
return path.match(FIRST_KEY)[0]; |
|
} |
|
|
|
var pendingQueue = []; |
|
|
|
// attempts to add the pendingQueue chains again. If some of them end up |
|
// back in the queue and reschedule is true, schedules a timeout to try |
|
// again. |
|
Ember.flushPendingChains = function() { |
|
if (pendingQueue.length === 0) { return; } // nothing to do |
|
|
|
var queue = pendingQueue; |
|
pendingQueue = []; |
|
|
|
forEach.call(queue, function(q) { q[0].add(q[1]); }); |
|
|
|
warn('Watching an undefined global, Ember expects watched globals to be setup by the time the run loop is flushed, check for typos', pendingQueue.length === 0); |
|
}; |
|
|
|
|
|
function addChainWatcher(obj, keyName, node) { |
|
if (!obj || ('object' !== typeof obj)) { return; } // nothing to do |
|
|
|
var m = metaFor(obj), nodes = m.chainWatchers; |
|
|
|
if (!m.hasOwnProperty('chainWatchers')) { |
|
nodes = m.chainWatchers = {}; |
|
} |
|
|
|
if (!nodes[keyName]) { nodes[keyName] = []; } |
|
nodes[keyName].push(node); |
|
watchKey(obj, keyName, m); |
|
} |
|
|
|
var removeChainWatcher = Ember.removeChainWatcher = function(obj, keyName, node) { |
|
if (!obj || 'object' !== typeof obj) { return; } // nothing to do |
|
|
|
var m = obj[META_KEY]; |
|
if (m && !m.hasOwnProperty('chainWatchers')) { return; } // nothing to do |
|
|
|
var nodes = m && m.chainWatchers; |
|
|
|
if (nodes && nodes[keyName]) { |
|
nodes = nodes[keyName]; |
|
for (var i = 0, l = nodes.length; i < l; i++) { |
|
if (nodes[i] === node) { nodes.splice(i, 1); } |
|
} |
|
} |
|
unwatchKey(obj, keyName, m); |
|
}; |
|
|
|
// A ChainNode watches a single key on an object. If you provide a starting |
|
// value for the key then the node won't actually watch it. For a root node |
|
// pass null for parent and key and object for value. |
|
var ChainNode = Ember._ChainNode = function(parent, key, value) { |
|
this._parent = parent; |
|
this._key = key; |
|
|
|
// _watching is true when calling get(this._parent, this._key) will |
|
// return the value of this node. |
|
// |
|
// It is false for the root of a chain (because we have no parent) |
|
// and for global paths (because the parent node is the object with |
|
// the observer on it) |
|
this._watching = value===undefined; |
|
|
|
this._value = value; |
|
this._paths = {}; |
|
if (this._watching) { |
|
this._object = parent.value(); |
|
if (this._object) { addChainWatcher(this._object, this._key, this); } |
|
} |
|
|
|
// Special-case: the EachProxy relies on immediate evaluation to |
|
// establish its observers. |
|
// |
|
// TODO: Replace this with an efficient callback that the EachProxy |
|
// can implement. |
|
if (this._parent && this._parent._key === '@each') { |
|
this.value(); |
|
} |
|
}; |
|
|
|
var ChainNodePrototype = ChainNode.prototype; |
|
|
|
function lazyGet(obj, key) { |
|
if (!obj) return undefined; |
|
|
|
var meta = obj[META_KEY]; |
|
// check if object meant only to be a prototype |
|
if (meta && meta.proto === obj) return undefined; |
|
|
|
if (key === "@each") return get(obj, key); |
|
|
|
// if a CP only return cached value |
|
var desc = meta && meta.descs[key]; |
|
if (desc && desc._cacheable) { |
|
if (key in meta.cache) { |
|
return meta.cache[key]; |
|
} else { |
|
return undefined; |
|
} |
|
} |
|
|
|
return get(obj, key); |
|
} |
|
|
|
ChainNodePrototype.value = function() { |
|
if (this._value === undefined && this._watching) { |
|
var obj = this._parent.value(); |
|
this._value = lazyGet(obj, this._key); |
|
} |
|
return this._value; |
|
}; |
|
|
|
ChainNodePrototype.destroy = function() { |
|
if (this._watching) { |
|
var obj = this._object; |
|
if (obj) { removeChainWatcher(obj, this._key, this); } |
|
this._watching = false; // so future calls do nothing |
|
} |
|
}; |
|
|
|
// copies a top level object only |
|
ChainNodePrototype.copy = function(obj) { |
|
var ret = new ChainNode(null, null, obj), |
|
paths = this._paths, path; |
|
for (path in paths) { |
|
if (paths[path] <= 0) { continue; } // this check will also catch non-number vals. |
|
ret.add(path); |
|
} |
|
return ret; |
|
}; |
|
|
|
// called on the root node of a chain to setup watchers on the specified |
|
// path. |
|
ChainNodePrototype.add = function(path) { |
|
var obj, tuple, key, src, paths; |
|
|
|
paths = this._paths; |
|
paths[path] = (paths[path] || 0) + 1; |
|
|
|
obj = this.value(); |
|
tuple = normalizeTuple(obj, path); |
|
|
|
// the path was a local path |
|
if (tuple[0] && tuple[0] === obj) { |
|
path = tuple[1]; |
|
key = firstKey(path); |
|
path = path.slice(key.length+1); |
|
|
|
// global path, but object does not exist yet. |
|
// put into a queue and try to connect later. |
|
} else if (!tuple[0]) { |
|
pendingQueue.push([this, path]); |
|
tuple.length = 0; |
|
return; |
|
|
|
// global path, and object already exists |
|
} else { |
|
src = tuple[0]; |
|
key = path.slice(0, 0-(tuple[1].length+1)); |
|
path = tuple[1]; |
|
} |
|
|
|
tuple.length = 0; |
|
this.chain(key, path, src); |
|
}; |
|
|
|
// called on the root node of a chain to teardown watcher on the specified |
|
// path |
|
ChainNodePrototype.remove = function(path) { |
|
var obj, tuple, key, src, paths; |
|
|
|
paths = this._paths; |
|
if (paths[path] > 0) { paths[path]--; } |
|
|
|
obj = this.value(); |
|
tuple = normalizeTuple(obj, path); |
|
if (tuple[0] === obj) { |
|
path = tuple[1]; |
|
key = firstKey(path); |
|
path = path.slice(key.length+1); |
|
} else { |
|
src = tuple[0]; |
|
key = path.slice(0, 0-(tuple[1].length+1)); |
|
path = tuple[1]; |
|
} |
|
|
|
tuple.length = 0; |
|
this.unchain(key, path); |
|
}; |
|
|
|
ChainNodePrototype.count = 0; |
|
|
|
ChainNodePrototype.chain = function(key, path, src) { |
|
var chains = this._chains, node; |
|
if (!chains) { chains = this._chains = {}; } |
|
|
|
node = chains[key]; |
|
if (!node) { node = chains[key] = new ChainNode(this, key, src); } |
|
node.count++; // count chains... |
|
|
|
// chain rest of path if there is one |
|
if (path && path.length>0) { |
|
key = firstKey(path); |
|
path = path.slice(key.length+1); |
|
node.chain(key, path); // NOTE: no src means it will observe changes... |
|
} |
|
}; |
|
|
|
ChainNodePrototype.unchain = function(key, path) { |
|
var chains = this._chains, node = chains[key]; |
|
|
|
// unchain rest of path first... |
|
if (path && path.length>1) { |
|
key = firstKey(path); |
|
path = path.slice(key.length+1); |
|
node.unchain(key, path); |
|
} |
|
|
|
// delete node if needed. |
|
node.count--; |
|
if (node.count<=0) { |
|
delete chains[node._key]; |
|
node.destroy(); |
|
} |
|
|
|
}; |
|
|
|
ChainNodePrototype.willChange = function(events) { |
|
var chains = this._chains; |
|
if (chains) { |
|
for(var key in chains) { |
|
if (!chains.hasOwnProperty(key)) { continue; } |
|
chains[key].willChange(events); |
|
} |
|
} |
|
|
|
if (this._parent) { this._parent.chainWillChange(this, this._key, 1, events); } |
|
}; |
|
|
|
ChainNodePrototype.chainWillChange = function(chain, path, depth, events) { |
|
if (this._key) { path = this._key + '.' + path; } |
|
|
|
if (this._parent) { |
|
this._parent.chainWillChange(this, path, depth+1, events); |
|
} else { |
|
if (depth > 1) { |
|
events.push(this.value(), path); |
|
} |
|
path = 'this.' + path; |
|
if (this._paths[path] > 0) { |
|
events.push(this.value(), path); |
|
} |
|
} |
|
}; |
|
|
|
ChainNodePrototype.chainDidChange = function(chain, path, depth, events) { |
|
if (this._key) { path = this._key + '.' + path; } |
|
if (this._parent) { |
|
this._parent.chainDidChange(this, path, depth+1, events); |
|
} else { |
|
if (depth > 1) { |
|
events.push(this.value(), path); |
|
} |
|
path = 'this.' + path; |
|
if (this._paths[path] > 0) { |
|
events.push(this.value(), path); |
|
} |
|
} |
|
}; |
|
|
|
ChainNodePrototype.didChange = function(events) { |
|
// invalidate my own value first. |
|
if (this._watching) { |
|
var obj = this._parent.value(); |
|
if (obj !== this._object) { |
|
removeChainWatcher(this._object, this._key, this); |
|
this._object = obj; |
|
addChainWatcher(obj, this._key, this); |
|
} |
|
this._value = undefined; |
|
|
|
// Special-case: the EachProxy relies on immediate evaluation to |
|
// establish its observers. |
|
if (this._parent && this._parent._key === '@each') |
|
this.value(); |
|
} |
|
|
|
// then notify chains... |
|
var chains = this._chains; |
|
if (chains) { |
|
for(var key in chains) { |
|
if (!chains.hasOwnProperty(key)) { continue; } |
|
chains[key].didChange(events); |
|
} |
|
} |
|
|
|
// if no events are passed in then we only care about the above wiring update |
|
if (events === null) { return; } |
|
|
|
// and finally tell parent about my path changing... |
|
if (this._parent) { this._parent.chainDidChange(this, this._key, 1, events); } |
|
}; |
|
|
|
Ember.finishChains = function(obj) { |
|
// We only create meta if we really have to |
|
var m = obj[META_KEY], chains = m && m.chains; |
|
if (chains) { |
|
if (chains.value() !== obj) { |
|
metaFor(obj).chains = chains = chains.copy(obj); |
|
} else { |
|
chains.didChange(null); |
|
} |
|
} |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
var forEach = Ember.EnumerableUtils.forEach, |
|
BRACE_EXPANSION = /^((?:[^\.]*\.)*)\{(.*)\}$/; |
|
|
|
/** |
|
Expands `pattern`, invoking `callback` for each expansion. |
|
|
|
The only pattern supported is brace-expansion, anything else will be passed |
|
once to `callback` directly. Brace expansion can only appear at the end of a |
|
pattern, for example as the last item in a chain. |
|
|
|
Example |
|
```js |
|
function echo(arg){ console.log(arg); } |
|
|
|
Ember.expandProperties('foo.bar', echo); //=> 'foo.bar' |
|
Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar' |
|
Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz' |
|
Ember.expandProperties('{foo,bar}.baz', echo); //=> '{foo,bar}.baz' |
|
``` |
|
|
|
@method |
|
@private |
|
@param {string} pattern The property pattern to expand. |
|
@param {function} callback The callback to invoke. It is invoked once per |
|
expansion, and is passed the expansion. |
|
*/ |
|
Ember.expandProperties = function (pattern, callback) { |
|
var match, prefix, list; |
|
|
|
if (match = BRACE_EXPANSION.exec(pattern)) { |
|
prefix = match[1]; |
|
list = match[2]; |
|
|
|
forEach(list.split(','), function (suffix) { |
|
callback(prefix + suffix); |
|
}); |
|
} else { |
|
callback(pattern); |
|
} |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var metaFor = Ember.meta, // utils.js |
|
typeOf = Ember.typeOf, // utils.js |
|
ChainNode = Ember._ChainNode; // chains.js |
|
|
|
// get the chains for the current object. If the current object has |
|
// chains inherited from the proto they will be cloned and reconfigured for |
|
// the current object. |
|
function chainsFor(obj, meta) { |
|
var m = meta || metaFor(obj), ret = m.chains; |
|
if (!ret) { |
|
ret = m.chains = new ChainNode(null, null, obj); |
|
} else if (ret.value() !== obj) { |
|
ret = m.chains = ret.copy(obj); |
|
} |
|
return ret; |
|
} |
|
|
|
Ember.watchPath = function(obj, keyPath, meta) { |
|
// can't watch length on Array - it is special... |
|
if (keyPath === 'length' && typeOf(obj) === 'array') { return; } |
|
|
|
var m = meta || metaFor(obj), watching = m.watching; |
|
|
|
if (!watching[keyPath]) { // activate watching first time |
|
watching[keyPath] = 1; |
|
chainsFor(obj, m).add(keyPath); |
|
} else { |
|
watching[keyPath] = (watching[keyPath] || 0) + 1; |
|
} |
|
}; |
|
|
|
Ember.unwatchPath = function(obj, keyPath, meta) { |
|
var m = meta || metaFor(obj), watching = m.watching; |
|
|
|
if (watching[keyPath] === 1) { |
|
watching[keyPath] = 0; |
|
chainsFor(obj, m).remove(keyPath); |
|
} else if (watching[keyPath] > 1) { |
|
watching[keyPath]--; |
|
} |
|
}; |
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
var metaFor = Ember.meta, // utils.js |
|
GUID_KEY = Ember.GUID_KEY, // utils.js |
|
META_KEY = Ember.META_KEY, // utils.js |
|
removeChainWatcher = Ember.removeChainWatcher, |
|
watchKey = Ember.watchKey, // watch_key.js |
|
unwatchKey = Ember.unwatchKey, |
|
watchPath = Ember.watchPath, // watch_path.js |
|
unwatchPath = Ember.unwatchPath, |
|
typeOf = Ember.typeOf, // utils.js |
|
generateGuid = Ember.generateGuid, |
|
IS_PATH = /[\.\*]/; |
|
|
|
// returns true if the passed path is just a keyName |
|
function isKeyName(path) { |
|
return path==='*' || !IS_PATH.test(path); |
|
} |
|
|
|
/** |
|
Starts watching a property on an object. Whenever the property changes, |
|
invokes `Ember.propertyWillChange` and `Ember.propertyDidChange`. This is the |
|
primitive used by observers and dependent keys; usually you will never call |
|
this method directly but instead use higher level methods like |
|
`Ember.addObserver()` |
|
|
|
@private |
|
@method watch |
|
@for Ember |
|
@param obj |
|
@param {String} keyName |
|
*/ |
|
Ember.watch = function(obj, _keyPath, m) { |
|
// can't watch length on Array - it is special... |
|
if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } |
|
|
|
if (isKeyName(_keyPath)) { |
|
watchKey(obj, _keyPath, m); |
|
} else { |
|
watchPath(obj, _keyPath, m); |
|
} |
|
}; |
|
|
|
Ember.isWatching = function isWatching(obj, key) { |
|
var meta = obj[META_KEY]; |
|
return (meta && meta.watching[key]) > 0; |
|
}; |
|
|
|
Ember.watch.flushPending = Ember.flushPendingChains; |
|
|
|
Ember.unwatch = function(obj, _keyPath, m) { |
|
// can't watch length on Array - it is special... |
|
if (_keyPath === 'length' && typeOf(obj) === 'array') { return; } |
|
|
|
if (isKeyName(_keyPath)) { |
|
unwatchKey(obj, _keyPath, m); |
|
} else { |
|
unwatchPath(obj, _keyPath, m); |
|
} |
|
}; |
|
|
|
/** |
|
Call on an object when you first beget it from another object. This will |
|
setup any chained watchers on the object instance as needed. This method is |
|
safe to call multiple times. |
|
|
|
@private |
|
@method rewatch |
|
@for Ember |
|
@param obj |
|
*/ |
|
Ember.rewatch = function(obj) { |
|
var m = obj[META_KEY], chains = m && m.chains; |
|
|
|
// make sure the object has its own guid. |
|
if (GUID_KEY in obj && !obj.hasOwnProperty(GUID_KEY)) { |
|
generateGuid(obj); |
|
} |
|
|
|
// make sure any chained watchers update. |
|
if (chains && chains.value() !== obj) { |
|
m.chains = chains.copy(obj); |
|
} |
|
}; |
|
|
|
var NODE_STACK = []; |
|
|
|
/** |
|
Tears down the meta on an object so that it can be garbage collected. |
|
Multiple calls will have no effect. |
|
|
|
@method destroy |
|
@for Ember |
|
@param {Object} obj the object to destroy |
|
@return {void} |
|
*/ |
|
Ember.destroy = function (obj) { |
|
var meta = obj[META_KEY], node, nodes, key, nodeObject; |
|
if (meta) { |
|
obj[META_KEY] = null; |
|
// remove chainWatchers to remove circular references that would prevent GC |
|
node = meta.chains; |
|
if (node) { |
|
NODE_STACK.push(node); |
|
// process tree |
|
while (NODE_STACK.length > 0) { |
|
node = NODE_STACK.pop(); |
|
// push children |
|
nodes = node._chains; |
|
if (nodes) { |
|
for (key in nodes) { |
|
if (nodes.hasOwnProperty(key)) { |
|
NODE_STACK.push(nodes[key]); |
|
} |
|
} |
|
} |
|
// remove chainWatcher in node object |
|
if (node._watching) { |
|
nodeObject = node._object; |
|
if (nodeObject) { |
|
removeChainWatcher(nodeObject, node._key, node); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
Ember.warn("The CP_DEFAULT_CACHEABLE flag has been removed and computed properties are always cached by default. Use `volatile` if you don't want caching.", Ember.ENV.CP_DEFAULT_CACHEABLE !== false); |
|
|
|
|
|
var get = Ember.get, |
|
set = Ember.set, |
|
metaFor = Ember.meta, |
|
a_slice = [].slice, |
|
o_create = Ember.create, |
|
META_KEY = Ember.META_KEY, |
|
watch = Ember.watch, |
|
unwatch = Ember.unwatch; |
|
|
|
var expandProperties = Ember.expandProperties; |
|
|
|
|
|
// .......................................................... |
|
// DEPENDENT KEYS |
|
// |
|
|
|
// data structure: |
|
// meta.deps = { |
|
// 'depKey': { |
|
// 'keyName': count, |
|
// } |
|
// } |
|
|
|
/* |
|
This function returns a map of unique dependencies for a |
|
given object and key. |
|
*/ |
|
function keysForDep(depsMeta, depKey) { |
|
var keys = depsMeta[depKey]; |
|
if (!keys) { |
|
// if there are no dependencies yet for a the given key |
|
// create a new empty list of dependencies for the key |
|
keys = depsMeta[depKey] = {}; |
|
} else if (!depsMeta.hasOwnProperty(depKey)) { |
|
// otherwise if the dependency list is inherited from |
|
// a superclass, clone the hash |
|
keys = depsMeta[depKey] = o_create(keys); |
|
} |
|
return keys; |
|
} |
|
|
|
function metaForDeps(meta) { |
|
return keysForDep(meta, 'deps'); |
|
} |
|
|
|
function addDependentKeys(desc, obj, keyName, meta) { |
|
// the descriptor has a list of dependent keys, so |
|
// add all of its dependent keys. |
|
var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; |
|
if (!depKeys) return; |
|
|
|
depsMeta = metaForDeps(meta); |
|
|
|
for(idx = 0, len = depKeys.length; idx < len; idx++) { |
|
depKey = depKeys[idx]; |
|
// Lookup keys meta for depKey |
|
keys = keysForDep(depsMeta, depKey); |
|
// Increment the number of times depKey depends on keyName. |
|
keys[keyName] = (keys[keyName] || 0) + 1; |
|
// Watch the depKey |
|
watch(obj, depKey, meta); |
|
} |
|
} |
|
|
|
function removeDependentKeys(desc, obj, keyName, meta) { |
|
// the descriptor has a list of dependent keys, so |
|
// add all of its dependent keys. |
|
var depKeys = desc._dependentKeys, depsMeta, idx, len, depKey, keys; |
|
if (!depKeys) return; |
|
|
|
depsMeta = metaForDeps(meta); |
|
|
|
for(idx = 0, len = depKeys.length; idx < len; idx++) { |
|
depKey = depKeys[idx]; |
|
// Lookup keys meta for depKey |
|
keys = keysForDep(depsMeta, depKey); |
|
// Increment the number of times depKey depends on keyName. |
|
keys[keyName] = (keys[keyName] || 0) - 1; |
|
// Watch the depKey |
|
unwatch(obj, depKey, meta); |
|
} |
|
} |
|
|
|
// .......................................................... |
|
// COMPUTED PROPERTY |
|
// |
|
|
|
/** |
|
A computed property transforms an objects function into a property. |
|
|
|
By default the function backing the computed property will only be called |
|
once and the result will be cached. You can specify various properties |
|
that your computed property is dependent on. This will force the cached |
|
result to be recomputed if the dependencies are modified. |
|
|
|
In the following example we declare a computed property (by calling |
|
`.property()` on the fullName function) and setup the properties |
|
dependencies (depending on firstName and lastName). The fullName function |
|
will be called once (regardless of how many times it is accessed) as long |
|
as it's dependencies have not been changed. Once firstName or lastName are updated |
|
any future calls (or anything bound) to fullName will incorporate the new |
|
values. |
|
|
|
```javascript |
|
Person = Ember.Object.extend({ |
|
// these will be supplied by `create` |
|
firstName: null, |
|
lastName: null, |
|
|
|
fullName: function() { |
|
var firstName = this.get('firstName'); |
|
var lastName = this.get('lastName'); |
|
|
|
return firstName + ' ' + lastName; |
|
}.property('firstName', 'lastName') |
|
}); |
|
|
|
var tom = Person.create({ |
|
firstName: "Tom", |
|
lastName: "Dale" |
|
}); |
|
|
|
tom.get('fullName') // "Tom Dale" |
|
``` |
|
|
|
You can also define what Ember should do when setting a computed property. |
|
If you try to set a computed property, it will be invoked with the key and |
|
value you want to set it to. You can also accept the previous value as the |
|
third parameter. |
|
|
|
```javascript |
|
|
|
Person = Ember.Object.extend({ |
|
// these will be supplied by `create` |
|
firstName: null, |
|
lastName: null, |
|
|
|
fullName: function(key, value, oldValue) { |
|
// getter |
|
if (arguments.length === 1) { |
|
var firstName = this.get('firstName'); |
|
var lastName = this.get('lastName'); |
|
|
|
return firstName + ' ' + lastName; |
|
|
|
// setter |
|
} else { |
|
var name = value.split(" "); |
|
|
|
this.set('firstName', name[0]); |
|
this.set('lastName', name[1]); |
|
|
|
return value; |
|
} |
|
}.property('firstName', 'lastName') |
|
}); |
|
|
|
var person = Person.create(); |
|
person.set('fullName', "Peter Wagenet"); |
|
person.get('firstName') // Peter |
|
person.get('lastName') // Wagenet |
|
``` |
|
|
|
@class ComputedProperty |
|
@namespace Ember |
|
@extends Ember.Descriptor |
|
@constructor |
|
*/ |
|
function ComputedProperty(func, opts) { |
|
this.func = func; |
|
if (Ember.FEATURES.isEnabled('composable-computed-properties')) { |
|
setDependentKeys(this, opts && opts.dependentKeys); |
|
} else { |
|
this._dependentKeys = opts && opts.dependentKeys; |
|
} |
|
|
|
this._cacheable = (opts && opts.cacheable !== undefined) ? opts.cacheable : true; |
|
this._readOnly = opts && (opts.readOnly !== undefined || !!opts.readOnly); |
|
} |
|
|
|
Ember.ComputedProperty = ComputedProperty; |
|
|
|
ComputedProperty.prototype = new Ember.Descriptor(); |
|
|
|
var ComputedPropertyPrototype = ComputedProperty.prototype; |
|
ComputedPropertyPrototype._dependentKeys = undefined; |
|
ComputedPropertyPrototype._suspended = undefined; |
|
ComputedPropertyPrototype._meta = undefined; |
|
|
|
if (Ember.FEATURES.isEnabled('composable-computed-properties')) { |
|
ComputedPropertyPrototype._dependentCPs = undefined; |
|
ComputedPropertyPrototype.implicitCPKey = undefined; |
|
|
|
ComputedPropertyPrototype.toString = function() { |
|
if (this.implicitCPKey) { |
|
return this.implicitCPKey; |
|
} |
|
return Ember.Descriptor.prototype.toString.apply(this, arguments); |
|
}; |
|
} |
|
|
|
/** |
|
Properties are cacheable by default. Computed property will automatically |
|
cache the return value of your function until one of the dependent keys changes. |
|
|
|
Call `volatile()` to set it into non-cached mode. When in this mode |
|
the computed property will not automatically cache the return value. |
|
|
|
However, if a property is properly observable, there is no reason to disable |
|
caching. |
|
|
|
@method cacheable |
|
@param {Boolean} aFlag optional set to `false` to disable caching |
|
@return {Ember.ComputedProperty} this |
|
@chainable |
|
*/ |
|
ComputedPropertyPrototype.cacheable = function(aFlag) { |
|
this._cacheable = aFlag !== false; |
|
return this; |
|
}; |
|
|
|
/** |
|
Call on a computed property to set it into non-cached mode. When in this |
|
mode the computed property will not automatically cache the return value. |
|
|
|
```javascript |
|
MyApp.outsideService = Ember.Object.extend({ |
|
value: function() { |
|
return OutsideService.getValue(); |
|
}.property().volatile() |
|
}).create(); |
|
``` |
|
|
|
@method volatile |
|
@return {Ember.ComputedProperty} this |
|
@chainable |
|
*/ |
|
ComputedPropertyPrototype.volatile = function() { |
|
return this.cacheable(false); |
|
}; |
|
|
|
/** |
|
Call on a computed property to set it into read-only mode. When in this |
|
mode the computed property will throw an error when set. |
|
|
|
```javascript |
|
MyApp.Person = Ember.Object.extend({ |
|
guid: function() { |
|
return 'guid-guid-guid'; |
|
}.property().readOnly() |
|
}); |
|
|
|
MyApp.person = MyApp.Person.create(); |
|
|
|
MyApp.person.set('guid', 'new-guid'); // will throw an exception |
|
``` |
|
|
|
@method readOnly |
|
@return {Ember.ComputedProperty} this |
|
@chainable |
|
*/ |
|
ComputedPropertyPrototype.readOnly = function(readOnly) { |
|
this._readOnly = readOnly === undefined || !!readOnly; |
|
return this; |
|
}; |
|
|
|
/** |
|
Sets the dependent keys on this computed property. Pass any number of |
|
arguments containing key paths that this computed property depends on. |
|
|
|
```javascript |
|
MyApp.President = Ember.Object.extend({ |
|
fullName: Ember.computed(function() { |
|
return this.get('firstName') + ' ' + this.get('lastName'); |
|
|
|
// Tell Ember that this computed property depends on firstName |
|
// and lastName |
|
}).property('firstName', 'lastName') |
|
}); |
|
|
|
MyApp.president = MyApp.President.create({ |
|
firstName: 'Barack', |
|
lastName: 'Obama', |
|
}); |
|
MyApp.president.get('fullName'); // Barack Obama |
|
``` |
|
|
|
@method property |
|
@param {String} path* zero or more property paths |
|
@return {Ember.ComputedProperty} this |
|
@chainable |
|
*/ |
|
ComputedPropertyPrototype.property = function() { |
|
var args; |
|
|
|
var addArg = function (property) { |
|
args.push(property); |
|
}; |
|
|
|
args = []; |
|
for (var i = 0, l = arguments.length; i < l; i++) { |
|
expandProperties(arguments[i], addArg); |
|
} |
|
|
|
if (Ember.FEATURES.isEnabled('composable-computed-properties')) { |
|
setDependentKeys(this, args); |
|
} else { |
|
this._dependentKeys = args; |
|
} |
|
|
|
return this; |
|
}; |
|
|
|
/** |
|
In some cases, you may want to annotate computed properties with additional |
|
metadata about how they function or what values they operate on. For example, |
|
computed property functions may close over variables that are then no longer |
|
available for introspection. |
|
|
|
You can pass a hash of these values to a computed property like this: |
|
|
|
``` |
|
person: function() { |
|
var personId = this.get('personId'); |
|
return App.Person.create({ id: personId }); |
|
}.property().meta({ type: App.Person }) |
|
``` |
|
|
|
The hash that you pass to the `meta()` function will be saved on the |
|
computed property descriptor under the `_meta` key. Ember runtime |
|
exposes a public API for retrieving these values from classes, |
|
via the `metaForProperty()` function. |
|
|
|
@method meta |
|
@param {Hash} meta |
|
@chainable |
|
*/ |
|
|
|
ComputedPropertyPrototype.meta = function(meta) { |
|
if (arguments.length === 0) { |
|
return this._meta || {}; |
|
} else { |
|
this._meta = meta; |
|
return this; |
|
} |
|
}; |
|
|
|
/* impl descriptor API */ |
|
ComputedPropertyPrototype.didChange = function(obj, keyName) { |
|
// _suspended is set via a CP.set to ensure we don't clear |
|
// the cached value set by the setter |
|
if (this._cacheable && this._suspended !== obj) { |
|
var meta = metaFor(obj); |
|
if (keyName in meta.cache) { |
|
delete meta.cache[keyName]; |
|
removeDependentKeys(this, obj, keyName, meta); |
|
} |
|
} |
|
}; |
|
|
|
function finishChains(chainNodes) |
|
{ |
|
for (var i=0, l=chainNodes.length; i<l; i++) { |
|
chainNodes[i].didChange(null); |
|
} |
|
} |
|
|
|
/** |
|
Access the value of the function backing the computed property. |
|
If this property has already been cached, return the cached result. |
|
Otherwise, call the function passing the property name as an argument. |
|
|
|
```javascript |
|
Person = Ember.Object.extend({ |
|
fullName: function(keyName) { |
|
// the keyName parameter is 'fullName' in this case. |
|
|
|
return this.get('firstName') + ' ' + this.get('lastName'); |
|
}.property('firstName', 'lastName') |
|
}); |
|
|
|
|
|
var tom = Person.create({ |
|
firstName: "Tom", |
|
lastName: "Dale" |
|
}); |
|
|
|
tom.get('fullName') // "Tom Dale" |
|
``` |
|
|
|
@method get |
|
@param {String} keyName The key being accessed. |
|
@return {Object} The return value of the function backing the CP. |
|
*/ |
|
ComputedPropertyPrototype.get = function(obj, keyName) { |
|
var ret, cache, meta, chainNodes; |
|
if (this._cacheable) { |
|
meta = metaFor(obj); |
|
cache = meta.cache; |
|
if (keyName in cache) { return cache[keyName]; } |
|
ret = cache[keyName] = this.func.call(obj, keyName); |
|
chainNodes = meta.chainWatchers && meta.chainWatchers[keyName]; |
|
if (chainNodes) { finishChains(chainNodes); } |
|
addDependentKeys(this, obj, keyName, meta); |
|
} else { |
|
ret = this.func.call(obj, keyName); |
|
} |
|
return ret; |
|
}; |
|
|
|
/** |
|
Set the value of a computed property. If the function that backs your |
|
computed property does not accept arguments then the default action for |
|
setting would be to define the property on the current object, and set |
|
the value of the property to the value being set. |
|
|
|
Generally speaking if you intend for your computed property to be set |
|
your backing function should accept either two or three arguments. |
|
|
|
@method set |
|
@param {String} keyName The key being accessed. |
|
@param {Object} newValue The new value being assigned. |
|
@param {String} oldValue The old value being replaced. |
|
@return {Object} The return value of the function backing the CP. |
|
*/ |
|
ComputedPropertyPrototype.set = function(obj, keyName, value) { |
|
var cacheable = this._cacheable, |
|
func = this.func, |
|
meta = metaFor(obj, cacheable), |
|
watched = meta.watching[keyName], |
|
oldSuspended = this._suspended, |
|
hadCachedValue = false, |
|
cache = meta.cache, |
|
funcArgLength, cachedValue, ret; |
|
|
|
if (this._readOnly) { |
|
throw new Ember.Error('Cannot set read-only property "' + keyName + '" on object: ' + Ember.inspect(obj)); |
|
} |
|
|
|
this._suspended = obj; |
|
|
|
try { |
|
|
|
if (cacheable && cache.hasOwnProperty(keyName)) { |
|
cachedValue = cache[keyName]; |
|
hadCachedValue = true; |
|
} |
|
|
|
// Check if the CP has been wrapped. If if has, use the |
|
// length from the wrapped function. |
|
funcArgLength = (func.wrappedFunction ? func.wrappedFunction.length : func.length); |
|
|
|
// For backwards-compatibility with computed properties |
|
// that check for arguments.length === 2 to determine if |
|
// they are being get or set, only pass the old cached |
|
// value if the computed property opts into a third |
|
// argument. |
|
if (funcArgLength === 3) { |
|
ret = func.call(obj, keyName, value, cachedValue); |
|
} else if (funcArgLength === 2) { |
|
ret = func.call(obj, keyName, value); |
|
} else { |
|
Ember.defineProperty(obj, keyName, null, cachedValue); |
|
Ember.set(obj, keyName, value); |
|
return; |
|
} |
|
|
|
if (hadCachedValue && cachedValue === ret) { return; } |
|
|
|
if (watched) { Ember.propertyWillChange(obj, keyName); } |
|
|
|
if (hadCachedValue) { |
|
delete cache[keyName]; |
|
} |
|
|
|
if (cacheable) { |
|
if (!hadCachedValue) { |
|
addDependentKeys(this, obj, keyName, meta); |
|
} |
|
cache[keyName] = ret; |
|
} |
|
|
|
if (watched) { Ember.propertyDidChange(obj, keyName); } |
|
} finally { |
|
this._suspended = oldSuspended; |
|
} |
|
return ret; |
|
}; |
|
|
|
/* called before property is overridden */ |
|
ComputedPropertyPrototype.teardown = function(obj, keyName) { |
|
var meta = metaFor(obj); |
|
|
|
if (keyName in meta.cache) { |
|
removeDependentKeys(this, obj, keyName, meta); |
|
} |
|
|
|
if (this._cacheable) { delete meta.cache[keyName]; } |
|
|
|
return null; // no value to restore |
|
}; |
|
|
|
|
|
/** |
|
This helper returns a new property descriptor that wraps the passed |
|
computed property function. You can use this helper to define properties |
|
with mixins or via `Ember.defineProperty()`. |
|
|
|
The function you pass will be used to both get and set property values. |
|
The function should accept two parameters, key and value. If value is not |
|
undefined you should set the value first. In either case return the |
|
current value of the property. |
|
@method computed |
|
@for Ember |
|
@param {Function} func The computed property function. |
|
@return {Ember.ComputedProperty} property descriptor instance |
|
*/ |
|
Ember.computed = function(func) { |
|
var args; |
|
|
|
if (arguments.length > 1) { |
|
args = a_slice.call(arguments, 0, -1); |
|
func = a_slice.call(arguments, -1)[0]; |
|
} |
|
|
|
if (typeof func !== "function") { |
|
throw new Ember.Error("Computed Property declared without a property function"); |
|
} |
|
|
|
var cp = new ComputedProperty(func); |
|
|
|
if (args) { |
|
cp.property.apply(cp, args); |
|
} |
|
|
|
return cp; |
|
}; |
|
|
|
/** |
|
Returns the cached value for a property, if one exists. |
|
This can be useful for peeking at the value of a computed |
|
property that is generated lazily, without accidentally causing |
|
it to be created. |
|
|
|
@method cacheFor |
|
@for Ember |
|
@param {Object} obj the object whose property you want to check |
|
@param {String} key the name of the property whose cached value you want |
|
to return |
|
@return {Object} the cached value |
|
*/ |
|
Ember.cacheFor = function cacheFor(obj, key) { |
|
var meta = obj[META_KEY], |
|
cache = meta && meta.cache; |
|
|
|
if (cache && key in cache) { |
|
return cache[key]; |
|
} |
|
}; |
|
|
|
function getProperties(self, propertyNames) { |
|
var ret = {}; |
|
for(var i = 0; i < propertyNames.length; i++) { |
|
ret[propertyNames[i]] = get(self, propertyNames[i]); |
|
} |
|
return ret; |
|
} |
|
|
|
var registerComputed, registerComputedWithProperties; |
|
|
|
if (Ember.FEATURES.isEnabled('composable-computed-properties')) { |
|
var guidFor = Ember.guidFor, |
|
map = Ember.EnumerableUtils.map, |
|
filter = Ember.EnumerableUtils.filter, |
|
typeOf = Ember.typeOf; |
|
|
|
var implicitKey = function (cp) { |
|
return [guidFor(cp)].concat(cp._dependentKeys).join('_').replace(/\./g, '_DOT_'); |
|
}; |
|
|
|
var normalizeDependentKey = function (key) { |
|
if (key instanceof Ember.ComputedProperty) { |
|
return implicitKey(key); |
|
} else { |
|
return key; |
|
} |
|
}; |
|
|
|
var normalizeDependentKeys = function (keys) { |
|
return map(keys, function (key) { |
|
return normalizeDependentKey(key); |
|
}); |
|
}; |
|
|
|
var selectDependentCPs = function (keys) { |
|
return filter(keys, function (key) { |
|
return key instanceof Ember.ComputedProperty; |
|
}); |
|
}; |
|
|
|
var setDependentKeys = function(cp, dependentKeys) { |
|
if (dependentKeys) { |
|
cp._dependentKeys = normalizeDependentKeys(dependentKeys); |
|
cp._dependentCPs = selectDependentCPs(dependentKeys); |
|
} else { |
|
cp._dependentKeys = cp._dependentCPs = []; |
|
} |
|
cp.implicitCPKey = implicitKey(cp); |
|
}; |
|
// expose `normalizeDependentKey[s]` so user CP macros can easily support |
|
// composition |
|
Ember.computed.normalizeDependentKey = normalizeDependentKey; |
|
Ember.computed.normalizeDependentKeys = normalizeDependentKeys; |
|
|
|
registerComputed = function (name, macro) { |
|
Ember.computed[name] = function(dependentKey) { |
|
var args = normalizeDependentKeys(a_slice.call(arguments)); |
|
return Ember.computed(dependentKey, function() { |
|
return macro.apply(this, args); |
|
}); |
|
}; |
|
}; |
|
} |
|
|
|
if (Ember.FEATURES.isEnabled('composable-computed-properties')) { |
|
registerComputedWithProperties = function(name, macro) { |
|
Ember.computed[name] = function() { |
|
var args = a_slice.call(arguments); |
|
var properties = normalizeDependentKeys(args); |
|
|
|
var computed = Ember.computed(function() { |
|
return macro.apply(this, [getProperties(this, properties)]); |
|
}); |
|
|
|
return computed.property.apply(computed, args); |
|
}; |
|
}; |
|
} else { |
|
registerComputed = function (name, macro) { |
|
Ember.computed[name] = function(dependentKey) { |
|
var args = a_slice.call(arguments); |
|
return Ember.computed(dependentKey, function() { |
|
return macro.apply(this, args); |
|
}); |
|
}; |
|
}; |
|
|
|
registerComputedWithProperties = function(name, macro) { |
|
Ember.computed[name] = function() { |
|
var properties = a_slice.call(arguments); |
|
|
|
var computed = Ember.computed(function() { |
|
return macro.apply(this, [getProperties(this, properties)]); |
|
}); |
|
|
|
return computed.property.apply(computed, properties); |
|
}; |
|
}; |
|
} |
|
|
|
|
|
if (Ember.FEATURES.isEnabled('composable-computed-properties')) { |
|
Ember.computed.literal = function (value) { |
|
return Ember.computed(function () { |
|
return value; |
|
}); |
|
}; |
|
} |
|
|
|
|
|
/** |
|
A computed property that returns true if the value of the dependent |
|
property is null, an empty string, empty array, or empty function. |
|
|
|
Note: When using `Ember.computed.empty` to watch an array make sure to |
|
use the `array.[]` syntax so the computed can subscribe to transitions |
|
from empty to non-empty states. |
|
|
|
Example |
|
|
|
```javascript |
|
var ToDoList = Ember.Object.extend({ |
|
done: Ember.computed.empty('todos.[]') // detect array changes |
|
}); |
|
var todoList = ToDoList.create({todos: ['Unit Test', 'Documentation', 'Release']}); |
|
todoList.get('done'); // false |
|
todoList.get('todos').clear(); // [] |
|
todoList.get('done'); // true |
|
``` |
|
|
|
@method computed.empty |
|
@for Ember |
|
@param {String} dependentKey |
|
@return {Ember.ComputedProperty} computed property which negate |
|
the original value for property |
|
*/ |
|
registerComputed('empty', function(dependentKey) { |
|
return Ember.isEmpty(get(this, dependentKey)); |
|
}); |
|
|
|
|
|
/** |
|
A computed property that returns true if the value of the dependent |
|
property is NOT null, an empty string, empty array, or empty function. |
|
|
|
Note: When using `Ember.computed.notEmpty` to watch an array make sure to |
|
use the `array.[]` syntax so the computed can subscribe to transitions |
|
from empty to non-empty states. |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
hasStuff: Ember.computed.notEmpty('backpack.[]') |
|
}); |
|
var hamster = Hamster.create({backpack: ['Food', 'Sleeping Bag', 'Tent']}); |
|
hamster.get('hasStuff'); // true |
|
hamster.get('backpack').clear(); // [] |
|
hamster.get('hasStuff'); // false |
|
``` |
|
|
|
@method computed.notEmpty |
|
@for Ember |
|
@param {String} dependentKey |
|
@return {Ember.ComputedProperty} computed property which returns true if |
|
original value for property is not empty. |
|
*/ |
|
registerComputed('notEmpty', function(dependentKey) { |
|
return !Ember.isEmpty(get(this, dependentKey)); |
|
}); |
|
|
|
/** |
|
A computed property that returns true if the value of the dependent |
|
property is null or undefined. This avoids errors from JSLint complaining |
|
about use of ==, which can be technically confusing. |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
isHungry: Ember.computed.none('food') |
|
}); |
|
var hamster = Hamster.create(); |
|
hamster.get('isHungry'); // true |
|
hamster.set('food', 'Banana'); |
|
hamster.get('isHungry'); // false |
|
hamster.set('food', null); |
|
hamster.get('isHungry'); // true |
|
``` |
|
|
|
@method computed.none |
|
@for Ember |
|
@param {String} dependentKey |
|
@return {Ember.ComputedProperty} computed property which |
|
returns true if original value for property is null or undefined. |
|
*/ |
|
registerComputed('none', function(dependentKey) { |
|
return Ember.isNone(get(this, dependentKey)); |
|
}); |
|
|
|
/** |
|
A computed property that returns the inverse boolean value |
|
of the original value for the dependent property. |
|
|
|
Example |
|
|
|
```javascript |
|
var User = Ember.Object.extend({ |
|
isAnonymous: Ember.computed.not('loggedIn') |
|
}); |
|
var user = User.create({loggedIn: false}); |
|
user.get('isAnonymous'); // true |
|
user.set('loggedIn', true); |
|
user.get('isAnonymous'); // false |
|
``` |
|
|
|
@method computed.not |
|
@for Ember |
|
@param {String} dependentKey |
|
@return {Ember.ComputedProperty} computed property which returns |
|
inverse of the original value for property |
|
*/ |
|
registerComputed('not', function(dependentKey) { |
|
return !get(this, dependentKey); |
|
}); |
|
|
|
/** |
|
A computed property that converts the provided dependent property |
|
into a boolean value. |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
hasBananas: Ember.computed.bool('numBananas') |
|
}); |
|
var hamster = Hamster.create(); |
|
hamster.get('hasBananas'); // false |
|
hamster.set('numBananas', 0); |
|
hamster.get('hasBananas'); // false |
|
hamster.set('numBananas', 1); |
|
hamster.get('hasBananas'); // true |
|
hamster.set('numBananas', null); |
|
hamster.get('hasBananas'); // false |
|
``` |
|
|
|
@method computed.bool |
|
@for Ember |
|
@param {String} dependentKey |
|
@return {Ember.ComputedProperty} computed property which converts |
|
to boolean the original value for property |
|
*/ |
|
registerComputed('bool', function(dependentKey) { |
|
return !!get(this, dependentKey); |
|
}); |
|
|
|
/** |
|
A computed property which matches the original value for the |
|
dependent property against a given RegExp, returning `true` |
|
if they values matches the RegExp and `false` if it does not. |
|
|
|
Example |
|
|
|
```javascript |
|
var User = Ember.Object.extend({ |
|
hasValidEmail: Ember.computed.match('email', /^.+@.+\..+$/) |
|
}); |
|
var user = User.create({loggedIn: false}); |
|
user.get('hasValidEmail'); // false |
|
user.set('email', ''); |
|
user.get('hasValidEmail'); // false |
|
user.set('email', 'ember_hamster@example.com'); |
|
user.get('hasValidEmail'); // true |
|
``` |
|
|
|
@method computed.match |
|
@for Ember |
|
@param {String} dependentKey |
|
@param {RegExp} regexp |
|
@return {Ember.ComputedProperty} computed property which match |
|
the original value for property against a given RegExp |
|
*/ |
|
registerComputed('match', function(dependentKey, regexp) { |
|
var value = get(this, dependentKey); |
|
return typeof value === 'string' ? regexp.test(value) : false; |
|
}); |
|
|
|
/** |
|
A computed property that returns true if the provided dependent property |
|
is equal to the given value. |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
napTime: Ember.computed.equal('state', 'sleepy') |
|
}); |
|
var hamster = Hamster.create(); |
|
hamster.get('napTime'); // false |
|
hamster.set('state', 'sleepy'); |
|
hamster.get('napTime'); // true |
|
hamster.set('state', 'hungry'); |
|
hamster.get('napTime'); // false |
|
``` |
|
|
|
@method computed.equal |
|
@for Ember |
|
@param {String} dependentKey |
|
@param {String|Number|Object} value |
|
@return {Ember.ComputedProperty} computed property which returns true if |
|
the original value for property is equal to the given value. |
|
*/ |
|
registerComputed('equal', function(dependentKey, value) { |
|
return get(this, dependentKey) === value; |
|
}); |
|
|
|
/** |
|
A computed property that returns true if the provied dependent property |
|
is greater than the provided value. |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
hasTooManyBananas: Ember.computed.gt('numBananas', 10) |
|
}); |
|
var hamster = Hamster.create(); |
|
hamster.get('hasTooManyBananas'); // false |
|
hamster.set('numBananas', 3); |
|
hamster.get('hasTooManyBananas'); // false |
|
hamster.set('numBananas', 11); |
|
hamster.get('hasTooManyBananas'); // true |
|
``` |
|
|
|
@method computed.gt |
|
@for Ember |
|
@param {String} dependentKey |
|
@param {Number} value |
|
@return {Ember.ComputedProperty} computed property which returns true if |
|
the original value for property is greater then given value. |
|
*/ |
|
registerComputed('gt', function(dependentKey, value) { |
|
return get(this, dependentKey) > value; |
|
}); |
|
|
|
/** |
|
A computed property that returns true if the provided dependent property |
|
is greater than or equal to the provided value. |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
hasTooManyBananas: Ember.computed.gte('numBananas', 10) |
|
}); |
|
var hamster = Hamster.create(); |
|
hamster.get('hasTooManyBananas'); // false |
|
hamster.set('numBananas', 3); |
|
hamster.get('hasTooManyBananas'); // false |
|
hamster.set('numBananas', 10); |
|
hamster.get('hasTooManyBananas'); // true |
|
``` |
|
|
|
@method computed.gte |
|
@for Ember |
|
@param {String} dependentKey |
|
@param {Number} value |
|
@return {Ember.ComputedProperty} computed property which returns true if |
|
the original value for property is greater or equal then given value. |
|
*/ |
|
registerComputed('gte', function(dependentKey, value) { |
|
return get(this, dependentKey) >= value; |
|
}); |
|
|
|
/** |
|
A computed property that returns true if the provided dependent property |
|
is less than the provided value. |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
needsMoreBananas: Ember.computed.lt('numBananas', 3) |
|
}); |
|
var hamster = Hamster.create(); |
|
hamster.get('needsMoreBananas'); // true |
|
hamster.set('numBananas', 3); |
|
hamster.get('needsMoreBananas'); // false |
|
hamster.set('numBananas', 2); |
|
hamster.get('needsMoreBananas'); // true |
|
``` |
|
|
|
@method computed.lt |
|
@for Ember |
|
@param {String} dependentKey |
|
@param {Number} value |
|
@return {Ember.ComputedProperty} computed property which returns true if |
|
the original value for property is less then given value. |
|
*/ |
|
registerComputed('lt', function(dependentKey, value) { |
|
return get(this, dependentKey) < value; |
|
}); |
|
|
|
/** |
|
A computed property that returns true if the provided dependent property |
|
is less than or equal to the provided value. |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
needsMoreBananas: Ember.computed.lte('numBananas', 3) |
|
}); |
|
var hamster = Hamster.create(); |
|
hamster.get('needsMoreBananas'); // true |
|
hamster.set('numBananas', 5); |
|
hamster.get('needsMoreBananas'); // false |
|
hamster.set('numBananas', 3); |
|
hamster.get('needsMoreBananas'); // true |
|
``` |
|
|
|
@method computed.lte |
|
@for Ember |
|
@param {String} dependentKey |
|
@param {Number} value |
|
@return {Ember.ComputedProperty} computed property which returns true if |
|
the original value for property is less or equal then given value. |
|
*/ |
|
registerComputed('lte', function(dependentKey, value) { |
|
return get(this, dependentKey) <= value; |
|
}); |
|
|
|
/** |
|
A computed property that performs a logical `and` on the |
|
original values for the provided dependent properties. |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
readyForCamp: Ember.computed.and('hasTent', 'hasBackpack') |
|
}); |
|
var hamster = Hamster.create(); |
|
hamster.get('readyForCamp'); // false |
|
hamster.set('hasTent', true); |
|
hamster.get('readyForCamp'); // false |
|
hamster.set('hasBackpack', true); |
|
hamster.get('readyForCamp'); // true |
|
``` |
|
|
|
@method computed.and |
|
@for Ember |
|
@param {String} dependentKey* |
|
@return {Ember.ComputedProperty} computed property which performs |
|
a logical `and` on the values of all the original values for properties. |
|
*/ |
|
registerComputedWithProperties('and', function(properties) { |
|
for (var key in properties) { |
|
if (properties.hasOwnProperty(key) && !properties[key]) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
}); |
|
|
|
/** |
|
A computed property which performs a logical `or` on the |
|
original values for the provided dependent properties. |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
readyForRain: Ember.computed.or('hasJacket', 'hasUmbrella') |
|
}); |
|
var hamster = Hamster.create(); |
|
hamster.get('readyForRain'); // false |
|
hamster.set('hasJacket', true); |
|
hamster.get('readyForRain'); // true |
|
``` |
|
|
|
@method computed.or |
|
@for Ember |
|
@param {String} dependentKey* |
|
@return {Ember.ComputedProperty} computed property which performs |
|
a logical `or` on the values of all the original values for properties. |
|
*/ |
|
registerComputedWithProperties('or', function(properties) { |
|
for (var key in properties) { |
|
if (properties.hasOwnProperty(key) && properties[key]) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
}); |
|
|
|
/** |
|
A computed property that returns the first truthy value |
|
from a list of dependent properties. |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
hasClothes: Ember.computed.any('hat', 'shirt') |
|
}); |
|
var hamster = Hamster.create(); |
|
hamster.get('hasClothes'); // null |
|
hamster.set('shirt', 'Hawaiian Shirt'); |
|
hamster.get('hasClothes'); // 'Hawaiian Shirt' |
|
``` |
|
|
|
@method computed.any |
|
@for Ember |
|
@param {String} dependentKey* |
|
@return {Ember.ComputedProperty} computed property which returns |
|
the first truthy value of given list of properties. |
|
*/ |
|
registerComputedWithProperties('any', function(properties) { |
|
for (var key in properties) { |
|
if (properties.hasOwnProperty(key) && properties[key]) { |
|
return properties[key]; |
|
} |
|
} |
|
return null; |
|
}); |
|
|
|
/** |
|
A computed property that returns the array of values |
|
for the provided dependent properties. |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
clothes: Ember.computed.collect('hat', 'shirt') |
|
}); |
|
var hamster = Hamster.create(); |
|
hamster.get('clothes'); // [null, null] |
|
hamster.set('hat', 'Camp Hat'); |
|
hamster.set('shirt', 'Camp Shirt'); |
|
hamster.get('clothes'); // ['Camp Hat', 'Camp Shirt'] |
|
``` |
|
|
|
@method computed.collect |
|
@for Ember |
|
@param {String} dependentKey* |
|
@return {Ember.ComputedProperty} computed property which maps |
|
values of all passed properties in to an array. |
|
*/ |
|
registerComputedWithProperties('collect', function(properties) { |
|
var res = []; |
|
for (var key in properties) { |
|
if (properties.hasOwnProperty(key)) { |
|
if (Ember.isNone(properties[key])) { |
|
res.push(null); |
|
} else { |
|
res.push(properties[key]); |
|
} |
|
} |
|
} |
|
return res; |
|
}); |
|
|
|
/** |
|
Creates a new property that is an alias for another property |
|
on an object. Calls to `get` or `set` this property behave as |
|
though they were called on the original property. |
|
|
|
```javascript |
|
Person = Ember.Object.extend({ |
|
name: 'Alex Matchneer', |
|
nomen: Ember.computed.alias('name') |
|
}); |
|
|
|
alex = Person.create(); |
|
alex.get('nomen'); // 'Alex Matchneer' |
|
alex.get('name'); // 'Alex Matchneer' |
|
|
|
alex.set('nomen', '@machty'); |
|
alex.get('name'); // '@machty' |
|
``` |
|
@method computed.alias |
|
@for Ember |
|
@param {String} dependentKey |
|
@return {Ember.ComputedProperty} computed property which creates an |
|
alias to the original value for property. |
|
*/ |
|
Ember.computed.alias = function(dependentKey) { |
|
return Ember.computed(dependentKey, function(key, value) { |
|
if (arguments.length > 1) { |
|
set(this, dependentKey, value); |
|
return value; |
|
} else { |
|
return get(this, dependentKey); |
|
} |
|
}); |
|
}; |
|
|
|
/** |
|
Where `computed.alias` aliases `get` and `set`, and allows for bidirectional |
|
data flow, `computed.oneWay` only provides an aliased `get`. The `set` will |
|
not mutate the upstream property, rather causes the current property to |
|
become the value set. This causes the downstream property to permentantly |
|
diverge from the upstream property. |
|
|
|
Example |
|
|
|
```javascript |
|
User = Ember.Object.extend({ |
|
firstName: null, |
|
lastName: null, |
|
nickName: Ember.computed.oneWay('firstName') |
|
}); |
|
|
|
user = User.create({ |
|
firstName: 'Teddy', |
|
lastName: 'Zeenny' |
|
}); |
|
|
|
user.get('nickName'); |
|
# 'Teddy' |
|
|
|
user.set('nickName', 'TeddyBear'); |
|
# 'TeddyBear' |
|
|
|
user.get('firstName'); |
|
# 'Teddy' |
|
``` |
|
|
|
@method computed.oneWay |
|
@for Ember |
|
@param {String} dependentKey |
|
@return {Ember.ComputedProperty} computed property which creates a |
|
one way computed property to the original value for property. |
|
*/ |
|
Ember.computed.oneWay = function(dependentKey) { |
|
return Ember.computed(dependentKey, function() { |
|
return get(this, dependentKey); |
|
}); |
|
}; |
|
|
|
|
|
|
|
/** |
|
Where `computed.oneWay` provides oneWay bindings, `computed.readOnly` provides |
|
a readOnly one way binding. Very often when using `computed.oneWay` one does |
|
not also want changes to propogate back up, as they will replace the value. |
|
|
|
This prevents the reverse flow, and also throws an exception when it occurs. |
|
|
|
Example |
|
|
|
```javascript |
|
User = Ember.Object.extend({ |
|
firstName: null, |
|
lastName: null, |
|
nickName: Ember.computed.readOnly('firstName') |
|
}); |
|
|
|
user = User.create({ |
|
firstName: 'Teddy', |
|
lastName: 'Zeenny' |
|
}); |
|
|
|
user.get('nickName'); |
|
# 'Teddy' |
|
|
|
user.set('nickName', 'TeddyBear'); |
|
# throws Exception |
|
# throw new Ember.Error('Cannot Set: nickName on: <User:ember27288>' );` |
|
|
|
user.get('firstName'); |
|
# 'Teddy' |
|
``` |
|
|
|
@method computed.readOnly |
|
@for Ember |
|
@param {String} dependentKey |
|
@return {Ember.ComputedProperty} computed property which creates a |
|
one way computed property to the original value for property. |
|
*/ |
|
Ember.computed.readOnly = function(dependentKey) { |
|
return Ember.computed(dependentKey, function() { |
|
return get(this, dependentKey); |
|
}).readOnly(); |
|
}; |
|
|
|
/** |
|
A computed property that acts like a standard getter and setter, |
|
but returns the value at the provided `defaultPath` if the |
|
property itself has not been set to a value |
|
|
|
Example |
|
|
|
```javascript |
|
var Hamster = Ember.Object.extend({ |
|
wishList: Ember.computed.defaultTo('favoriteFood') |
|
}); |
|
var hamster = Hamster.create({favoriteFood: 'Banana'}); |
|
hamster.get('wishList'); // 'Banana' |
|
hamster.set('wishList', 'More Unit Tests'); |
|
hamster.get('wishList'); // 'More Unit Tests' |
|
hamster.get('favoriteFood'); // 'Banana' |
|
``` |
|
|
|
@method computed.defaultTo |
|
@for Ember |
|
@param {String} defaultPath |
|
@return {Ember.ComputedProperty} computed property which acts like |
|
a standard getter and setter, but defaults to the value from `defaultPath`. |
|
*/ |
|
Ember.computed.defaultTo = function(defaultPath) { |
|
return Ember.computed(function(key, newValue, cachedValue) { |
|
if (arguments.length === 1) { |
|
return cachedValue != null ? cachedValue : get(this, defaultPath); |
|
} |
|
return newValue != null ? newValue : get(this, defaultPath); |
|
}); |
|
}; |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
// Ember.tryFinally |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
var AFTER_OBSERVERS = ':change', |
|
BEFORE_OBSERVERS = ':before'; |
|
|
|
function changeEvent(keyName) { |
|
return keyName+AFTER_OBSERVERS; |
|
} |
|
|
|
function beforeEvent(keyName) { |
|
return keyName+BEFORE_OBSERVERS; |
|
} |
|
|
|
/** |
|
@method addObserver |
|
@for Ember |
|
@param obj |
|
@param {String} path |
|
@param {Object|Function} targetOrMethod |
|
@param {Function|String} [method] |
|
*/ |
|
Ember.addObserver = function(obj, _path, target, method) { |
|
Ember.addListener(obj, changeEvent(_path), target, method); |
|
Ember.watch(obj, _path); |
|
|
|
return this; |
|
}; |
|
|
|
Ember.observersFor = function(obj, path) { |
|
return Ember.listenersFor(obj, changeEvent(path)); |
|
}; |
|
|
|
/** |
|
@method removeObserver |
|
@for Ember |
|
@param obj |
|
@param {String} path |
|
@param {Object|Function} targetOrMethod |
|
@param {Function|String} [method] |
|
*/ |
|
Ember.removeObserver = function(obj, _path, target, method) { |
|
Ember.unwatch(obj, _path); |
|
Ember.removeListener(obj, changeEvent(_path), target, method); |
|
|
|
return this; |
|
}; |
|
|
|
/** |
|
@method addBeforeObserver |
|
@for Ember |
|
@param obj |
|
@param {String} path |
|
@param {Object|Function} targetOrMethod |
|
@param {Function|String} [method] |
|
*/ |
|
Ember.addBeforeObserver = function(obj, _path, target, method) { |
|
Ember.addListener(obj, beforeEvent(_path), target, method); |
|
Ember.watch(obj, _path); |
|
|
|
return this; |
|
}; |
|
|
|
// Suspend observer during callback. |
|
// |
|
// This should only be used by the target of the observer |
|
// while it is setting the observed path. |
|
Ember._suspendBeforeObserver = function(obj, path, target, method, callback) { |
|
return Ember._suspendListener(obj, beforeEvent(path), target, method, callback); |
|
}; |
|
|
|
Ember._suspendObserver = function(obj, path, target, method, callback) { |
|
return Ember._suspendListener(obj, changeEvent(path), target, method, callback); |
|
}; |
|
|
|
var map = Ember.ArrayPolyfills.map; |
|
|
|
Ember._suspendBeforeObservers = function(obj, paths, target, method, callback) { |
|
var events = map.call(paths, beforeEvent); |
|
return Ember._suspendListeners(obj, events, target, method, callback); |
|
}; |
|
|
|
Ember._suspendObservers = function(obj, paths, target, method, callback) { |
|
var events = map.call(paths, changeEvent); |
|
return Ember._suspendListeners(obj, events, target, method, callback); |
|
}; |
|
|
|
Ember.beforeObserversFor = function(obj, path) { |
|
return Ember.listenersFor(obj, beforeEvent(path)); |
|
}; |
|
|
|
/** |
|
@method removeBeforeObserver |
|
@for Ember |
|
@param obj |
|
@param {String} path |
|
@param {Object|Function} targetOrMethod |
|
@param {Function|String} [method] |
|
*/ |
|
Ember.removeBeforeObserver = function(obj, _path, target, method) { |
|
Ember.unwatch(obj, _path); |
|
Ember.removeListener(obj, beforeEvent(_path), target, method); |
|
|
|
return this; |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
define("backburner/queue", |
|
["exports"], |
|
function(__exports__) { |
|
"use strict"; |
|
function Queue(daq, name, options) { |
|
this.daq = daq; |
|
this.name = name; |
|
this.options = options; |
|
this._queue = []; |
|
} |
|
|
|
Queue.prototype = { |
|
daq: null, |
|
name: null, |
|
options: null, |
|
_queue: null, |
|
|
|
push: function(target, method, args, stack) { |
|
var queue = this._queue; |
|
queue.push(target, method, args, stack); |
|
return {queue: this, target: target, method: method}; |
|
}, |
|
|
|
pushUnique: function(target, method, args, stack) { |
|
var queue = this._queue, currentTarget, currentMethod, i, l; |
|
|
|
for (i = 0, l = queue.length; i < l; i += 4) { |
|
currentTarget = queue[i]; |
|
currentMethod = queue[i+1]; |
|
|
|
if (currentTarget === target && currentMethod === method) { |
|
queue[i+2] = args; // replace args |
|
queue[i+3] = stack; // replace stack |
|
return {queue: this, target: target, method: method}; // TODO: test this code path |
|
} |
|
} |
|
|
|
this._queue.push(target, method, args, stack); |
|
return {queue: this, target: target, method: method}; |
|
}, |
|
|
|
// TODO: remove me, only being used for Ember.run.sync |
|
flush: function() { |
|
var queue = this._queue, |
|
options = this.options, |
|
before = options && options.before, |
|
after = options && options.after, |
|
target, method, args, stack, i, l = queue.length; |
|
|
|
if (l && before) { before(); } |
|
for (i = 0; i < l; i += 4) { |
|
target = queue[i]; |
|
method = queue[i+1]; |
|
args = queue[i+2]; |
|
stack = queue[i+3]; // Debugging assistance |
|
|
|
// TODO: error handling |
|
if (args && args.length > 0) { |
|
method.apply(target, args); |
|
} else { |
|
method.call(target); |
|
} |
|
} |
|
if (l && after) { after(); } |
|
|
|
// check if new items have been added |
|
if (queue.length > l) { |
|
this._queue = queue.slice(l); |
|
this.flush(); |
|
} else { |
|
this._queue.length = 0; |
|
} |
|
}, |
|
|
|
cancel: function(actionToCancel) { |
|
var queue = this._queue, currentTarget, currentMethod, i, l; |
|
|
|
for (i = 0, l = queue.length; i < l; i += 4) { |
|
currentTarget = queue[i]; |
|
currentMethod = queue[i+1]; |
|
|
|
if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { |
|
queue.splice(i, 4); |
|
return true; |
|
} |
|
} |
|
|
|
// if not found in current queue |
|
// could be in the queue that is being flushed |
|
queue = this._queueBeingFlushed; |
|
if (!queue) { |
|
return; |
|
} |
|
for (i = 0, l = queue.length; i < l; i += 4) { |
|
currentTarget = queue[i]; |
|
currentMethod = queue[i+1]; |
|
|
|
if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { |
|
// don't mess with array during flush |
|
// just nullify the method |
|
queue[i+1] = null; |
|
return true; |
|
} |
|
} |
|
} |
|
}; |
|
|
|
__exports__.Queue = Queue; |
|
}); |
|
|
|
define("backburner/deferred_action_queues", |
|
["backburner/queue","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
var Queue = __dependency1__.Queue; |
|
|
|
function DeferredActionQueues(queueNames, options) { |
|
var queues = this.queues = {}; |
|
this.queueNames = queueNames = queueNames || []; |
|
|
|
var queueName; |
|
for (var i = 0, l = queueNames.length; i < l; i++) { |
|
queueName = queueNames[i]; |
|
queues[queueName] = new Queue(this, queueName, options[queueName]); |
|
} |
|
} |
|
|
|
DeferredActionQueues.prototype = { |
|
queueNames: null, |
|
queues: null, |
|
|
|
schedule: function(queueName, target, method, args, onceFlag, stack) { |
|
var queues = this.queues, |
|
queue = queues[queueName]; |
|
|
|
if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); } |
|
|
|
if (onceFlag) { |
|
return queue.pushUnique(target, method, args, stack); |
|
} else { |
|
return queue.push(target, method, args, stack); |
|
} |
|
}, |
|
|
|
flush: function() { |
|
var queues = this.queues, |
|
queueNames = this.queueNames, |
|
queueName, queue, queueItems, priorQueueNameIndex, |
|
queueNameIndex = 0, numberOfQueues = queueNames.length; |
|
|
|
outerloop: |
|
while (queueNameIndex < numberOfQueues) { |
|
queueName = queueNames[queueNameIndex]; |
|
queue = queues[queueName]; |
|
queueItems = queue._queueBeingFlushed = queue._queue.slice(); |
|
queue._queue = []; |
|
|
|
var options = queue.options, |
|
before = options && options.before, |
|
after = options && options.after, |
|
target, method, args, stack, |
|
queueIndex = 0, numberOfQueueItems = queueItems.length; |
|
|
|
if (numberOfQueueItems && before) { before(); } |
|
while (queueIndex < numberOfQueueItems) { |
|
target = queueItems[queueIndex]; |
|
method = queueItems[queueIndex+1]; |
|
args = queueItems[queueIndex+2]; |
|
stack = queueItems[queueIndex+3]; // Debugging assistance |
|
|
|
if (typeof method === 'string') { method = target[method]; } |
|
|
|
// method could have been nullified / canceled during flush |
|
if (method) { |
|
// TODO: error handling |
|
if (args && args.length > 0) { |
|
method.apply(target, args); |
|
} else { |
|
method.call(target); |
|
} |
|
} |
|
|
|
queueIndex += 4; |
|
} |
|
queue._queueBeingFlushed = null; |
|
if (numberOfQueueItems && after) { after(); } |
|
|
|
if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) { |
|
queueNameIndex = priorQueueNameIndex; |
|
continue outerloop; |
|
} |
|
|
|
queueNameIndex++; |
|
} |
|
} |
|
}; |
|
|
|
function indexOfPriorQueueWithActions(daq, currentQueueIndex) { |
|
var queueName, queue; |
|
|
|
for (var i = 0, l = currentQueueIndex; i <= l; i++) { |
|
queueName = daq.queueNames[i]; |
|
queue = daq.queues[queueName]; |
|
if (queue._queue.length) { return i; } |
|
} |
|
|
|
return -1; |
|
} |
|
|
|
__exports__.DeferredActionQueues = DeferredActionQueues; |
|
}); |
|
|
|
define("backburner", |
|
["backburner/deferred_action_queues","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
var DeferredActionQueues = __dependency1__.DeferredActionQueues; |
|
|
|
var slice = [].slice, |
|
pop = [].pop, |
|
throttlers = [], |
|
debouncees = [], |
|
timers = [], |
|
autorun, laterTimer, laterTimerExpiresAt, |
|
global = this, |
|
NUMBER = /\d+/; |
|
|
|
function isCoercableNumber(number) { |
|
return typeof number === 'number' || NUMBER.test(number); |
|
} |
|
|
|
function Backburner(queueNames, options) { |
|
this.queueNames = queueNames; |
|
this.options = options || {}; |
|
if (!this.options.defaultQueue) { |
|
this.options.defaultQueue = queueNames[0]; |
|
} |
|
this.instanceStack = []; |
|
} |
|
|
|
Backburner.prototype = { |
|
queueNames: null, |
|
options: null, |
|
currentInstance: null, |
|
instanceStack: null, |
|
|
|
begin: function() { |
|
var onBegin = this.options && this.options.onBegin, |
|
previousInstance = this.currentInstance; |
|
|
|
if (previousInstance) { |
|
this.instanceStack.push(previousInstance); |
|
} |
|
|
|
this.currentInstance = new DeferredActionQueues(this.queueNames, this.options); |
|
if (onBegin) { |
|
onBegin(this.currentInstance, previousInstance); |
|
} |
|
}, |
|
|
|
end: function() { |
|
var onEnd = this.options && this.options.onEnd, |
|
currentInstance = this.currentInstance, |
|
nextInstance = null; |
|
|
|
try { |
|
currentInstance.flush(); |
|
} finally { |
|
this.currentInstance = null; |
|
|
|
if (this.instanceStack.length) { |
|
nextInstance = this.instanceStack.pop(); |
|
this.currentInstance = nextInstance; |
|
} |
|
|
|
if (onEnd) { |
|
onEnd(currentInstance, nextInstance); |
|
} |
|
} |
|
}, |
|
|
|
run: function(target, method /*, args */) { |
|
var ret; |
|
this.begin(); |
|
|
|
if (!method) { |
|
method = target; |
|
target = null; |
|
} |
|
|
|
if (typeof method === 'string') { |
|
method = target[method]; |
|
} |
|
|
|
// Prevent Safari double-finally. |
|
var finallyAlreadyCalled = false; |
|
try { |
|
if (arguments.length > 2) { |
|
ret = method.apply(target, slice.call(arguments, 2)); |
|
} else { |
|
ret = method.call(target); |
|
} |
|
} finally { |
|
if (!finallyAlreadyCalled) { |
|
finallyAlreadyCalled = true; |
|
this.end(); |
|
} |
|
} |
|
return ret; |
|
}, |
|
|
|
defer: function(queueName, target, method /* , args */) { |
|
if (!method) { |
|
method = target; |
|
target = null; |
|
} |
|
|
|
if (typeof method === 'string') { |
|
method = target[method]; |
|
} |
|
|
|
var stack = this.DEBUG ? new Error() : undefined, |
|
args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; |
|
if (!this.currentInstance) { createAutorun(this); } |
|
return this.currentInstance.schedule(queueName, target, method, args, false, stack); |
|
}, |
|
|
|
deferOnce: function(queueName, target, method /* , args */) { |
|
if (!method) { |
|
method = target; |
|
target = null; |
|
} |
|
|
|
if (typeof method === 'string') { |
|
method = target[method]; |
|
} |
|
|
|
var stack = this.DEBUG ? new Error() : undefined, |
|
args = arguments.length > 3 ? slice.call(arguments, 3) : undefined; |
|
if (!this.currentInstance) { createAutorun(this); } |
|
return this.currentInstance.schedule(queueName, target, method, args, true, stack); |
|
}, |
|
|
|
setTimeout: function() { |
|
var args = slice.call(arguments); |
|
var length = args.length; |
|
var method, wait, target; |
|
var self = this; |
|
var methodOrTarget, methodOrWait, methodOrArgs; |
|
|
|
if (length === 0) { |
|
return; |
|
} else if (length === 1) { |
|
method = args.shift(); |
|
wait = 0; |
|
} else if (length === 2) { |
|
methodOrTarget = args[0]; |
|
methodOrWait = args[1]; |
|
|
|
if (typeof methodOrWait === 'function' || typeof methodOrTarget[methodOrWait] === 'function') { |
|
target = args.shift(); |
|
method = args.shift(); |
|
wait = 0; |
|
} else if (isCoercableNumber(methodOrWait)) { |
|
method = args.shift(); |
|
wait = args.shift(); |
|
} else { |
|
method = args.shift(); |
|
wait = 0; |
|
} |
|
} else { |
|
var last = args[args.length - 1]; |
|
|
|
if (isCoercableNumber(last)) { |
|
wait = args.pop(); |
|
} |
|
|
|
methodOrTarget = args[0]; |
|
methodOrArgs = args[1]; |
|
|
|
if (typeof methodOrArgs === 'function' || (typeof methodOrArgs === 'string' && |
|
methodOrTarget !== null && |
|
methodOrArgs in methodOrTarget)) { |
|
target = args.shift(); |
|
method = args.shift(); |
|
} else { |
|
method = args.shift(); |
|
} |
|
} |
|
|
|
var executeAt = (+new Date()) + parseInt(wait, 10); |
|
|
|
if (typeof method === 'string') { |
|
method = target[method]; |
|
} |
|
|
|
function fn() { |
|
method.apply(target, args); |
|
} |
|
|
|
// find position to insert - TODO: binary search |
|
var i, l; |
|
for (i = 0, l = timers.length; i < l; i += 2) { |
|
if (executeAt < timers[i]) { break; } |
|
} |
|
|
|
timers.splice(i, 0, executeAt, fn); |
|
|
|
updateLaterTimer(self, executeAt, wait); |
|
|
|
return fn; |
|
}, |
|
|
|
throttle: function(target, method /* , args, wait, [immediate] */) { |
|
var self = this, |
|
args = arguments, |
|
immediate = pop.call(args), |
|
wait, |
|
throttler, |
|
index, |
|
timer; |
|
|
|
if (typeof immediate === "number" || typeof immediate === "string") { |
|
wait = immediate; |
|
immediate = true; |
|
} else { |
|
wait = pop.call(args); |
|
} |
|
|
|
wait = parseInt(wait, 10); |
|
|
|
index = findThrottler(target, method); |
|
if (index > -1) { return throttlers[index]; } // throttled |
|
|
|
timer = global.setTimeout(function() { |
|
if (!immediate) { |
|
self.run.apply(self, args); |
|
} |
|
var index = findThrottler(target, method); |
|
if (index > -1) { throttlers.splice(index, 1); } |
|
}, wait); |
|
|
|
if (immediate) { |
|
self.run.apply(self, args); |
|
} |
|
|
|
throttler = [target, method, timer]; |
|
|
|
throttlers.push(throttler); |
|
|
|
return throttler; |
|
}, |
|
|
|
debounce: function(target, method /* , args, wait, [immediate] */) { |
|
var self = this, |
|
args = arguments, |
|
immediate = pop.call(args), |
|
wait, |
|
index, |
|
debouncee, |
|
timer; |
|
|
|
if (typeof immediate === "number" || typeof immediate === "string") { |
|
wait = immediate; |
|
immediate = false; |
|
} else { |
|
wait = pop.call(args); |
|
} |
|
|
|
wait = parseInt(wait, 10); |
|
// Remove debouncee |
|
index = findDebouncee(target, method); |
|
|
|
if (index > -1) { |
|
debouncee = debouncees[index]; |
|
debouncees.splice(index, 1); |
|
clearTimeout(debouncee[2]); |
|
} |
|
|
|
timer = global.setTimeout(function() { |
|
if (!immediate) { |
|
self.run.apply(self, args); |
|
} |
|
var index = findDebouncee(target, method); |
|
if (index > -1) { |
|
debouncees.splice(index, 1); |
|
} |
|
}, wait); |
|
|
|
if (immediate && index === -1) { |
|
self.run.apply(self, args); |
|
} |
|
|
|
debouncee = [target, method, timer]; |
|
|
|
debouncees.push(debouncee); |
|
|
|
return debouncee; |
|
}, |
|
|
|
cancelTimers: function() { |
|
var i, len; |
|
|
|
for (i = 0, len = throttlers.length; i < len; i++) { |
|
clearTimeout(throttlers[i][2]); |
|
} |
|
throttlers = []; |
|
|
|
for (i = 0, len = debouncees.length; i < len; i++) { |
|
clearTimeout(debouncees[i][2]); |
|
} |
|
debouncees = []; |
|
|
|
if (laterTimer) { |
|
clearTimeout(laterTimer); |
|
laterTimer = null; |
|
} |
|
timers = []; |
|
|
|
if (autorun) { |
|
clearTimeout(autorun); |
|
autorun = null; |
|
} |
|
}, |
|
|
|
hasTimers: function() { |
|
return !!timers.length || autorun; |
|
}, |
|
|
|
cancel: function(timer) { |
|
var timerType = typeof timer; |
|
|
|
if (timer && timerType === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce |
|
return timer.queue.cancel(timer); |
|
} else if (timerType === 'function') { // we're cancelling a setTimeout |
|
for (var i = 0, l = timers.length; i < l; i += 2) { |
|
if (timers[i + 1] === timer) { |
|
timers.splice(i, 2); // remove the two elements |
|
return true; |
|
} |
|
} |
|
} else if (Object.prototype.toString.call(timer) === "[object Array]"){ // we're cancelling a throttle or debounce |
|
return this._cancelItem(findThrottler, throttlers, timer) || |
|
this._cancelItem(findDebouncee, debouncees, timer); |
|
} else { |
|
return; // timer was null or not a timer |
|
} |
|
}, |
|
|
|
_cancelItem: function(findMethod, array, timer){ |
|
var item, |
|
index; |
|
|
|
if (timer.length < 3) { return false; } |
|
|
|
index = findMethod(timer[0], timer[1]); |
|
|
|
if(index > -1) { |
|
|
|
item = array[index]; |
|
|
|
if(item[2] === timer[2]){ |
|
array.splice(index, 1); |
|
clearTimeout(timer[2]); |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
}; |
|
|
|
Backburner.prototype.schedule = Backburner.prototype.defer; |
|
Backburner.prototype.scheduleOnce = Backburner.prototype.deferOnce; |
|
Backburner.prototype.later = Backburner.prototype.setTimeout; |
|
|
|
function createAutorun(backburner) { |
|
backburner.begin(); |
|
autorun = global.setTimeout(function() { |
|
autorun = null; |
|
backburner.end(); |
|
}); |
|
} |
|
|
|
function updateLaterTimer(self, executeAt, wait) { |
|
if (!laterTimer || executeAt < laterTimerExpiresAt) { |
|
if (laterTimer) { |
|
clearTimeout(laterTimer); |
|
} |
|
laterTimer = global.setTimeout(function() { |
|
laterTimer = null; |
|
laterTimerExpiresAt = null; |
|
executeTimers(self); |
|
}, wait); |
|
laterTimerExpiresAt = executeAt; |
|
} |
|
} |
|
|
|
function executeTimers(self) { |
|
var now = +new Date(), |
|
time, fns, i, l; |
|
|
|
self.run(function() { |
|
// TODO: binary search |
|
for (i = 0, l = timers.length; i < l; i += 2) { |
|
time = timers[i]; |
|
if (time > now) { break; } |
|
} |
|
|
|
fns = timers.splice(0, i); |
|
|
|
for (i = 1, l = fns.length; i < l; i += 2) { |
|
self.schedule(self.options.defaultQueue, null, fns[i]); |
|
} |
|
}); |
|
|
|
if (timers.length) { |
|
updateLaterTimer(self, timers[0], timers[0] - now); |
|
} |
|
} |
|
|
|
function findDebouncee(target, method) { |
|
var debouncee, |
|
index = -1; |
|
|
|
for (var i = 0, l = debouncees.length; i < l; i++) { |
|
debouncee = debouncees[i]; |
|
if (debouncee[0] === target && debouncee[1] === method) { |
|
index = i; |
|
break; |
|
} |
|
} |
|
|
|
return index; |
|
} |
|
|
|
function findThrottler(target, method) { |
|
var throttler, |
|
index = -1; |
|
|
|
for (var i = 0, l = throttlers.length; i < l; i++) { |
|
throttler = throttlers[i]; |
|
if (throttler[0] === target && throttler[1] === method) { |
|
index = i; |
|
break; |
|
} |
|
} |
|
|
|
return index; |
|
} |
|
|
|
__exports__.Backburner = Backburner; |
|
}); |
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var onBegin = function(current) { |
|
Ember.run.currentRunLoop = current; |
|
}; |
|
|
|
var onEnd = function(current, next) { |
|
Ember.run.currentRunLoop = next; |
|
}; |
|
|
|
var Backburner = requireModule('backburner').Backburner, |
|
backburner = new Backburner(['sync', 'actions', 'destroy'], { |
|
sync: { |
|
before: Ember.beginPropertyChanges, |
|
after: Ember.endPropertyChanges |
|
}, |
|
defaultQueue: 'actions', |
|
onBegin: onBegin, |
|
onEnd: onEnd |
|
}), |
|
slice = [].slice, |
|
concat = [].concat; |
|
|
|
// .......................................................... |
|
// Ember.run - this is ideally the only public API the dev sees |
|
// |
|
|
|
/** |
|
Runs the passed target and method inside of a RunLoop, ensuring any |
|
deferred actions including bindings and views updates are flushed at the |
|
end. |
|
|
|
Normally you should not need to invoke this method yourself. However if |
|
you are implementing raw event handlers when interfacing with other |
|
libraries or plugins, you should probably wrap all of your code inside this |
|
call. |
|
|
|
```javascript |
|
Ember.run(function() { |
|
// code to be execute within a RunLoop |
|
}); |
|
``` |
|
|
|
@class run |
|
@namespace Ember |
|
@static |
|
@constructor |
|
@param {Object} [target] target of method to call |
|
@param {Function|String} method Method to invoke. |
|
May be a function or a string. If you pass a string |
|
then it will be looked up on the passed target. |
|
@param {Object} [args*] Any additional arguments you wish to pass to the method. |
|
@return {Object} return value from invoking the passed function. |
|
*/ |
|
Ember.run = function() { |
|
if (Ember.onerror) { |
|
return onerror(arguments); |
|
} else { |
|
return backburner.run.apply(backburner, arguments); |
|
} |
|
}; |
|
|
|
function onerror(args) { |
|
try { |
|
return backburner.run.apply(backburner, args); |
|
} catch(error) { |
|
Ember.onerror(error); |
|
} |
|
} |
|
/** |
|
If no run-loop is present, it creates a new one. If a run loop is |
|
present it will queue itself to run on the existing run-loops action |
|
queue. |
|
|
|
Please note: This is not for normal usage, and should be used sparingly. |
|
|
|
If invoked when not within a run loop: |
|
|
|
```javascript |
|
Ember.run.join(function() { |
|
// creates a new run-loop |
|
}); |
|
``` |
|
|
|
Alternatively, if called within an existing run loop: |
|
|
|
```javascript |
|
Ember.run(function() { |
|
// creates a new run-loop |
|
Ember.run.join(function() { |
|
// joins with the existing run-loop, and queues for invocation on |
|
// the existing run-loops action queue. |
|
}); |
|
}); |
|
``` |
|
|
|
@method join |
|
@namespace Ember |
|
@param {Object} [target] target of method to call |
|
@param {Function|String} method Method to invoke. |
|
May be a function or a string. If you pass a string |
|
then it will be looked up on the passed target. |
|
@param {Object} [args*] Any additional arguments you wish to pass to the method. |
|
@return {Object} Return value from invoking the passed function. Please note, |
|
when called within an existing loop, no return value is possible. |
|
*/ |
|
Ember.run.join = function(target, method /* args */) { |
|
if (!Ember.run.currentRunLoop) { |
|
return Ember.run.apply(Ember.run, arguments); |
|
} |
|
|
|
var args = slice.call(arguments); |
|
args.unshift('actions'); |
|
Ember.run.schedule.apply(Ember.run, args); |
|
}; |
|
|
|
/** |
|
Provides a useful utility for when integrating with non-Ember libraries |
|
that provide asynchronous callbacks. |
|
|
|
Ember utilizes a run-loop to batch and coalesce changes. This works by |
|
marking the start and end of Ember-related Javascript execution. |
|
|
|
When using events such as a View's click handler, Ember wraps the event |
|
handler in a run-loop, but when integrating with non-Ember libraries this |
|
can be tedious. |
|
|
|
For example, the following is rather verbose but is the correct way to combine |
|
third-party events and Ember code. |
|
|
|
```javascript |
|
var that = this; |
|
jQuery(window).on('resize', function(){ |
|
Ember.run(function(){ |
|
that.handleResize(); |
|
}); |
|
}); |
|
``` |
|
|
|
To reduce the boilerplate, the following can be used to construct a |
|
run-loop-wrapped callback handler. |
|
|
|
```javascript |
|
jQuery(window).on('resize', Ember.run.bind(this, this.handleResize)); |
|
``` |
|
|
|
@method bind |
|
@namespace Ember.run |
|
@param {Object} [target] target of method to call |
|
@param {Function|String} method Method to invoke. |
|
May be a function or a string. If you pass a string |
|
then it will be looked up on the passed target. |
|
@param {Object} [args*] Any additional arguments you wish to pass to the method. |
|
@return {Object} return value from invoking the passed function. Please note, |
|
when called within an existing loop, no return value is possible. |
|
*/ |
|
Ember.run.bind = function(target, method /* args*/) { |
|
var args = slice.call(arguments); |
|
return function() { |
|
return Ember.run.join.apply(Ember.run, args.concat(slice.call(arguments))); |
|
}; |
|
}; |
|
|
|
Ember.run.backburner = backburner; |
|
|
|
var run = Ember.run; |
|
|
|
Ember.run.currentRunLoop = null; |
|
|
|
Ember.run.queues = backburner.queueNames; |
|
|
|
/** |
|
Begins a new RunLoop. Any deferred actions invoked after the begin will |
|
be buffered until you invoke a matching call to `Ember.run.end()`. This is |
|
a lower-level way to use a RunLoop instead of using `Ember.run()`. |
|
|
|
```javascript |
|
Ember.run.begin(); |
|
// code to be execute within a RunLoop |
|
Ember.run.end(); |
|
``` |
|
|
|
@method begin |
|
@return {void} |
|
*/ |
|
Ember.run.begin = function() { |
|
backburner.begin(); |
|
}; |
|
|
|
/** |
|
Ends a RunLoop. This must be called sometime after you call |
|
`Ember.run.begin()` to flush any deferred actions. This is a lower-level way |
|
to use a RunLoop instead of using `Ember.run()`. |
|
|
|
```javascript |
|
Ember.run.begin(); |
|
// code to be execute within a RunLoop |
|
Ember.run.end(); |
|
``` |
|
|
|
@method end |
|
@return {void} |
|
*/ |
|
Ember.run.end = function() { |
|
backburner.end(); |
|
}; |
|
|
|
/** |
|
Array of named queues. This array determines the order in which queues |
|
are flushed at the end of the RunLoop. You can define your own queues by |
|
simply adding the queue name to this array. Normally you should not need |
|
to inspect or modify this property. |
|
|
|
@property queues |
|
@type Array |
|
@default ['sync', 'actions', 'destroy'] |
|
*/ |
|
|
|
/** |
|
Adds the passed target/method and any optional arguments to the named |
|
queue to be executed at the end of the RunLoop. If you have not already |
|
started a RunLoop when calling this method one will be started for you |
|
automatically. |
|
|
|
At the end of a RunLoop, any methods scheduled in this way will be invoked. |
|
Methods will be invoked in an order matching the named queues defined in |
|
the `Ember.run.queues` property. |
|
|
|
```javascript |
|
Ember.run.schedule('sync', this, function() { |
|
// this will be executed in the first RunLoop queue, when bindings are synced |
|
console.log("scheduled on sync queue"); |
|
}); |
|
|
|
Ember.run.schedule('actions', this, function() { |
|
// this will be executed in the 'actions' queue, after bindings have synced. |
|
console.log("scheduled on actions queue"); |
|
}); |
|
|
|
// Note the functions will be run in order based on the run queues order. |
|
// Output would be: |
|
// scheduled on sync queue |
|
// scheduled on actions queue |
|
``` |
|
|
|
@method schedule |
|
@param {String} queue The name of the queue to schedule against. |
|
Default queues are 'sync' and 'actions' |
|
@param {Object} [target] target object to use as the context when invoking a method. |
|
@param {String|Function} method The method to invoke. If you pass a string it |
|
will be resolved on the target object at the time the scheduled item is |
|
invoked allowing you to change the target function. |
|
@param {Object} [arguments*] Optional arguments to be passed to the queued method. |
|
@return {void} |
|
*/ |
|
Ember.run.schedule = function(queue, target, method) { |
|
checkAutoRun(); |
|
backburner.schedule.apply(backburner, arguments); |
|
}; |
|
|
|
// Used by global test teardown |
|
Ember.run.hasScheduledTimers = function() { |
|
return backburner.hasTimers(); |
|
}; |
|
|
|
// Used by global test teardown |
|
Ember.run.cancelTimers = function () { |
|
backburner.cancelTimers(); |
|
}; |
|
|
|
/** |
|
Immediately flushes any events scheduled in the 'sync' queue. Bindings |
|
use this queue so this method is a useful way to immediately force all |
|
bindings in the application to sync. |
|
|
|
You should call this method anytime you need any changed state to propagate |
|
throughout the app immediately without repainting the UI (which happens |
|
in the later 'render' queue added by the `ember-views` package). |
|
|
|
```javascript |
|
Ember.run.sync(); |
|
``` |
|
|
|
@method sync |
|
@return {void} |
|
*/ |
|
Ember.run.sync = function() { |
|
if (backburner.currentInstance) { |
|
backburner.currentInstance.queues.sync.flush(); |
|
} |
|
}; |
|
|
|
/** |
|
Invokes the passed target/method and optional arguments after a specified |
|
period if time. The last parameter of this method must always be a number |
|
of milliseconds. |
|
|
|
You should use this method whenever you need to run some action after a |
|
period of time instead of using `setTimeout()`. This method will ensure that |
|
items that expire during the same script execution cycle all execute |
|
together, which is often more efficient than using a real setTimeout. |
|
|
|
```javascript |
|
Ember.run.later(myContext, function() { |
|
// code here will execute within a RunLoop in about 500ms with this == myContext |
|
}, 500); |
|
``` |
|
|
|
@method later |
|
@param {Object} [target] target of method to invoke |
|
@param {Function|String} method The method to invoke. |
|
If you pass a string it will be resolved on the |
|
target at the time the method is invoked. |
|
@param {Object} [args*] Optional arguments to pass to the timeout. |
|
@param {Number} wait Number of milliseconds to wait. |
|
@return {String} a string you can use to cancel the timer in |
|
`Ember.run.cancel` later. |
|
*/ |
|
Ember.run.later = function(target, method) { |
|
return backburner.later.apply(backburner, arguments); |
|
}; |
|
|
|
/** |
|
Schedule a function to run one time during the current RunLoop. This is equivalent |
|
to calling `scheduleOnce` with the "actions" queue. |
|
|
|
@method once |
|
@param {Object} [target] The target of the method to invoke. |
|
@param {Function|String} method The method to invoke. |
|
If you pass a string it will be resolved on the |
|
target at the time the method is invoked. |
|
@param {Object} [args*] Optional arguments to pass to the timeout. |
|
@return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. |
|
*/ |
|
Ember.run.once = function(target, method) { |
|
checkAutoRun(); |
|
var args = slice.call(arguments); |
|
args.unshift('actions'); |
|
return backburner.scheduleOnce.apply(backburner, args); |
|
}; |
|
|
|
/** |
|
Schedules a function to run one time in a given queue of the current RunLoop. |
|
Calling this method with the same queue/target/method combination will have |
|
no effect (past the initial call). |
|
|
|
Note that although you can pass optional arguments these will not be |
|
considered when looking for duplicates. New arguments will replace previous |
|
calls. |
|
|
|
```javascript |
|
Ember.run(function() { |
|
var sayHi = function() { console.log('hi'); } |
|
Ember.run.scheduleOnce('afterRender', myContext, sayHi); |
|
Ember.run.scheduleOnce('afterRender', myContext, sayHi); |
|
// sayHi will only be executed once, in the afterRender queue of the RunLoop |
|
}); |
|
``` |
|
|
|
Also note that passing an anonymous function to `Ember.run.scheduleOnce` will |
|
not prevent additional calls with an identical anonymous function from |
|
scheduling the items multiple times, e.g.: |
|
|
|
```javascript |
|
function scheduleIt() { |
|
Ember.run.scheduleOnce('actions', myContext, function() { console.log("Closure"); }); |
|
} |
|
scheduleIt(); |
|
scheduleIt(); |
|
// "Closure" will print twice, even though we're using `Ember.run.scheduleOnce`, |
|
// because the function we pass to it is anonymous and won't match the |
|
// previously scheduled operation. |
|
``` |
|
|
|
Available queues, and their order, can be found at `Ember.run.queues` |
|
|
|
@method scheduleOnce |
|
@param {String} [queue] The name of the queue to schedule against. Default queues are 'sync' and 'actions'. |
|
@param {Object} [target] The target of the method to invoke. |
|
@param {Function|String} method The method to invoke. |
|
If you pass a string it will be resolved on the |
|
target at the time the method is invoked. |
|
@param {Object} [args*] Optional arguments to pass to the timeout. |
|
@return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. |
|
*/ |
|
Ember.run.scheduleOnce = function(queue, target, method) { |
|
checkAutoRun(); |
|
return backburner.scheduleOnce.apply(backburner, arguments); |
|
}; |
|
|
|
/** |
|
Schedules an item to run from within a separate run loop, after |
|
control has been returned to the system. This is equivalent to calling |
|
`Ember.run.later` with a wait time of 1ms. |
|
|
|
```javascript |
|
Ember.run.next(myContext, function() { |
|
// code to be executed in the next run loop, |
|
// which will be scheduled after the current one |
|
}); |
|
``` |
|
|
|
Multiple operations scheduled with `Ember.run.next` will coalesce |
|
into the same later run loop, along with any other operations |
|
scheduled by `Ember.run.later` that expire right around the same |
|
time that `Ember.run.next` operations will fire. |
|
|
|
Note that there are often alternatives to using `Ember.run.next`. |
|
For instance, if you'd like to schedule an operation to happen |
|
after all DOM element operations have completed within the current |
|
run loop, you can make use of the `afterRender` run loop queue (added |
|
by the `ember-views` package, along with the preceding `render` queue |
|
where all the DOM element operations happen). Example: |
|
|
|
```javascript |
|
App.MyCollectionView = Ember.CollectionView.extend({ |
|
didInsertElement: function() { |
|
Ember.run.scheduleOnce('afterRender', this, 'processChildElements'); |
|
}, |
|
processChildElements: function() { |
|
// ... do something with collectionView's child view |
|
// elements after they've finished rendering, which |
|
// can't be done within the CollectionView's |
|
// `didInsertElement` hook because that gets run |
|
// before the child elements have been added to the DOM. |
|
} |
|
}); |
|
``` |
|
|
|
One benefit of the above approach compared to using `Ember.run.next` is |
|
that you will be able to perform DOM/CSS operations before unprocessed |
|
elements are rendered to the screen, which may prevent flickering or |
|
other artifacts caused by delaying processing until after rendering. |
|
|
|
The other major benefit to the above approach is that `Ember.run.next` |
|
introduces an element of non-determinism, which can make things much |
|
harder to test, due to its reliance on `setTimeout`; it's much harder |
|
to guarantee the order of scheduled operations when they are scheduled |
|
outside of the current run loop, i.e. with `Ember.run.next`. |
|
|
|
@method next |
|
@param {Object} [target] target of method to invoke |
|
@param {Function|String} method The method to invoke. |
|
If you pass a string it will be resolved on the |
|
target at the time the method is invoked. |
|
@param {Object} [args*] Optional arguments to pass to the timeout. |
|
@return {Object} Timer information for use in cancelling, see `Ember.run.cancel`. |
|
*/ |
|
Ember.run.next = function() { |
|
var args = slice.call(arguments); |
|
args.push(1); |
|
return backburner.later.apply(backburner, args); |
|
}; |
|
|
|
/** |
|
Cancels a scheduled item. Must be a value returned by `Ember.run.later()`, |
|
`Ember.run.once()`, `Ember.run.next()`, `Ember.run.debounce()`, or |
|
`Ember.run.throttle()`. |
|
|
|
```javascript |
|
var runNext = Ember.run.next(myContext, function() { |
|
// will not be executed |
|
}); |
|
Ember.run.cancel(runNext); |
|
|
|
var runLater = Ember.run.later(myContext, function() { |
|
// will not be executed |
|
}, 500); |
|
Ember.run.cancel(runLater); |
|
|
|
var runOnce = Ember.run.once(myContext, function() { |
|
// will not be executed |
|
}); |
|
Ember.run.cancel(runOnce); |
|
|
|
var throttle = Ember.run.throttle(myContext, function() { |
|
// will not be executed |
|
}, 1, false); |
|
Ember.run.cancel(throttle); |
|
|
|
var debounce = Ember.run.debounce(myContext, function() { |
|
// will not be executed |
|
}, 1); |
|
Ember.run.cancel(debounce); |
|
|
|
var debounceImmediate = Ember.run.debounce(myContext, function() { |
|
// will be executed since we passed in true (immediate) |
|
}, 100, true); |
|
// the 100ms delay until this method can be called again will be cancelled |
|
Ember.run.cancel(debounceImmediate); |
|
``` |
|
|
|
@method cancel |
|
@param {Object} timer Timer object to cancel |
|
@return {Boolean} true if cancelled or false/undefined if it wasn't found |
|
*/ |
|
Ember.run.cancel = function(timer) { |
|
return backburner.cancel(timer); |
|
}; |
|
|
|
/** |
|
Delay calling the target method until the debounce period has elapsed |
|
with no additional debounce calls. If `debounce` is called again before |
|
the specified time has elapsed, the timer is reset and the entire period |
|
must pass again before the target method is called. |
|
|
|
This method should be used when an event may be called multiple times |
|
but the action should only be called once when the event is done firing. |
|
A common example is for scroll events where you only want updates to |
|
happen once scrolling has ceased. |
|
|
|
```javascript |
|
var myFunc = function() { console.log(this.name + ' ran.'); }; |
|
var myContext = {name: 'debounce'}; |
|
|
|
Ember.run.debounce(myContext, myFunc, 150); |
|
|
|
// less than 150ms passes |
|
|
|
Ember.run.debounce(myContext, myFunc, 150); |
|
|
|
// 150ms passes |
|
// myFunc is invoked with context myContext |
|
// console logs 'debounce ran.' one time. |
|
``` |
|
|
|
Immediate allows you to run the function immediately, but debounce |
|
other calls for this function until the wait time has elapsed. If |
|
`debounce` is called again before the specified time has elapsed, |
|
the timer is reset and the entire period must pass again before |
|
the method can be called again. |
|
|
|
```javascript |
|
var myFunc = function() { console.log(this.name + ' ran.'); }; |
|
var myContext = {name: 'debounce'}; |
|
|
|
Ember.run.debounce(myContext, myFunc, 150, true); |
|
|
|
// console logs 'debounce ran.' one time immediately. |
|
// 100ms passes |
|
|
|
Ember.run.debounce(myContext, myFunc, 150, true); |
|
|
|
// 150ms passes and nothing else is logged to the console and |
|
// the debouncee is no longer being watched |
|
|
|
Ember.run.debounce(myContext, myFunc, 150, true); |
|
|
|
// console logs 'debounce ran.' one time immediately. |
|
// 150ms passes and nothing else is logged tot he console and |
|
// the debouncee is no longer being watched |
|
|
|
``` |
|
|
|
@method debounce |
|
@param {Object} [target] target of method to invoke |
|
@param {Function|String} method The method to invoke. |
|
May be a function or a string. If you pass a string |
|
then it will be looked up on the passed target. |
|
@param {Object} [args*] Optional arguments to pass to the timeout. |
|
@param {Number} wait Number of milliseconds to wait. |
|
@param {Boolean} immediate Trigger the function on the leading instead |
|
of the trailing edge of the wait interval. Defaults to false. |
|
@return {Array} Timer information for use in cancelling, see `Ember.run.cancel`. |
|
*/ |
|
Ember.run.debounce = function() { |
|
return backburner.debounce.apply(backburner, arguments); |
|
}; |
|
|
|
/** |
|
Ensure that the target method is never called more frequently than |
|
the specified spacing period. |
|
|
|
```javascript |
|
var myFunc = function() { console.log(this.name + ' ran.'); }; |
|
var myContext = {name: 'throttle'}; |
|
|
|
Ember.run.throttle(myContext, myFunc, 150); |
|
// myFunc is invoked with context myContext |
|
|
|
// 50ms passes |
|
Ember.run.throttle(myContext, myFunc, 150); |
|
|
|
// 50ms passes |
|
Ember.run.throttle(myContext, myFunc, 150); |
|
|
|
// 150ms passes |
|
Ember.run.throttle(myContext, myFunc, 150); |
|
// myFunc is invoked with context myContext |
|
// console logs 'throttle ran.' twice, 250ms apart. |
|
``` |
|
|
|
@method throttle |
|
@param {Object} [target] target of method to invoke |
|
@param {Function|String} method The method to invoke. |
|
May be a function or a string. If you pass a string |
|
then it will be looked up on the passed target. |
|
@param {Object} [args*] Optional arguments to pass to the timeout. |
|
@param {Number} spacing Number of milliseconds to space out requests. |
|
@return {Array} Timer information for use in cancelling, see `Ember.run.cancel`. |
|
*/ |
|
Ember.run.throttle = function() { |
|
return backburner.throttle.apply(backburner, arguments); |
|
}; |
|
|
|
// Make sure it's not an autorun during testing |
|
function checkAutoRun() { |
|
if (!Ember.run.currentRunLoop) { |
|
Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); |
|
} |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
// Ember.Logger |
|
// get |
|
// set |
|
// guidFor, meta |
|
// addObserver, removeObserver |
|
// Ember.run.schedule |
|
/** |
|
@module ember-metal |
|
*/ |
|
|
|
// .......................................................... |
|
// CONSTANTS |
|
// |
|
|
|
/** |
|
Debug parameter you can turn on. This will log all bindings that fire to |
|
the console. This should be disabled in production code. Note that you |
|
can also enable this from the console or temporarily. |
|
|
|
@property LOG_BINDINGS |
|
@for Ember |
|
@type Boolean |
|
@default false |
|
*/ |
|
Ember.LOG_BINDINGS = false || !!Ember.ENV.LOG_BINDINGS; |
|
|
|
var get = Ember.get, |
|
set = Ember.set, |
|
guidFor = Ember.guidFor, |
|
IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/; |
|
|
|
/** |
|
Returns true if the provided path is global (e.g., `MyApp.fooController.bar`) |
|
instead of local (`foo.bar.baz`). |
|
|
|
@method isGlobalPath |
|
@for Ember |
|
@private |
|
@param {String} path |
|
@return Boolean |
|
*/ |
|
var isGlobalPath = Ember.isGlobalPath = function(path) { |
|
return IS_GLOBAL.test(path); |
|
}; |
|
|
|
function getWithGlobals(obj, path) { |
|
return get(isGlobalPath(path) ? Ember.lookup : obj, path); |
|
} |
|
|
|
// .......................................................... |
|
// BINDING |
|
// |
|
|
|
var Binding = function(toPath, fromPath) { |
|
this._direction = 'fwd'; |
|
this._from = fromPath; |
|
this._to = toPath; |
|
this._directionMap = Ember.Map.create(); |
|
}; |
|
|
|
/** |
|
@class Binding |
|
@namespace Ember |
|
*/ |
|
|
|
Binding.prototype = { |
|
/** |
|
This copies the Binding so it can be connected to another object. |
|
|
|
@method copy |
|
@return {Ember.Binding} `this` |
|
*/ |
|
copy: function () { |
|
var copy = new Binding(this._to, this._from); |
|
if (this._oneWay) { copy._oneWay = true; } |
|
return copy; |
|
}, |
|
|
|
// .......................................................... |
|
// CONFIG |
|
// |
|
|
|
/** |
|
This will set `from` property path to the specified value. It will not |
|
attempt to resolve this property path to an actual object until you |
|
connect the binding. |
|
|
|
The binding will search for the property path starting at the root object |
|
you pass when you `connect()` the binding. It follows the same rules as |
|
`get()` - see that method for more information. |
|
|
|
@method from |
|
@param {String} path the property path to connect to |
|
@return {Ember.Binding} `this` |
|
*/ |
|
from: function(path) { |
|
this._from = path; |
|
return this; |
|
}, |
|
|
|
/** |
|
This will set the `to` property path to the specified value. It will not |
|
attempt to resolve this property path to an actual object until you |
|
connect the binding. |
|
|
|
The binding will search for the property path starting at the root object |
|
you pass when you `connect()` the binding. It follows the same rules as |
|
`get()` - see that method for more information. |
|
|
|
@method to |
|
@param {String|Tuple} path A property path or tuple |
|
@return {Ember.Binding} `this` |
|
*/ |
|
to: function(path) { |
|
this._to = path; |
|
return this; |
|
}, |
|
|
|
/** |
|
Configures the binding as one way. A one-way binding will relay changes |
|
on the `from` side to the `to` side, but not the other way around. This |
|
means that if you change the `to` side directly, the `from` side may have |
|
a different value. |
|
|
|
@method oneWay |
|
@return {Ember.Binding} `this` |
|
*/ |
|
oneWay: function() { |
|
this._oneWay = true; |
|
return this; |
|
}, |
|
|
|
/** |
|
@method toString |
|
@return {String} string representation of binding |
|
*/ |
|
toString: function() { |
|
var oneWay = this._oneWay ? '[oneWay]' : ''; |
|
return "Ember.Binding<" + guidFor(this) + ">(" + this._from + " -> " + this._to + ")" + oneWay; |
|
}, |
|
|
|
// .......................................................... |
|
// CONNECT AND SYNC |
|
// |
|
|
|
/** |
|
Attempts to connect this binding instance so that it can receive and relay |
|
changes. This method will raise an exception if you have not set the |
|
from/to properties yet. |
|
|
|
@method connect |
|
@param {Object} obj The root object for this binding. |
|
@return {Ember.Binding} `this` |
|
*/ |
|
connect: function(obj) { |
|
Ember.assert('Must pass a valid object to Ember.Binding.connect()', !!obj); |
|
|
|
var fromPath = this._from, toPath = this._to; |
|
Ember.trySet(obj, toPath, getWithGlobals(obj, fromPath)); |
|
|
|
// add an observer on the object to be notified when the binding should be updated |
|
Ember.addObserver(obj, fromPath, this, this.fromDidChange); |
|
|
|
// if the binding is a two-way binding, also set up an observer on the target |
|
if (!this._oneWay) { Ember.addObserver(obj, toPath, this, this.toDidChange); } |
|
|
|
this._readyToSync = true; |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Disconnects the binding instance. Changes will no longer be relayed. You |
|
will not usually need to call this method. |
|
|
|
@method disconnect |
|
@param {Object} obj The root object you passed when connecting the binding. |
|
@return {Ember.Binding} `this` |
|
*/ |
|
disconnect: function(obj) { |
|
Ember.assert('Must pass a valid object to Ember.Binding.disconnect()', !!obj); |
|
|
|
var twoWay = !this._oneWay; |
|
|
|
// remove an observer on the object so we're no longer notified of |
|
// changes that should update bindings. |
|
Ember.removeObserver(obj, this._from, this, this.fromDidChange); |
|
|
|
// if the binding is two-way, remove the observer from the target as well |
|
if (twoWay) { Ember.removeObserver(obj, this._to, this, this.toDidChange); } |
|
|
|
this._readyToSync = false; // disable scheduled syncs... |
|
return this; |
|
}, |
|
|
|
// .......................................................... |
|
// PRIVATE |
|
// |
|
|
|
/* called when the from side changes */ |
|
fromDidChange: function(target) { |
|
this._scheduleSync(target, 'fwd'); |
|
}, |
|
|
|
/* called when the to side changes */ |
|
toDidChange: function(target) { |
|
this._scheduleSync(target, 'back'); |
|
}, |
|
|
|
_scheduleSync: function(obj, dir) { |
|
var directionMap = this._directionMap; |
|
var existingDir = directionMap.get(obj); |
|
|
|
// if we haven't scheduled the binding yet, schedule it |
|
if (!existingDir) { |
|
Ember.run.schedule('sync', this, this._sync, obj); |
|
directionMap.set(obj, dir); |
|
} |
|
|
|
// If both a 'back' and 'fwd' sync have been scheduled on the same object, |
|
// default to a 'fwd' sync so that it remains deterministic. |
|
if (existingDir === 'back' && dir === 'fwd') { |
|
directionMap.set(obj, 'fwd'); |
|
} |
|
}, |
|
|
|
_sync: function(obj) { |
|
var log = Ember.LOG_BINDINGS; |
|
|
|
// don't synchronize destroyed objects or disconnected bindings |
|
if (obj.isDestroyed || !this._readyToSync) { return; } |
|
|
|
// get the direction of the binding for the object we are |
|
// synchronizing from |
|
var directionMap = this._directionMap; |
|
var direction = directionMap.get(obj); |
|
|
|
var fromPath = this._from, toPath = this._to; |
|
|
|
directionMap.remove(obj); |
|
|
|
// if we're synchronizing from the remote object... |
|
if (direction === 'fwd') { |
|
var fromValue = getWithGlobals(obj, this._from); |
|
if (log) { |
|
Ember.Logger.log(' ', this.toString(), '->', fromValue, obj); |
|
} |
|
if (this._oneWay) { |
|
Ember.trySet(obj, toPath, fromValue); |
|
} else { |
|
Ember._suspendObserver(obj, toPath, this, this.toDidChange, function () { |
|
Ember.trySet(obj, toPath, fromValue); |
|
}); |
|
} |
|
// if we're synchronizing *to* the remote object |
|
} else if (direction === 'back') { |
|
var toValue = get(obj, this._to); |
|
if (log) { |
|
Ember.Logger.log(' ', this.toString(), '<-', toValue, obj); |
|
} |
|
Ember._suspendObserver(obj, fromPath, this, this.fromDidChange, function () { |
|
Ember.trySet(Ember.isGlobalPath(fromPath) ? Ember.lookup : obj, fromPath, toValue); |
|
}); |
|
} |
|
} |
|
|
|
}; |
|
|
|
function mixinProperties(to, from) { |
|
for (var key in from) { |
|
if (from.hasOwnProperty(key)) { |
|
to[key] = from[key]; |
|
} |
|
} |
|
} |
|
|
|
mixinProperties(Binding, { |
|
|
|
/* |
|
See `Ember.Binding.from`. |
|
|
|
@method from |
|
@static |
|
*/ |
|
from: function() { |
|
var C = this, binding = new C(); |
|
return binding.from.apply(binding, arguments); |
|
}, |
|
|
|
/* |
|
See `Ember.Binding.to`. |
|
|
|
@method to |
|
@static |
|
*/ |
|
to: function() { |
|
var C = this, binding = new C(); |
|
return binding.to.apply(binding, arguments); |
|
}, |
|
|
|
/** |
|
Creates a new Binding instance and makes it apply in a single direction. |
|
A one-way binding will relay changes on the `from` side object (supplied |
|
as the `from` argument) the `to` side, but not the other way around. |
|
This means that if you change the "to" side directly, the "from" side may have |
|
a different value. |
|
|
|
See `Binding.oneWay`. |
|
|
|
@method oneWay |
|
@param {String} from from path. |
|
@param {Boolean} [flag] (Optional) passing nothing here will make the |
|
binding `oneWay`. You can instead pass `false` to disable `oneWay`, making the |
|
binding two way again. |
|
@return {Ember.Binding} `this` |
|
*/ |
|
oneWay: function(from, flag) { |
|
var C = this, binding = new C(null, from); |
|
return binding.oneWay(flag); |
|
} |
|
|
|
}); |
|
|
|
/** |
|
An `Ember.Binding` connects the properties of two objects so that whenever |
|
the value of one property changes, the other property will be changed also. |
|
|
|
## Automatic Creation of Bindings with `/^*Binding/`-named Properties |
|
|
|
You do not usually create Binding objects directly but instead describe |
|
bindings in your class or object definition using automatic binding |
|
detection. |
|
|
|
Properties ending in a `Binding` suffix will be converted to `Ember.Binding` |
|
instances. The value of this property should be a string representing a path |
|
to another object or a custom binding instanced created using Binding helpers |
|
(see "One Way Bindings"): |
|
|
|
``` |
|
valueBinding: "MyApp.someController.title" |
|
``` |
|
|
|
This will create a binding from `MyApp.someController.title` to the `value` |
|
property of your object instance automatically. Now the two values will be |
|
kept in sync. |
|
|
|
## One Way Bindings |
|
|
|
One especially useful binding customization you can use is the `oneWay()` |
|
helper. This helper tells Ember that you are only interested in |
|
receiving changes on the object you are binding from. For example, if you |
|
are binding to a preference and you want to be notified if the preference |
|
has changed, but your object will not be changing the preference itself, you |
|
could do: |
|
|
|
``` |
|
bigTitlesBinding: Ember.Binding.oneWay("MyApp.preferencesController.bigTitles") |
|
``` |
|
|
|
This way if the value of `MyApp.preferencesController.bigTitles` changes the |
|
`bigTitles` property of your object will change also. However, if you |
|
change the value of your `bigTitles` property, it will not update the |
|
`preferencesController`. |
|
|
|
One way bindings are almost twice as fast to setup and twice as fast to |
|
execute because the binding only has to worry about changes to one side. |
|
|
|
You should consider using one way bindings anytime you have an object that |
|
may be created frequently and you do not intend to change a property; only |
|
to monitor it for changes (such as in the example above). |
|
|
|
## Adding Bindings Manually |
|
|
|
All of the examples above show you how to configure a custom binding, but the |
|
result of these customizations will be a binding template, not a fully active |
|
Binding instance. The binding will actually become active only when you |
|
instantiate the object the binding belongs to. It is useful however, to |
|
understand what actually happens when the binding is activated. |
|
|
|
For a binding to function it must have at least a `from` property and a `to` |
|
property. The `from` property path points to the object/key that you want to |
|
bind from while the `to` path points to the object/key you want to bind to. |
|
|
|
When you define a custom binding, you are usually describing the property |
|
you want to bind from (such as `MyApp.someController.value` in the examples |
|
above). When your object is created, it will automatically assign the value |
|
you want to bind `to` based on the name of your binding key. In the |
|
examples above, during init, Ember objects will effectively call |
|
something like this on your binding: |
|
|
|
```javascript |
|
binding = Ember.Binding.from(this.valueBinding).to("value"); |
|
``` |
|
|
|
This creates a new binding instance based on the template you provide, and |
|
sets the to path to the `value` property of the new object. Now that the |
|
binding is fully configured with a `from` and a `to`, it simply needs to be |
|
connected to become active. This is done through the `connect()` method: |
|
|
|
```javascript |
|
binding.connect(this); |
|
``` |
|
|
|
Note that when you connect a binding you pass the object you want it to be |
|
connected to. This object will be used as the root for both the from and |
|
to side of the binding when inspecting relative paths. This allows the |
|
binding to be automatically inherited by subclassed objects as well. |
|
|
|
Now that the binding is connected, it will observe both the from and to side |
|
and relay changes. |
|
|
|
If you ever needed to do so (you almost never will, but it is useful to |
|
understand this anyway), you could manually create an active binding by |
|
using the `Ember.bind()` helper method. (This is the same method used by |
|
to setup your bindings on objects): |
|
|
|
```javascript |
|
Ember.bind(MyApp.anotherObject, "value", "MyApp.someController.value"); |
|
``` |
|
|
|
Both of these code fragments have the same effect as doing the most friendly |
|
form of binding creation like so: |
|
|
|
```javascript |
|
MyApp.anotherObject = Ember.Object.create({ |
|
valueBinding: "MyApp.someController.value", |
|
|
|
// OTHER CODE FOR THIS OBJECT... |
|
}); |
|
``` |
|
|
|
Ember's built in binding creation method makes it easy to automatically |
|
create bindings for you. You should always use the highest-level APIs |
|
available, even if you understand how it works underneath. |
|
|
|
@class Binding |
|
@namespace Ember |
|
@since Ember 0.9 |
|
*/ |
|
Ember.Binding = Binding; |
|
|
|
|
|
/** |
|
Global helper method to create a new binding. Just pass the root object |
|
along with a `to` and `from` path to create and connect the binding. |
|
|
|
@method bind |
|
@for Ember |
|
@param {Object} obj The root object of the transform. |
|
@param {String} to The path to the 'to' side of the binding. |
|
Must be relative to obj. |
|
@param {String} from The path to the 'from' side of the binding. |
|
Must be relative to obj or a global path. |
|
@return {Ember.Binding} binding instance |
|
*/ |
|
Ember.bind = function(obj, to, from) { |
|
return new Ember.Binding(to, from).connect(obj); |
|
}; |
|
|
|
/** |
|
@method oneWay |
|
@for Ember |
|
@param {Object} obj The root object of the transform. |
|
@param {String} to The path to the 'to' side of the binding. |
|
Must be relative to obj. |
|
@param {String} from The path to the 'from' side of the binding. |
|
Must be relative to obj or a global path. |
|
@return {Ember.Binding} binding instance |
|
*/ |
|
Ember.oneWay = function(obj, to, from) { |
|
return new Ember.Binding(to, from).oneWay().connect(obj); |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-metal |
|
*/ |
|
|
|
var Mixin, REQUIRED, Alias, |
|
a_map = Ember.ArrayPolyfills.map, |
|
a_indexOf = Ember.ArrayPolyfills.indexOf, |
|
a_forEach = Ember.ArrayPolyfills.forEach, |
|
a_slice = [].slice, |
|
o_create = Ember.create, |
|
defineProperty = Ember.defineProperty, |
|
guidFor = Ember.guidFor, |
|
metaFor = Ember.meta, |
|
META_KEY = Ember.META_KEY; |
|
|
|
var expandProperties = Ember.expandProperties; |
|
|
|
function superFunction(){ |
|
var ret, func = this.__nextSuper; |
|
if (func) { |
|
this.__nextSuper = null; |
|
ret = func.apply(this, arguments); |
|
this.__nextSuper = func; |
|
} |
|
return ret; |
|
} |
|
|
|
function mixinsMeta(obj) { |
|
var m = metaFor(obj, true), ret = m.mixins; |
|
if (!ret) { |
|
ret = m.mixins = {}; |
|
} else if (!m.hasOwnProperty('mixins')) { |
|
ret = m.mixins = o_create(ret); |
|
} |
|
return ret; |
|
} |
|
|
|
function initMixin(mixin, args) { |
|
if (args && args.length > 0) { |
|
mixin.mixins = a_map.call(args, function(x) { |
|
if (x instanceof Mixin) { return x; } |
|
|
|
// Note: Manually setup a primitive mixin here. This is the only |
|
// way to actually get a primitive mixin. This way normal creation |
|
// of mixins will give you combined mixins... |
|
var mixin = new Mixin(); |
|
mixin.properties = x; |
|
return mixin; |
|
}); |
|
} |
|
return mixin; |
|
} |
|
|
|
function isMethod(obj) { |
|
return 'function' === typeof obj && |
|
obj.isMethod !== false && |
|
obj !== Boolean && obj !== Object && obj !== Number && obj !== Array && obj !== Date && obj !== String; |
|
} |
|
|
|
var CONTINUE = {}; |
|
|
|
function mixinProperties(mixinsMeta, mixin) { |
|
var guid; |
|
|
|
if (mixin instanceof Mixin) { |
|
guid = guidFor(mixin); |
|
if (mixinsMeta[guid]) { return CONTINUE; } |
|
mixinsMeta[guid] = mixin; |
|
return mixin.properties; |
|
} else { |
|
return mixin; // apply anonymous mixin properties |
|
} |
|
} |
|
|
|
function concatenatedMixinProperties(concatProp, props, values, base) { |
|
var concats; |
|
|
|
// reset before adding each new mixin to pickup concats from previous |
|
concats = values[concatProp] || base[concatProp]; |
|
if (props[concatProp]) { |
|
concats = concats ? concats.concat(props[concatProp]) : props[concatProp]; |
|
} |
|
|
|
return concats; |
|
} |
|
|
|
function giveDescriptorSuper(meta, key, property, values, descs) { |
|
var superProperty; |
|
|
|
// Computed properties override methods, and do not call super to them |
|
if (values[key] === undefined) { |
|
// Find the original descriptor in a parent mixin |
|
superProperty = descs[key]; |
|
} |
|
|
|
// If we didn't find the original descriptor in a parent mixin, find |
|
// it on the original object. |
|
superProperty = superProperty || meta.descs[key]; |
|
|
|
if (!superProperty || !(superProperty instanceof Ember.ComputedProperty)) { |
|
return property; |
|
} |
|
|
|
// Since multiple mixins may inherit from the same parent, we need |
|
// to clone the computed property so that other mixins do not receive |
|
// the wrapped version. |
|
property = o_create(property); |
|
property.func = Ember.wrap(property.func, superProperty.func); |
|
|
|
return property; |
|
} |
|
|
|
function giveMethodSuper(obj, key, method, values, descs) { |
|
var superMethod; |
|
|
|
// Methods overwrite computed properties, and do not call super to them. |
|
if (descs[key] === undefined) { |
|
// Find the original method in a parent mixin |
|
superMethod = values[key]; |
|
} |
|
|
|
// If we didn't find the original value in a parent mixin, find it in |
|
// the original object |
|
superMethod = superMethod || obj[key]; |
|
|
|
// Only wrap the new method if the original method was a function |
|
if ('function' !== typeof superMethod) { |
|
return method; |
|
} |
|
|
|
return Ember.wrap(method, superMethod); |
|
} |
|
|
|
function applyConcatenatedProperties(obj, key, value, values) { |
|
var baseValue = values[key] || obj[key]; |
|
|
|
if (baseValue) { |
|
if ('function' === typeof baseValue.concat) { |
|
return baseValue.concat(value); |
|
} else { |
|
return Ember.makeArray(baseValue).concat(value); |
|
} |
|
} else { |
|
return Ember.makeArray(value); |
|
} |
|
} |
|
|
|
function applyMergedProperties(obj, key, value, values) { |
|
var baseValue = values[key] || obj[key]; |
|
|
|
if (!baseValue) { return value; } |
|
|
|
var newBase = Ember.merge({}, baseValue), |
|
hasFunction = false; |
|
|
|
for (var prop in value) { |
|
if (!value.hasOwnProperty(prop)) { continue; } |
|
|
|
var propValue = value[prop]; |
|
if (isMethod(propValue)) { |
|
// TODO: support for Computed Properties, etc? |
|
hasFunction = true; |
|
newBase[prop] = giveMethodSuper(obj, prop, propValue, baseValue, {}); |
|
} else { |
|
newBase[prop] = propValue; |
|
} |
|
} |
|
|
|
if (hasFunction) { |
|
newBase._super = superFunction; |
|
} |
|
|
|
return newBase; |
|
} |
|
|
|
function addNormalizedProperty(base, key, value, meta, descs, values, concats, mergings) { |
|
if (value instanceof Ember.Descriptor) { |
|
if (value === REQUIRED && descs[key]) { return CONTINUE; } |
|
|
|
// Wrap descriptor function to implement |
|
// __nextSuper() if needed |
|
if (value.func) { |
|
value = giveDescriptorSuper(meta, key, value, values, descs); |
|
} |
|
|
|
descs[key] = value; |
|
values[key] = undefined; |
|
} else { |
|
if ((concats && a_indexOf.call(concats, key) >= 0) || |
|
key === 'concatenatedProperties' || |
|
key === 'mergedProperties') { |
|
value = applyConcatenatedProperties(base, key, value, values); |
|
} else if ((mergings && a_indexOf.call(mergings, key) >= 0)) { |
|
value = applyMergedProperties(base, key, value, values); |
|
} else if (isMethod(value)) { |
|
value = giveMethodSuper(base, key, value, values, descs); |
|
} |
|
|
|
descs[key] = undefined; |
|
values[key] = value; |
|
} |
|
} |
|
|
|
function mergeMixins(mixins, m, descs, values, base, keys) { |
|
var mixin, props, key, concats, mergings, meta; |
|
|
|
function removeKeys(keyName) { |
|
delete descs[keyName]; |
|
delete values[keyName]; |
|
} |
|
|
|
for(var i=0, l=mixins.length; i<l; i++) { |
|
mixin = mixins[i]; |
|
Ember.assert('Expected hash or Mixin instance, got ' + Object.prototype.toString.call(mixin), |
|
typeof mixin === 'object' && mixin !== null && Object.prototype.toString.call(mixin) !== '[object Array]'); |
|
|
|
props = mixinProperties(m, mixin); |
|
if (props === CONTINUE) { continue; } |
|
|
|
if (props) { |
|
meta = metaFor(base); |
|
if (base.willMergeMixin) { base.willMergeMixin(props); } |
|
concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); |
|
mergings = concatenatedMixinProperties('mergedProperties', props, values, base); |
|
|
|
for (key in props) { |
|
if (!props.hasOwnProperty(key)) { continue; } |
|
keys.push(key); |
|
addNormalizedProperty(base, key, props[key], meta, descs, values, concats, mergings); |
|
} |
|
|
|
// manually copy toString() because some JS engines do not enumerate it |
|
if (props.hasOwnProperty('toString')) { base.toString = props.toString; } |
|
} else if (mixin.mixins) { |
|
mergeMixins(mixin.mixins, m, descs, values, base, keys); |
|
if (mixin._without) { a_forEach.call(mixin._without, removeKeys); } |
|
} |
|
} |
|
} |
|
|
|
var IS_BINDING = Ember.IS_BINDING = /^.+Binding$/; |
|
|
|
function detectBinding(obj, key, value, m) { |
|
if (IS_BINDING.test(key)) { |
|
var bindings = m.bindings; |
|
if (!bindings) { |
|
bindings = m.bindings = {}; |
|
} else if (!m.hasOwnProperty('bindings')) { |
|
bindings = m.bindings = o_create(m.bindings); |
|
} |
|
bindings[key] = value; |
|
} |
|
} |
|
|
|
function connectBindings(obj, m) { |
|
// TODO Mixin.apply(instance) should disconnect binding if exists |
|
var bindings = m.bindings, key, binding, to; |
|
if (bindings) { |
|
for (key in bindings) { |
|
binding = bindings[key]; |
|
if (binding) { |
|
to = key.slice(0, -7); // strip Binding off end |
|
if (binding instanceof Ember.Binding) { |
|
binding = binding.copy(); // copy prototypes' instance |
|
binding.to(to); |
|
} else { // binding is string path |
|
binding = new Ember.Binding(to, binding); |
|
} |
|
binding.connect(obj); |
|
obj[key] = binding; |
|
} |
|
} |
|
// mark as applied |
|
m.bindings = {}; |
|
} |
|
} |
|
|
|
function finishPartial(obj, m) { |
|
connectBindings(obj, m || metaFor(obj)); |
|
return obj; |
|
} |
|
|
|
function followAlias(obj, desc, m, descs, values) { |
|
var altKey = desc.methodName, value; |
|
if (descs[altKey] || values[altKey]) { |
|
value = values[altKey]; |
|
desc = descs[altKey]; |
|
} else if (m.descs[altKey]) { |
|
desc = m.descs[altKey]; |
|
value = undefined; |
|
} else { |
|
desc = undefined; |
|
value = obj[altKey]; |
|
} |
|
|
|
return { desc: desc, value: value }; |
|
} |
|
|
|
function updateObserversAndListeners(obj, key, observerOrListener, pathsKey, updateMethod) { |
|
var paths = observerOrListener[pathsKey]; |
|
|
|
if (paths) { |
|
for (var i=0, l=paths.length; i<l; i++) { |
|
Ember[updateMethod](obj, paths[i], null, key); |
|
} |
|
} |
|
} |
|
|
|
function replaceObserversAndListeners(obj, key, observerOrListener) { |
|
var prev = obj[key]; |
|
|
|
if ('function' === typeof prev) { |
|
updateObserversAndListeners(obj, key, prev, '__ember_observesBefore__', 'removeBeforeObserver'); |
|
updateObserversAndListeners(obj, key, prev, '__ember_observes__', 'removeObserver'); |
|
updateObserversAndListeners(obj, key, prev, '__ember_listens__', 'removeListener'); |
|
} |
|
|
|
if ('function' === typeof observerOrListener) { |
|
updateObserversAndListeners(obj, key, observerOrListener, '__ember_observesBefore__', 'addBeforeObserver'); |
|
updateObserversAndListeners(obj, key, observerOrListener, '__ember_observes__', 'addObserver'); |
|
updateObserversAndListeners(obj, key, observerOrListener, '__ember_listens__', 'addListener'); |
|
} |
|
} |
|
|
|
function applyMixin(obj, mixins, partial) { |
|
var descs = {}, values = {}, m = metaFor(obj), |
|
key, value, desc, keys = []; |
|
|
|
obj._super = superFunction; |
|
|
|
// Go through all mixins and hashes passed in, and: |
|
// |
|
// * Handle concatenated properties |
|
// * Handle merged properties |
|
// * Set up _super wrapping if necessary |
|
// * Set up computed property descriptors |
|
// * Copying `toString` in broken browsers |
|
mergeMixins(mixins, mixinsMeta(obj), descs, values, obj, keys); |
|
|
|
for(var i = 0, l = keys.length; i < l; i++) { |
|
key = keys[i]; |
|
if (key === 'constructor' || !values.hasOwnProperty(key)) { continue; } |
|
|
|
desc = descs[key]; |
|
value = values[key]; |
|
|
|
if (desc === REQUIRED) { continue; } |
|
|
|
while (desc && desc instanceof Alias) { |
|
var followed = followAlias(obj, desc, m, descs, values); |
|
desc = followed.desc; |
|
value = followed.value; |
|
} |
|
|
|
if (desc === undefined && value === undefined) { continue; } |
|
|
|
replaceObserversAndListeners(obj, key, value); |
|
detectBinding(obj, key, value, m); |
|
defineProperty(obj, key, desc, value, m); |
|
} |
|
|
|
if (!partial) { // don't apply to prototype |
|
finishPartial(obj, m); |
|
} |
|
|
|
return obj; |
|
} |
|
|
|
/** |
|
@method mixin |
|
@for Ember |
|
@param obj |
|
@param mixins* |
|
@return obj |
|
*/ |
|
Ember.mixin = function(obj) { |
|
var args = a_slice.call(arguments, 1); |
|
applyMixin(obj, args, false); |
|
return obj; |
|
}; |
|
|
|
/** |
|
The `Ember.Mixin` class allows you to create mixins, whose properties can be |
|
added to other classes. For instance, |
|
|
|
```javascript |
|
App.Editable = Ember.Mixin.create({ |
|
edit: function() { |
|
console.log('starting to edit'); |
|
this.set('isEditing', true); |
|
}, |
|
isEditing: false |
|
}); |
|
|
|
// Mix mixins into classes by passing them as the first arguments to |
|
// .extend. |
|
App.CommentView = Ember.View.extend(App.Editable, { |
|
template: Ember.Handlebars.compile('{{#if view.isEditing}}...{{else}}...{{/if}}') |
|
}); |
|
|
|
commentView = App.CommentView.create(); |
|
commentView.edit(); // outputs 'starting to edit' |
|
``` |
|
|
|
Note that Mixins are created with `Ember.Mixin.create`, not |
|
`Ember.Mixin.extend`. |
|
|
|
Note that mixins extend a constructor's prototype so arrays and object literals |
|
defined as properties will be shared amongst objects that implement the mixin. |
|
If you want to define a property in a mixin that is not shared, you can define |
|
it either as a computed property or have it be created on initialization of the object. |
|
|
|
```javascript |
|
//filters array will be shared amongst any object implementing mixin |
|
App.Filterable = Ember.Mixin.create({ |
|
filters: Ember.A() |
|
}); |
|
|
|
//filters will be a separate array for every object implementing the mixin |
|
App.Filterable = Ember.Mixin.create({ |
|
filters: Ember.computed(function(){return Ember.A();}) |
|
}); |
|
|
|
//filters will be created as a separate array during the object's initialization |
|
App.Filterable = Ember.Mixin.create({ |
|
init: function() { |
|
this._super(); |
|
this.set("filters", Ember.A()); |
|
} |
|
}); |
|
``` |
|
|
|
@class Mixin |
|
@namespace Ember |
|
*/ |
|
Ember.Mixin = function() { return initMixin(this, arguments); }; |
|
|
|
Mixin = Ember.Mixin; |
|
|
|
Mixin.prototype = { |
|
properties: null, |
|
mixins: null, |
|
ownerConstructor: null |
|
}; |
|
|
|
Mixin._apply = applyMixin; |
|
|
|
Mixin.applyPartial = function(obj) { |
|
var args = a_slice.call(arguments, 1); |
|
return applyMixin(obj, args, true); |
|
}; |
|
|
|
Mixin.finishPartial = finishPartial; |
|
|
|
Ember.anyUnprocessedMixins = false; |
|
|
|
/** |
|
@method create |
|
@static |
|
@param arguments* |
|
*/ |
|
Mixin.create = function() { |
|
Ember.anyUnprocessedMixins = true; |
|
var M = this; |
|
return initMixin(new M(), arguments); |
|
}; |
|
|
|
var MixinPrototype = Mixin.prototype; |
|
|
|
/** |
|
@method reopen |
|
@param arguments* |
|
*/ |
|
MixinPrototype.reopen = function() { |
|
var mixin, tmp; |
|
|
|
if (this.properties) { |
|
mixin = Mixin.create(); |
|
mixin.properties = this.properties; |
|
delete this.properties; |
|
this.mixins = [mixin]; |
|
} else if (!this.mixins) { |
|
this.mixins = []; |
|
} |
|
|
|
var len = arguments.length, mixins = this.mixins, idx; |
|
|
|
for(idx=0; idx < len; idx++) { |
|
mixin = arguments[idx]; |
|
Ember.assert('Expected hash or Mixin instance, got ' + Object.prototype.toString.call(mixin), |
|
typeof mixin === 'object' && mixin !== null && Object.prototype.toString.call(mixin) !== '[object Array]'); |
|
|
|
if (mixin instanceof Mixin) { |
|
mixins.push(mixin); |
|
} else { |
|
tmp = Mixin.create(); |
|
tmp.properties = mixin; |
|
mixins.push(tmp); |
|
} |
|
} |
|
|
|
return this; |
|
}; |
|
|
|
/** |
|
@method apply |
|
@param obj |
|
@return applied object |
|
*/ |
|
MixinPrototype.apply = function(obj) { |
|
return applyMixin(obj, [this], false); |
|
}; |
|
|
|
MixinPrototype.applyPartial = function(obj) { |
|
return applyMixin(obj, [this], true); |
|
}; |
|
|
|
function _detect(curMixin, targetMixin, seen) { |
|
var guid = guidFor(curMixin); |
|
|
|
if (seen[guid]) { return false; } |
|
seen[guid] = true; |
|
|
|
if (curMixin === targetMixin) { return true; } |
|
var mixins = curMixin.mixins, loc = mixins ? mixins.length : 0; |
|
while (--loc >= 0) { |
|
if (_detect(mixins[loc], targetMixin, seen)) { return true; } |
|
} |
|
return false; |
|
} |
|
|
|
/** |
|
@method detect |
|
@param obj |
|
@return {Boolean} |
|
*/ |
|
MixinPrototype.detect = function(obj) { |
|
if (!obj) { return false; } |
|
if (obj instanceof Mixin) { return _detect(obj, this, {}); } |
|
var m = obj[META_KEY], |
|
mixins = m && m.mixins; |
|
if (mixins) { |
|
return !!mixins[guidFor(this)]; |
|
} |
|
return false; |
|
}; |
|
|
|
MixinPrototype.without = function() { |
|
var ret = new Mixin(this); |
|
ret._without = a_slice.call(arguments); |
|
return ret; |
|
}; |
|
|
|
function _keys(ret, mixin, seen) { |
|
if (seen[guidFor(mixin)]) { return; } |
|
seen[guidFor(mixin)] = true; |
|
|
|
if (mixin.properties) { |
|
var props = mixin.properties; |
|
for (var key in props) { |
|
if (props.hasOwnProperty(key)) { ret[key] = true; } |
|
} |
|
} else if (mixin.mixins) { |
|
a_forEach.call(mixin.mixins, function(x) { _keys(ret, x, seen); }); |
|
} |
|
} |
|
|
|
MixinPrototype.keys = function() { |
|
var keys = {}, seen = {}, ret = []; |
|
_keys(keys, this, seen); |
|
for(var key in keys) { |
|
if (keys.hasOwnProperty(key)) { ret.push(key); } |
|
} |
|
return ret; |
|
}; |
|
|
|
// returns the mixins currently applied to the specified object |
|
// TODO: Make Ember.mixin |
|
Mixin.mixins = function(obj) { |
|
var m = obj[META_KEY], |
|
mixins = m && m.mixins, ret = []; |
|
|
|
if (!mixins) { return ret; } |
|
|
|
for (var key in mixins) { |
|
var mixin = mixins[key]; |
|
|
|
// skip primitive mixins since these are always anonymous |
|
if (!mixin.properties) { ret.push(mixin); } |
|
} |
|
|
|
return ret; |
|
}; |
|
|
|
REQUIRED = new Ember.Descriptor(); |
|
REQUIRED.toString = function() { return '(Required Property)'; }; |
|
|
|
/** |
|
Denotes a required property for a mixin |
|
|
|
@method required |
|
@for Ember |
|
*/ |
|
Ember.required = function() { |
|
return REQUIRED; |
|
}; |
|
|
|
Alias = function(methodName) { |
|
this.methodName = methodName; |
|
}; |
|
Alias.prototype = new Ember.Descriptor(); |
|
|
|
/** |
|
Makes a method available via an additional name. |
|
|
|
```javascript |
|
App.Person = Ember.Object.extend({ |
|
name: function() { |
|
return 'Tomhuda Katzdale'; |
|
}, |
|
moniker: Ember.aliasMethod('name') |
|
}); |
|
|
|
var goodGuy = App.Person.create() |
|
``` |
|
|
|
@method aliasMethod |
|
@for Ember |
|
@param {String} methodName name of the method to alias |
|
@return {Ember.Descriptor} |
|
*/ |
|
Ember.aliasMethod = function(methodName) { |
|
return new Alias(methodName); |
|
}; |
|
|
|
// .......................................................... |
|
// OBSERVER HELPER |
|
// |
|
|
|
/** |
|
Specify a method that observes property changes. |
|
|
|
```javascript |
|
Ember.Object.extend({ |
|
valueObserver: Ember.observer('value', function() { |
|
// Executes whenever the "value" property changes |
|
}) |
|
}); |
|
``` |
|
|
|
In the future this method may become asynchronous. If you want to ensure |
|
synchronous behavior, use `immediateObserver`. |
|
|
|
Also available as `Function.prototype.observes` if prototype extensions are |
|
enabled. |
|
|
|
@method observer |
|
@for Ember |
|
@param {String} propertyNames* |
|
@param {Function} func |
|
@return func |
|
*/ |
|
Ember.observer = function() { |
|
var func = a_slice.call(arguments, -1)[0]; |
|
var paths; |
|
|
|
var addWatchedProperty = function (path) { paths.push(path); }; |
|
var _paths = a_slice.call(arguments, 0, -1); |
|
|
|
if (typeof func !== "function") { |
|
// revert to old, soft-deprecated argument ordering |
|
|
|
func = arguments[0]; |
|
_paths = a_slice.call(arguments, 1); |
|
} |
|
|
|
paths = []; |
|
|
|
for (var i=0; i<_paths.length; ++i) { |
|
expandProperties(_paths[i], addWatchedProperty); |
|
} |
|
|
|
if (typeof func !== "function") { |
|
throw new Ember.Error("Ember.observer called without a function"); |
|
} |
|
|
|
func.__ember_observes__ = paths; |
|
return func; |
|
}; |
|
|
|
/** |
|
Specify a method that observes property changes. |
|
|
|
```javascript |
|
Ember.Object.extend({ |
|
valueObserver: Ember.immediateObserver('value', function() { |
|
// Executes whenever the "value" property changes |
|
}) |
|
}); |
|
``` |
|
|
|
In the future, `Ember.observer` may become asynchronous. In this event, |
|
`Ember.immediateObserver` will maintain the synchronous behavior. |
|
|
|
Also available as `Function.prototype.observesImmediately` if prototype extensions are |
|
enabled. |
|
|
|
@method immediateObserver |
|
@for Ember |
|
@param {String} propertyNames* |
|
@param {Function} func |
|
@return func |
|
*/ |
|
Ember.immediateObserver = function() { |
|
for (var i=0, l=arguments.length; i<l; i++) { |
|
var arg = arguments[i]; |
|
Ember.assert("Immediate observers must observe internal properties only, not properties on other objects.", typeof arg !== "string" || arg.indexOf('.') === -1); |
|
} |
|
|
|
return Ember.observer.apply(this, arguments); |
|
}; |
|
|
|
/** |
|
When observers fire, they are called with the arguments `obj`, `keyName`. |
|
|
|
Note, `@each.property` observer is called per each add or replace of an element |
|
and it's not called with a specific enumeration item. |
|
|
|
A `beforeObserver` fires before a property changes. |
|
|
|
A `beforeObserver` is an alternative form of `.observesBefore()`. |
|
|
|
```javascript |
|
App.PersonView = Ember.View.extend({ |
|
|
|
friends: [{ name: 'Tom' }, { name: 'Stefan' }, { name: 'Kris' }], |
|
|
|
valueWillChange: Ember.beforeObserver('content.value', function(obj, keyName) { |
|
this.changingFrom = obj.get(keyName); |
|
}), |
|
|
|
valueDidChange: Ember.observer('content.value', function(obj, keyName) { |
|
// only run if updating a value already in the DOM |
|
if (this.get('state') === 'inDOM') { |
|
var color = obj.get(keyName) > this.changingFrom ? 'green' : 'red'; |
|
// logic |
|
} |
|
}), |
|
|
|
friendsDidChange: Ember.observer('friends.@each.name', function(obj, keyName) { |
|
// some logic |
|
// obj.get(keyName) returns friends array |
|
}) |
|
}); |
|
``` |
|
|
|
Also available as `Function.prototype.observesBefore` if prototype extensions are |
|
enabled. |
|
|
|
@method beforeObserver |
|
@for Ember |
|
@param {String} propertyNames* |
|
@param {Function} func |
|
@return func |
|
*/ |
|
Ember.beforeObserver = function() { |
|
var func = a_slice.call(arguments, -1)[0]; |
|
var paths; |
|
|
|
var addWatchedProperty = function(path) { paths.push(path); }; |
|
|
|
var _paths = a_slice.call(arguments, 0, -1); |
|
|
|
if (typeof func !== "function") { |
|
// revert to old, soft-deprecated argument ordering |
|
|
|
func = arguments[0]; |
|
_paths = a_slice.call(arguments, 1); |
|
} |
|
|
|
paths = []; |
|
|
|
for (var i=0; i<_paths.length; ++i) { |
|
expandProperties(_paths[i], addWatchedProperty); |
|
} |
|
|
|
if (typeof func !== "function") { |
|
throw new Ember.Error("Ember.beforeObserver called without a function"); |
|
} |
|
|
|
func.__ember_observesBefore__ = paths; |
|
return func; |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
// Provides a way to register library versions with ember. |
|
var forEach = Ember.EnumerableUtils.forEach, |
|
indexOf = Ember.EnumerableUtils.indexOf; |
|
|
|
Ember.libraries = function() { |
|
var libraries = []; |
|
var coreLibIndex = 0; |
|
|
|
var getLibrary = function(name) { |
|
for (var i = 0; i < libraries.length; i++) { |
|
if (libraries[i].name === name) { |
|
return libraries[i]; |
|
} |
|
} |
|
}; |
|
|
|
libraries.register = function(name, version) { |
|
if (!getLibrary(name)) { |
|
libraries.push({name: name, version: version}); |
|
} |
|
}; |
|
|
|
libraries.registerCoreLibrary = function(name, version) { |
|
if (!getLibrary(name)) { |
|
libraries.splice(coreLibIndex++, 0, {name: name, version: version}); |
|
} |
|
}; |
|
|
|
libraries.deRegister = function(name) { |
|
var lib = getLibrary(name); |
|
if (lib) libraries.splice(indexOf(libraries, lib), 1); |
|
}; |
|
|
|
libraries.each = function (callback) { |
|
forEach(libraries, function(lib) { |
|
callback(lib.name, lib.version); |
|
}); |
|
}; |
|
|
|
return libraries; |
|
}(); |
|
|
|
Ember.libraries.registerCoreLibrary('Ember', Ember.VERSION); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
Ember Metal |
|
|
|
@module ember |
|
@submodule ember-metal |
|
*/ |
|
|
|
})(); |
|
|
|
(function() { |
|
/** |
|
@class RSVP |
|
@module RSVP |
|
*/ |
|
define("rsvp/all", |
|
["./promise","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
var Promise = __dependency1__["default"]; |
|
|
|
/** |
|
This is a convenient alias for `RSVP.Promise.all`. |
|
|
|
@method all |
|
@for RSVP |
|
@param {Array} array Array of promises. |
|
@param {String} label An optional label. This is useful |
|
for tooling. |
|
@static |
|
*/ |
|
__exports__["default"] = function all(array, label) { |
|
return Promise.all(array, label); |
|
}; |
|
}); |
|
define("rsvp/all_settled", |
|
["./promise","./utils","exports"], |
|
function(__dependency1__, __dependency2__, __exports__) { |
|
"use strict"; |
|
var Promise = __dependency1__["default"]; |
|
var isArray = __dependency2__.isArray; |
|
var isNonThenable = __dependency2__.isNonThenable; |
|
|
|
/** |
|
`RSVP.allSettled` is similar to `RSVP.all`, but instead of implementing |
|
a fail-fast method, it waits until all the promises have returned and |
|
shows you all the results. This is useful if you want to handle multiple |
|
promises' failure states together as a set. |
|
|
|
Returns a promise that is fulfilled when all the given promises have been |
|
settled. The return promise is fulfilled with an array of the states of |
|
the promises passed into the `promises` array argument. |
|
|
|
Each state object will either indicate fulfillment or rejection, and |
|
provide the corresponding value or reason. The states will take one of |
|
the following formats: |
|
|
|
```javascript |
|
{ state: 'fulfilled', value: value } |
|
or |
|
{ state: 'rejected', reason: reason } |
|
``` |
|
|
|
Example: |
|
|
|
```javascript |
|
var promise1 = RSVP.Promise.resolve(1); |
|
var promise2 = RSVP.Promise.reject(new Error('2')); |
|
var promise3 = RSVP.Promise.reject(new Error('3')); |
|
var promises = [ promise1, promise2, promise3 ]; |
|
|
|
RSVP.allSettled(promises).then(function(array){ |
|
// array == [ |
|
// { state: 'fulfilled', value: 1 }, |
|
// { state: 'rejected', reason: Error }, |
|
// { state: 'rejected', reason: Error } |
|
// ] |
|
// Note that for the second item, reason.message will be "2", and for the |
|
// third item, reason.message will be "3". |
|
}, function(error) { |
|
// Not run. (This block would only be called if allSettled had failed, |
|
// for instance if passed an incorrect argument type.) |
|
}); |
|
``` |
|
|
|
@method allSettled |
|
@for RSVP |
|
@param {Array} promises |
|
@param {String} label - optional string that describes the promise. |
|
Useful for tooling. |
|
@return {Promise} promise that is fulfilled with an array of the settled |
|
states of the constituent promises. |
|
@static |
|
*/ |
|
|
|
__exports__["default"] = function allSettled(entries, label) { |
|
return new Promise(function(resolve, reject) { |
|
if (!isArray(entries)) { |
|
throw new TypeError('You must pass an array to allSettled.'); |
|
} |
|
|
|
var remaining = entries.length; |
|
var entry; |
|
|
|
if (remaining === 0) { |
|
resolve([]); |
|
return; |
|
} |
|
|
|
var results = new Array(remaining); |
|
|
|
function fulfilledResolver(index) { |
|
return function(value) { |
|
resolveAll(index, fulfilled(value)); |
|
}; |
|
} |
|
|
|
function rejectedResolver(index) { |
|
return function(reason) { |
|
resolveAll(index, rejected(reason)); |
|
}; |
|
} |
|
|
|
function resolveAll(index, value) { |
|
results[index] = value; |
|
if (--remaining === 0) { |
|
resolve(results); |
|
} |
|
} |
|
|
|
for (var index = 0; index < entries.length; index++) { |
|
entry = entries[index]; |
|
|
|
if (isNonThenable(entry)) { |
|
resolveAll(index, fulfilled(entry)); |
|
} else { |
|
Promise.cast(entry).then(fulfilledResolver(index), rejectedResolver(index)); |
|
} |
|
} |
|
}, label); |
|
}; |
|
|
|
function fulfilled(value) { |
|
return { state: 'fulfilled', value: value }; |
|
} |
|
|
|
function rejected(reason) { |
|
return { state: 'rejected', reason: reason }; |
|
} |
|
}); |
|
define("rsvp/config", |
|
["./events","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
var EventTarget = __dependency1__["default"]; |
|
|
|
var config = { |
|
instrument: false |
|
}; |
|
|
|
EventTarget.mixin(config); |
|
|
|
function configure(name, value) { |
|
if (name === 'onerror') { |
|
// handle for legacy users that expect the actual |
|
// error to be passed to their function added via |
|
// `RSVP.configure('onerror', someFunctionHere);` |
|
config.on('error', value); |
|
return; |
|
} |
|
|
|
if (arguments.length === 2) { |
|
config[name] = value; |
|
} else { |
|
return config[name]; |
|
} |
|
} |
|
|
|
__exports__.config = config; |
|
__exports__.configure = configure; |
|
}); |
|
define("rsvp/defer", |
|
["./promise","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
var Promise = __dependency1__["default"]; |
|
|
|
/** |
|
`RSVP.defer` returns an object similar to jQuery's `$.Deferred`. |
|
`RSVP.defer` should be used when porting over code reliant on `$.Deferred`'s |
|
interface. New code should use the `RSVP.Promise` constructor instead. |
|
|
|
The object returned from `RSVP.defer` is a plain object with three properties: |
|
|
|
* promise - an `RSVP.Promise`. |
|
* reject - a function that causes the `promise` property on this object to |
|
become rejected |
|
* resolve - a function that causes the `promise` property on this object to |
|
become fulfilled. |
|
|
|
Example: |
|
|
|
```javascript |
|
var deferred = RSVP.defer(); |
|
|
|
deferred.resolve("Success!"); |
|
|
|
defered.promise.then(function(value){ |
|
// value here is "Success!" |
|
}); |
|
``` |
|
|
|
@method defer |
|
@for RSVP |
|
@param {String} label optional string for labeling the promise. |
|
Useful for tooling. |
|
@return {Object} |
|
*/ |
|
|
|
__exports__["default"] = function defer(label) { |
|
var deferred = { }; |
|
|
|
deferred.promise = new Promise(function(resolve, reject) { |
|
deferred.resolve = resolve; |
|
deferred.reject = reject; |
|
}, label); |
|
|
|
return deferred; |
|
}; |
|
}); |
|
define("rsvp/events", |
|
["exports"], |
|
function(__exports__) { |
|
"use strict"; |
|
var indexOf = function(callbacks, callback) { |
|
for (var i=0, l=callbacks.length; i<l; i++) { |
|
if (callbacks[i] === callback) { return i; } |
|
} |
|
|
|
return -1; |
|
}; |
|
|
|
var callbacksFor = function(object) { |
|
var callbacks = object._promiseCallbacks; |
|
|
|
if (!callbacks) { |
|
callbacks = object._promiseCallbacks = {}; |
|
} |
|
|
|
return callbacks; |
|
}; |
|
|
|
/** |
|
@class RSVP.EventTarget |
|
*/ |
|
__exports__["default"] = { |
|
|
|
/** |
|
`RSVP.EventTarget.mixin` extends an object with EventTarget methods. For |
|
Example: |
|
|
|
```javascript |
|
var object = {}; |
|
|
|
RSVP.EventTarget.mixin(object); |
|
|
|
object.on("finished", function(event) { |
|
// handle event |
|
}); |
|
|
|
object.trigger("finished", { detail: value }); |
|
``` |
|
|
|
`EventTarget.mixin` also works with prototypes: |
|
|
|
```javascript |
|
var Person = function() {}; |
|
RSVP.EventTarget.mixin(Person.prototype); |
|
|
|
var yehuda = new Person(); |
|
var tom = new Person(); |
|
|
|
yehuda.on("poke", function(event) { |
|
console.log("Yehuda says OW"); |
|
}); |
|
|
|
tom.on("poke", function(event) { |
|
console.log("Tom says OW"); |
|
}); |
|
|
|
yehuda.trigger("poke"); |
|
tom.trigger("poke"); |
|
``` |
|
|
|
@method mixin |
|
@param {Object} object object to extend with EventTarget methods |
|
@private |
|
*/ |
|
mixin: function(object) { |
|
object.on = this.on; |
|
object.off = this.off; |
|
object.trigger = this.trigger; |
|
object._promiseCallbacks = undefined; |
|
return object; |
|
}, |
|
|
|
/** |
|
Registers a callback to be executed when `eventName` is triggered |
|
|
|
```javascript |
|
object.on('event', function(eventInfo){ |
|
// handle the event |
|
}); |
|
|
|
object.trigger('event'); |
|
``` |
|
|
|
@method on |
|
@param {String} eventName name of the event to listen for |
|
@param {Function} callback function to be called when the event is triggered. |
|
@private |
|
*/ |
|
on: function(eventName, callback) { |
|
var allCallbacks = callbacksFor(this), callbacks; |
|
|
|
callbacks = allCallbacks[eventName]; |
|
|
|
if (!callbacks) { |
|
callbacks = allCallbacks[eventName] = []; |
|
} |
|
|
|
if (indexOf(callbacks, callback) === -1) { |
|
callbacks.push(callback); |
|
} |
|
}, |
|
|
|
/** |
|
You can use `off` to stop firing a particular callback for an event: |
|
|
|
```javascript |
|
function doStuff() { // do stuff! } |
|
object.on('stuff', doStuff); |
|
|
|
object.trigger('stuff'); // doStuff will be called |
|
|
|
// Unregister ONLY the doStuff callback |
|
object.off('stuff', doStuff); |
|
object.trigger('stuff'); // doStuff will NOT be called |
|
``` |
|
|
|
If you don't pass a `callback` argument to `off`, ALL callbacks for the |
|
event will not be executed when the event fires. For example: |
|
|
|
```javascript |
|
var callback1 = function(){}; |
|
var callback2 = function(){}; |
|
|
|
object.on('stuff', callback1); |
|
object.on('stuff', callback2); |
|
|
|
object.trigger('stuff'); // callback1 and callback2 will be executed. |
|
|
|
object.off('stuff'); |
|
object.trigger('stuff'); // callback1 and callback2 will not be executed! |
|
``` |
|
|
|
@method off |
|
@param {String} eventName event to stop listening to |
|
@param {Function} callback optional argument. If given, only the function |
|
given will be removed from the event's callback queue. If no `callback` |
|
argument is given, all callbacks will be removed from the event's callback |
|
queue. |
|
@private |
|
|
|
*/ |
|
off: function(eventName, callback) { |
|
var allCallbacks = callbacksFor(this), callbacks, index; |
|
|
|
if (!callback) { |
|
allCallbacks[eventName] = []; |
|
return; |
|
} |
|
|
|
callbacks = allCallbacks[eventName]; |
|
|
|
index = indexOf(callbacks, callback); |
|
|
|
if (index !== -1) { callbacks.splice(index, 1); } |
|
}, |
|
|
|
/** |
|
Use `trigger` to fire custom events. For example: |
|
|
|
```javascript |
|
object.on('foo', function(){ |
|
console.log('foo event happened!'); |
|
}); |
|
object.trigger('foo'); |
|
// 'foo event happened!' logged to the console |
|
``` |
|
|
|
You can also pass a value as a second argument to `trigger` that will be |
|
passed as an argument to all event listeners for the event: |
|
|
|
```javascript |
|
object.on('foo', function(value){ |
|
console.log(value.name); |
|
}); |
|
|
|
object.trigger('foo', { name: 'bar' }); |
|
// 'bar' logged to the console |
|
``` |
|
|
|
@method trigger |
|
@param {String} eventName name of the event to be triggered |
|
@param {Any} options optional value to be passed to any event handlers for |
|
the given `eventName` |
|
@private |
|
*/ |
|
trigger: function(eventName, options) { |
|
var allCallbacks = callbacksFor(this), |
|
callbacks, callbackTuple, callback, binding; |
|
|
|
if (callbacks = allCallbacks[eventName]) { |
|
// Don't cache the callbacks.length since it may grow |
|
for (var i=0; i<callbacks.length; i++) { |
|
callback = callbacks[i]; |
|
|
|
callback(options); |
|
} |
|
} |
|
} |
|
}; |
|
}); |
|
define("rsvp/filter", |
|
["./all","./map","./utils","exports"], |
|
function(__dependency1__, __dependency2__, __dependency3__, __exports__) { |
|
"use strict"; |
|
var all = __dependency1__["default"]; |
|
var map = __dependency2__["default"]; |
|
var isFunction = __dependency3__.isFunction; |
|
var isArray = __dependency3__.isArray; |
|
|
|
/** |
|
`RSVP.filter` is similar to JavaScript's native `filter` method, except that it |
|
waits for all promises to become fulfilled before running the `filterFn` on |
|
each item in given to `promises`. `RSVP.filter` returns a promise that will |
|
become fulfilled with the result of running `filterFn` on the values the |
|
promises become fulfilled with. |
|
|
|
For example: |
|
|
|
```javascript |
|
|
|
var promise1 = RSVP.resolve(1); |
|
var promise2 = RSVP.resolve(2); |
|
var promise3 = RSVP.resolve(3); |
|
|
|
var filterFn = function(item){ |
|
return item > 1; |
|
}; |
|
|
|
RSVP.filter(promises, filterFn).then(function(result){ |
|
// result is [ 2, 3 ] |
|
}); |
|
``` |
|
|
|
If any of the `promises` given to `RSVP.filter` are rejected, the first promise |
|
that is rejected will be given as an argument to the returned promise's |
|
rejection handler. For example: |
|
|
|
```javascript |
|
var promise1 = RSVP.resolve(1); |
|
var promise2 = RSVP.reject(new Error("2")); |
|
var promise3 = RSVP.reject(new Error("3")); |
|
var promises = [ promise1, promise2, promise3 ]; |
|
|
|
var filterFn = function(item){ |
|
return item > 1; |
|
}; |
|
|
|
RSVP.filter(promises, filterFn).then(function(array){ |
|
// Code here never runs because there are rejected promises! |
|
}, function(reason) { |
|
// reason.message === "2" |
|
}); |
|
``` |
|
|
|
`RSVP.filter` will also wait for any promises returned from `filterFn`. |
|
For instance, you may want to fetch a list of users then return a subset |
|
of those users based on some asynchronous operation: |
|
|
|
```javascript |
|
|
|
var alice = { name: 'alice' }; |
|
var bob = { name: 'bob' }; |
|
var users = [ alice, bob ]; |
|
|
|
var promises = users.map(function(user){ |
|
return RSVP.resolve(user); |
|
}); |
|
|
|
var filterFn = function(user){ |
|
// Here, Alice has permissions to create a blog post, but Bob does not. |
|
return getPrivilegesForUser(user).then(function(privs){ |
|
return privs.can_create_blog_post === true; |
|
}); |
|
}; |
|
RSVP.filter(promises, filterFn).then(function(users){ |
|
// true, because the server told us only Alice can create a blog post. |
|
users.length === 1; |
|
// false, because Alice is the only user present in `users` |
|
users[0] === bob; |
|
}); |
|
``` |
|
|
|
@method filter |
|
@for RSVP |
|
@param {Array} promises |
|
@param {Function} filterFn - function to be called on each resolved value to |
|
filter the final results. |
|
@param {String} label optional string describing the promise. Useful for |
|
tooling. |
|
@return {Promise} |
|
*/ |
|
function filter(promises, filterFn, label) { |
|
return all(promises, label).then(function(values){ |
|
if (!isArray(promises)) { |
|
throw new TypeError('You must pass an array to filter.'); |
|
} |
|
|
|
if (!isFunction(filterFn)){ |
|
throw new TypeError("You must pass a function to filter's second argument."); |
|
} |
|
|
|
return map(promises, filterFn, label).then(function(filterResults){ |
|
var i, |
|
valuesLen = values.length, |
|
filtered = []; |
|
|
|
for (i = 0; i < valuesLen; i++){ |
|
if(filterResults[i]) filtered.push(values[i]); |
|
} |
|
return filtered; |
|
}); |
|
}); |
|
} |
|
|
|
__exports__["default"] = filter; |
|
}); |
|
define("rsvp/hash", |
|
["./promise","./utils","exports"], |
|
function(__dependency1__, __dependency2__, __exports__) { |
|
"use strict"; |
|
var Promise = __dependency1__["default"]; |
|
var isNonThenable = __dependency2__.isNonThenable; |
|
var keysOf = __dependency2__.keysOf; |
|
|
|
/** |
|
`RSVP.hash` is similar to `RSVP.all`, but takes an object instead of an array |
|
for its `promises` argument. |
|
|
|
Returns a promise that is fulfilled when all the given promises have been |
|
fulfilled, or rejected if any of them become rejected. The returned promise |
|
is fulfilled with a hash that has the same key names as the `promises` object |
|
argument. If any of the values in the object are not promises, they will |
|
simply be copied over to the fulfilled object. |
|
|
|
Example: |
|
|
|
```javascript |
|
var promises = { |
|
myPromise: RSVP.resolve(1), |
|
yourPromise: RSVP.resolve(2), |
|
theirPromise: RSVP.resolve(3), |
|
notAPromise: 4 |
|
}; |
|
|
|
RSVP.hash(promises).then(function(hash){ |
|
// hash here is an object that looks like: |
|
// { |
|
// myPromise: 1, |
|
// yourPromise: 2, |
|
// theirPromise: 3, |
|
// notAPromise: 4 |
|
// } |
|
}); |
|
```` |
|
|
|
If any of the `promises` given to `RSVP.hash` are rejected, the first promise |
|
that is rejected will be given as the reason to the rejection handler. |
|
|
|
Example: |
|
|
|
```javascript |
|
var promises = { |
|
myPromise: RSVP.resolve(1), |
|
rejectedPromise: RSVP.reject(new Error("rejectedPromise")), |
|
anotherRejectedPromise: RSVP.reject(new Error("anotherRejectedPromise")), |
|
}; |
|
|
|
RSVP.hash(promises).then(function(hash){ |
|
// Code here never runs because there are rejected promises! |
|
}, function(reason) { |
|
// reason.message === "rejectedPromise" |
|
}); |
|
``` |
|
|
|
An important note: `RSVP.hash` is intended for plain JavaScript objects that |
|
are just a set of keys and values. `RSVP.hash` will NOT preserve prototype |
|
chains. |
|
|
|
Example: |
|
|
|
```javascript |
|
function MyConstructor(){ |
|
this.example = RSVP.resolve("Example"); |
|
} |
|
|
|
MyConstructor.prototype = { |
|
protoProperty: RSVP.resolve("Proto Property") |
|
}; |
|
|
|
var myObject = new MyConstructor(); |
|
|
|
RSVP.hash(myObject).then(function(hash){ |
|
// protoProperty will not be present, instead you will just have an |
|
// object that looks like: |
|
// { |
|
// example: "Example" |
|
// } |
|
// |
|
// hash.hasOwnProperty('protoProperty'); // false |
|
// 'undefined' === typeof hash.protoProperty |
|
}); |
|
``` |
|
|
|
@method hash |
|
@for RSVP |
|
@param {Object} promises |
|
@param {String} label optional string that describes the promise. |
|
Useful for tooling. |
|
@return {Promise} promise that is fulfilled when all properties of `promises` |
|
have been fulfilled, or rejected if any of them become rejected. |
|
@static |
|
*/ |
|
__exports__["default"] = function hash(object, label) { |
|
return new Promise(function(resolve, reject){ |
|
var results = {}; |
|
var keys = keysOf(object); |
|
var remaining = keys.length; |
|
var entry, property; |
|
|
|
if (remaining === 0) { |
|
resolve(results); |
|
return; |
|
} |
|
|
|
function fulfilledTo(property) { |
|
return function(value) { |
|
results[property] = value; |
|
if (--remaining === 0) { |
|
resolve(results); |
|
} |
|
}; |
|
} |
|
|
|
function onRejection(reason) { |
|
remaining = 0; |
|
reject(reason); |
|
} |
|
|
|
for (var i = 0; i < keys.length; i++) { |
|
property = keys[i]; |
|
entry = object[property]; |
|
|
|
if (isNonThenable(entry)) { |
|
results[property] = entry; |
|
if (--remaining === 0) { |
|
resolve(results); |
|
} |
|
} else { |
|
Promise.cast(entry).then(fulfilledTo(property), onRejection); |
|
} |
|
} |
|
}); |
|
}; |
|
}); |
|
define("rsvp/instrument", |
|
["./config","./utils","exports"], |
|
function(__dependency1__, __dependency2__, __exports__) { |
|
"use strict"; |
|
var config = __dependency1__.config; |
|
var now = __dependency2__.now; |
|
|
|
__exports__["default"] = function instrument(eventName, promise, child) { |
|
// instrumentation should not disrupt normal usage. |
|
try { |
|
config.trigger(eventName, { |
|
guid: promise._guidKey + promise._id, |
|
eventName: eventName, |
|
detail: promise._detail, |
|
childGuid: child && promise._guidKey + child._id, |
|
label: promise._label, |
|
timeStamp: now(), |
|
stack: new Error(promise._label).stack |
|
}); |
|
} catch(error) { |
|
setTimeout(function(){ |
|
throw error; |
|
}, 0); |
|
} |
|
}; |
|
}); |
|
define("rsvp/map", |
|
["./promise","./all","./utils","exports"], |
|
function(__dependency1__, __dependency2__, __dependency3__, __exports__) { |
|
"use strict"; |
|
var Promise = __dependency1__["default"]; |
|
var all = __dependency2__["default"]; |
|
var isArray = __dependency3__.isArray; |
|
var isFunction = __dependency3__.isFunction; |
|
|
|
/** |
|
`RSVP.map` is similar to JavaScript's native `map` method, except that it |
|
waits for all promises to become fulfilled before running the `mapFn` on |
|
each item in given to `promises`. `RSVP.map` returns a promise that will |
|
become fulfilled with the result of running `mapFn` on the values the promises |
|
become fulfilled with. |
|
|
|
For example: |
|
|
|
```javascript |
|
|
|
var promise1 = RSVP.resolve(1); |
|
var promise2 = RSVP.resolve(2); |
|
var promise3 = RSVP.resolve(3); |
|
var promises = [ promise1, promise2, promise3 ]; |
|
|
|
var mapFn = function(item){ |
|
return item + 1; |
|
}; |
|
|
|
RSVP.map(promises, mapFn).then(function(result){ |
|
// result is [ 2, 3, 4 ] |
|
}); |
|
``` |
|
|
|
If any of the `promises` given to `RSVP.map` are rejected, the first promise |
|
that is rejected will be given as an argument to the returned promise's |
|
rejection handler. For example: |
|
|
|
```javascript |
|
var promise1 = RSVP.resolve(1); |
|
var promise2 = RSVP.reject(new Error("2")); |
|
var promise3 = RSVP.reject(new Error("3")); |
|
var promises = [ promise1, promise2, promise3 ]; |
|
|
|
var mapFn = function(item){ |
|
return item + 1; |
|
}; |
|
|
|
RSVP.map(promises, mapFn).then(function(array){ |
|
// Code here never runs because there are rejected promises! |
|
}, function(reason) { |
|
// reason.message === "2" |
|
}); |
|
``` |
|
|
|
`RSVP.map` will also wait if a promise is returned from `mapFn`. For example, |
|
say you want to get all comments from a set of blog posts, but you need |
|
the blog posts first becuase they contain a url to those comments. |
|
|
|
```javscript |
|
|
|
var mapFn = function(blogPost){ |
|
// getComments does some ajax and returns an RSVP.Promise that is fulfilled |
|
// with some comments data |
|
return getComments(blogPost.comments_url); |
|
}; |
|
|
|
// getBlogPosts does some ajax and returns an RSVP.Promise that is fulfilled |
|
// with some blog post data |
|
RSVP.map(getBlogPosts(), mapFn).then(function(comments){ |
|
// comments is the result of asking the server for the comments |
|
// of all blog posts returned from getBlogPosts() |
|
}); |
|
``` |
|
|
|
@method map |
|
@for RSVP |
|
@param {Array} promises |
|
@param {Function} mapFn function to be called on each fulfilled promise. |
|
@param {String} label optional string for labeling the promise. |
|
Useful for tooling. |
|
@return {Promise} promise that is fulfilled with the result of calling |
|
`mapFn` on each fulfilled promise or value when they become fulfilled. |
|
The promise will be rejected if any of the given `promises` become rejected. |
|
@static |
|
*/ |
|
__exports__["default"] = function map(promises, mapFn, label) { |
|
return all(promises, label).then(function(results){ |
|
if (!isArray(promises)) { |
|
throw new TypeError('You must pass an array to map.'); |
|
} |
|
|
|
if (!isFunction(mapFn)){ |
|
throw new TypeError("You must pass a function to map's second argument."); |
|
} |
|
|
|
|
|
var resultLen = results.length, |
|
mappedResults = [], |
|
i; |
|
|
|
for (i = 0; i < resultLen; i++){ |
|
mappedResults.push(mapFn(results[i])); |
|
} |
|
|
|
return all(mappedResults, label); |
|
}); |
|
}; |
|
}); |
|
define("rsvp/node", |
|
["./promise","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
var Promise = __dependency1__["default"]; |
|
|
|
var slice = Array.prototype.slice; |
|
|
|
function makeNodeCallbackFor(resolve, reject) { |
|
return function (error, value) { |
|
if (error) { |
|
reject(error); |
|
} else if (arguments.length > 2) { |
|
resolve(slice.call(arguments, 1)); |
|
} else { |
|
resolve(value); |
|
} |
|
}; |
|
} |
|
|
|
/** |
|
`RSVP.denodeify` takes a "node-style" function and returns a function that |
|
will return an `RSVP.Promise`. You can use `denodeify` in Node.js or the |
|
browser when you'd prefer to use promises over using callbacks. For example, |
|
`denodeify` transforms the following: |
|
|
|
```javascript |
|
var fs = require('fs'); |
|
|
|
fs.readFile('myfile.txt', function(err, data){ |
|
if (err) return handleError(err); |
|
handleData(data); |
|
}); |
|
``` |
|
|
|
into: |
|
|
|
```javascript |
|
var fs = require('fs'); |
|
|
|
var readFile = RSVP.denodeify(fs.readFile); |
|
|
|
readFile('myfile.txt').then(handleData, handleError); |
|
``` |
|
|
|
Using `denodeify` makes it easier to compose asynchronous operations instead |
|
of using callbacks. For example, instead of: |
|
|
|
```javascript |
|
var fs = require('fs'); |
|
var log = require('some-async-logger'); |
|
|
|
fs.readFile('myfile.txt', function(err, data){ |
|
if (err) return handleError(err); |
|
fs.writeFile('myfile2.txt', data, function(err){ |
|
if (err) throw err; |
|
log('success', function(err) { |
|
if (err) throw err; |
|
}); |
|
}); |
|
}); |
|
``` |
|
|
|
You can chain the operations together using `then` from the returned promise: |
|
|
|
```javascript |
|
var fs = require('fs'); |
|
var denodeify = RSVP.denodeify; |
|
var readFile = denodeify(fs.readFile); |
|
var writeFile = denodeify(fs.writeFile); |
|
var log = denodeify(require('some-async-logger')); |
|
|
|
readFile('myfile.txt').then(function(data){ |
|
return writeFile('myfile2.txt', data); |
|
}).then(function(){ |
|
return log('SUCCESS'); |
|
}).then(function(){ |
|
// success handler |
|
}, function(reason){ |
|
// rejection handler |
|
}); |
|
``` |
|
|
|
@method denodeify |
|
@for RSVP |
|
@param {Function} nodeFunc a "node-style" function that takes a callback as |
|
its last argument. The callback expects an error to be passed as its first |
|
argument (if an error occurred, otherwise null), and the value from the |
|
operation as its second argument ("function(err, value){ }"). |
|
@param {Any} binding optional argument for binding the "this" value when |
|
calling the `nodeFunc` function. |
|
@return {Function} a function that wraps `nodeFunc` to return an |
|
`RSVP.Promise` |
|
@static |
|
*/ |
|
__exports__["default"] = function denodeify(nodeFunc, binding) { |
|
return function() { |
|
var nodeArgs = slice.call(arguments), resolve, reject; |
|
var thisArg = this || binding; |
|
|
|
return new Promise(function(resolve, reject) { |
|
Promise.all(nodeArgs).then(function(nodeArgs) { |
|
try { |
|
nodeArgs.push(makeNodeCallbackFor(resolve, reject)); |
|
nodeFunc.apply(thisArg, nodeArgs); |
|
} catch(e) { |
|
reject(e); |
|
} |
|
}); |
|
}); |
|
}; |
|
}; |
|
}); |
|
define("rsvp/promise", |
|
["./config","./events","./instrument","./utils","./promise/cast","./promise/all","./promise/race","./promise/resolve","./promise/reject","exports"], |
|
function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) { |
|
"use strict"; |
|
var config = __dependency1__.config; |
|
var EventTarget = __dependency2__["default"]; |
|
var instrument = __dependency3__["default"]; |
|
var objectOrFunction = __dependency4__.objectOrFunction; |
|
var isFunction = __dependency4__.isFunction; |
|
var now = __dependency4__.now; |
|
var cast = __dependency5__["default"]; |
|
var all = __dependency6__["default"]; |
|
var race = __dependency7__["default"]; |
|
var Resolve = __dependency8__["default"]; |
|
var Reject = __dependency9__["default"]; |
|
|
|
var guidKey = 'rsvp_' + now() + '-'; |
|
var counter = 0; |
|
|
|
function noop() {} |
|
|
|
__exports__["default"] = Promise; |
|
|
|
|
|
/** |
|
Promise objects represent the eventual result of an asynchronous operation. The |
|
primary way of interacting with a promise is through its `then` method, which |
|
registers callbacks to receive either a promise’s eventual value or the reason |
|
why the promise cannot be fulfilled. |
|
|
|
Terminology |
|
----------- |
|
|
|
- `promise` is an object or function with a `then` method whose behavior conforms to this specification. |
|
- `thenable` is an object or function that defines a `then` method. |
|
- `value` is any legal JavaScript value (including undefined, a thenable, or a promise). |
|
- `exception` is a value that is thrown using the throw statement. |
|
- `reason` is a value that indicates why a promise was rejected. |
|
- `settled` the final resting state of a promise, fulfilled or rejected. |
|
|
|
A promise can be in one of three states: pending, fulfilled, or rejected. |
|
|
|
Promises that are fulfilled have a fulfillment value and are in the fulfilled |
|
state. Promises that are rejected have a rejection reason and are in the |
|
rejected state. A fulfillment value is never a thenable. Similarly, a |
|
rejection reason is never a thenable. |
|
|
|
Promises can also be said to *resolve* a value. If this value is also a |
|
promise, then the original promise's settled state will match the value's |
|
settled state. So a promise that *resolves* a promise that rejects will |
|
itself reject, and a promise that *resolves* a promise that fulfills will |
|
itself fulfill. |
|
|
|
|
|
Basic Usage: |
|
------------ |
|
|
|
```js |
|
var promise = new Promise(function(resolve, reject) { |
|
// on success |
|
resolve(value); |
|
|
|
// on failure |
|
reject(reason); |
|
}); |
|
|
|
promise.then(function(value) { |
|
// on fulfillment |
|
}, function(reason) { |
|
// on rejection |
|
}); |
|
``` |
|
|
|
Advanced Usage: |
|
--------------- |
|
|
|
Promises shine when abstracting away asynchronous interactions such as |
|
`XMLHttpRequest`s. |
|
|
|
```js |
|
function getJSON(url) { |
|
return new Promise(function(resolve, reject){ |
|
var xhr = new XMLHttpRequest(); |
|
|
|
xhr.open('GET', url); |
|
xhr.onreadystatechange = handler; |
|
xhr.responseType = 'json'; |
|
xhr.setRequestHeader('Accept', 'application/json'); |
|
xhr.send(); |
|
|
|
function handler() { |
|
if (this.readyState === this.DONE) { |
|
if (this.status === 200) { |
|
resolve(this.response); |
|
} else { |
|
reject(new Error("getJSON: `" + url + "` failed with status: [" + this.status + "]"); |
|
} |
|
} |
|
}; |
|
}); |
|
} |
|
|
|
getJSON('/posts.json').then(function(json) { |
|
// on fulfillment |
|
}, function(reason) { |
|
// on rejection |
|
}); |
|
``` |
|
|
|
Unlike callbacks, promises are great composable primitives. |
|
|
|
```js |
|
Promise.all([ |
|
getJSON('/posts'), |
|
getJSON('/comments') |
|
]).then(function(values){ |
|
values[0] // => postsJSON |
|
values[1] // => commentsJSON |
|
|
|
return values; |
|
}); |
|
``` |
|
|
|
@class RSVP.Promise |
|
@param {function} |
|
@param {String} label optional string for labeling the promise. |
|
Useful for tooling. |
|
@constructor |
|
*/ |
|
function Promise(resolver, label) { |
|
if (!isFunction(resolver)) { |
|
throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); |
|
} |
|
|
|
if (!(this instanceof Promise)) { |
|
throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); |
|
} |
|
|
|
this._id = counter++; |
|
this._label = label; |
|
this._subscribers = []; |
|
|
|
if (config.instrument) { |
|
instrument('created', this); |
|
} |
|
|
|
if (noop !== resolver) { |
|
invokeResolver(resolver, this); |
|
} |
|
} |
|
|
|
function invokeResolver(resolver, promise) { |
|
function resolvePromise(value) { |
|
resolve(promise, value); |
|
} |
|
|
|
function rejectPromise(reason) { |
|
reject(promise, reason); |
|
} |
|
|
|
try { |
|
resolver(resolvePromise, rejectPromise); |
|
} catch(e) { |
|
rejectPromise(e); |
|
} |
|
} |
|
|
|
Promise.cast = cast; |
|
Promise.all = all; |
|
Promise.race = race; |
|
Promise.resolve = Resolve; |
|
Promise.reject = Reject; |
|
|
|
var PENDING = void 0; |
|
var SEALED = 0; |
|
var FULFILLED = 1; |
|
var REJECTED = 2; |
|
|
|
function subscribe(parent, child, onFulfillment, onRejection) { |
|
var subscribers = parent._subscribers; |
|
var length = subscribers.length; |
|
|
|
subscribers[length] = child; |
|
subscribers[length + FULFILLED] = onFulfillment; |
|
subscribers[length + REJECTED] = onRejection; |
|
} |
|
|
|
function publish(promise, settled) { |
|
var child, callback, subscribers = promise._subscribers, detail = promise._detail; |
|
|
|
if (config.instrument) { |
|
instrument(settled === FULFILLED ? 'fulfilled' : 'rejected', promise); |
|
} |
|
|
|
for (var i = 0; i < subscribers.length; i += 3) { |
|
child = subscribers[i]; |
|
callback = subscribers[i + settled]; |
|
|
|
invokeCallback(settled, child, callback, detail); |
|
} |
|
|
|
promise._subscribers = null; |
|
} |
|
|
|
Promise.prototype = { |
|
constructor: Promise, |
|
|
|
_id: undefined, |
|
_guidKey: guidKey, |
|
_label: undefined, |
|
|
|
_state: undefined, |
|
_detail: undefined, |
|
_subscribers: undefined, |
|
|
|
_onerror: function (reason) { |
|
config.trigger('error', reason); |
|
}, |
|
|
|
/** |
|
The primary way of interacting with a promise is through its `then` method, |
|
which registers callbacks to receive either a promise's eventual value or the |
|
reason why the promise cannot be fulfilled. |
|
|
|
```js |
|
findUser().then(function(user){ |
|
// user is available |
|
}, function(reason){ |
|
// user is unavailable, and you are given the reason why |
|
}); |
|
``` |
|
|
|
Chaining |
|
-------- |
|
|
|
The return value of `then` is itself a promise. This second, "downstream" |
|
promise is resolved with the return value of the first promise's fulfillment |
|
or rejection handler, or rejected if the handler throws an exception. |
|
|
|
```js |
|
findUser().then(function (user) { |
|
return user.name; |
|
}, function (reason) { |
|
return "default name"; |
|
}).then(function (userName) { |
|
// If `findUser` fulfilled, `userName` will be the user's name, otherwise it |
|
// will be `"default name"` |
|
}); |
|
|
|
findUser().then(function (user) { |
|
throw new Error("Found user, but still unhappy"); |
|
}, function (reason) { |
|
throw new Error("`findUser` rejected and we're unhappy"); |
|
}).then(function (value) { |
|
// never reached |
|
}, function (reason) { |
|
// if `findUser` fulfilled, `reason` will be "Found user, but still unhappy". |
|
// If `findUser` rejected, `reason` will be "`findUser` rejected and we're unhappy". |
|
}); |
|
``` |
|
If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. |
|
|
|
```js |
|
findUser().then(function (user) { |
|
throw new PedagogicalException("Upstream error"); |
|
}).then(function (value) { |
|
// never reached |
|
}).then(function (value) { |
|
// never reached |
|
}, function (reason) { |
|
// The `PedgagocialException` is propagated all the way down to here |
|
}); |
|
``` |
|
|
|
Assimilation |
|
------------ |
|
|
|
Sometimes the value you want to propagate to a downstream promise can only be |
|
retrieved asynchronously. This can be achieved by returning a promise in the |
|
fulfillment or rejection handler. The downstream promise will then be pending |
|
until the returned promise is settled. This is called *assimilation*. |
|
|
|
```js |
|
findUser().then(function (user) { |
|
return findCommentsByAuthor(user); |
|
}).then(function (comments) { |
|
// The user's comments are now available |
|
}); |
|
``` |
|
|
|
If the assimliated promise rejects, then the downstream promise will also reject. |
|
|
|
```js |
|
findUser().then(function (user) { |
|
return findCommentsByAuthor(user); |
|
}).then(function (comments) { |
|
// If `findCommentsByAuthor` fulfills, we'll have the value here |
|
}, function (reason) { |
|
// If `findCommentsByAuthor` rejects, we'll have the reason here |
|
}); |
|
``` |
|
|
|
Simple Example |
|
-------------- |
|
|
|
Synchronous Example |
|
|
|
```javascript |
|
var result; |
|
|
|
try { |
|
result = findResult(); |
|
// success |
|
} catch(reason) { |
|
// failure |
|
} |
|
``` |
|
|
|
Errback Example |
|
|
|
```js |
|
findResult(function(result, err){ |
|
if (err) { |
|
// failure |
|
} else { |
|
// success |
|
} |
|
}); |
|
``` |
|
|
|
Promise Example; |
|
|
|
```javascript |
|
findResult().then(function(result){ |
|
// success |
|
}, function(reason){ |
|
// failure |
|
}); |
|
``` |
|
|
|
Advanced Example |
|
-------------- |
|
|
|
Synchronous Example |
|
|
|
```javascript |
|
var author, books; |
|
|
|
try { |
|
author = findAuthor(); |
|
books = findBooksByAuthor(author); |
|
// success |
|
} catch(reason) { |
|
// failure |
|
} |
|
``` |
|
|
|
Errback Example |
|
|
|
```js |
|
|
|
function foundBooks(books) { |
|
|
|
} |
|
|
|
function failure(reason) { |
|
|
|
} |
|
|
|
findAuthor(function(author, err){ |
|
if (err) { |
|
failure(err); |
|
// failure |
|
} else { |
|
try { |
|
findBoooksByAuthor(author, function(books, err) { |
|
if (err) { |
|
failure(err); |
|
} else { |
|
try { |
|
foundBooks(books); |
|
} catch(reason) { |
|
failure(reason); |
|
} |
|
} |
|
}); |
|
} catch(error) { |
|
failure(err); |
|
} |
|
// success |
|
} |
|
}); |
|
``` |
|
|
|
Promise Example; |
|
|
|
```javascript |
|
findAuthor(). |
|
then(findBooksByAuthor). |
|
then(function(books){ |
|
// found books |
|
}).catch(function(reason){ |
|
// something went wrong |
|
}); |
|
``` |
|
|
|
@method then |
|
@param {Function} onFulfilled |
|
@param {Function} onRejected |
|
@param {String} label optional string for labeling the promise. |
|
Useful for tooling. |
|
@return {Promise} |
|
*/ |
|
then: function(onFulfillment, onRejection, label) { |
|
var promise = this; |
|
this._onerror = null; |
|
|
|
var thenPromise = new this.constructor(noop, label); |
|
|
|
if (this._state) { |
|
var callbacks = arguments; |
|
config.async(function invokePromiseCallback() { |
|
invokeCallback(promise._state, thenPromise, callbacks[promise._state - 1], promise._detail); |
|
}); |
|
} else { |
|
subscribe(this, thenPromise, onFulfillment, onRejection); |
|
} |
|
|
|
if (config.instrument) { |
|
instrument('chained', promise, thenPromise); |
|
} |
|
|
|
return thenPromise; |
|
}, |
|
|
|
/** |
|
`catch` is simply sugar for `then(undefined, onRejection)` which makes it the same |
|
as the catch block of a try/catch statement. |
|
|
|
```js |
|
function findAuthor(){ |
|
throw new Error("couldn't find that author"); |
|
} |
|
|
|
// synchronous |
|
try { |
|
findAuthor(); |
|
} catch(reason) { |
|
// something went wrong |
|
} |
|
|
|
// async with promises |
|
findAuthor().catch(function(reason){ |
|
// something went wrong |
|
}); |
|
``` |
|
|
|
@method catch |
|
@param {Function} onRejection |
|
@param {String} label optional string for labeling the promise. |
|
Useful for tooling. |
|
@return {Promise} |
|
*/ |
|
'catch': function(onRejection, label) { |
|
return this.then(null, onRejection, label); |
|
}, |
|
|
|
/** |
|
`finally` will be invoked regardless of the promise's fate just as native |
|
try/catch/finally behaves |
|
|
|
Synchronous example: |
|
|
|
```js |
|
findAuthor() { |
|
if (Math.random() > 0.5) { |
|
throw new Error(); |
|
} |
|
return new Author(); |
|
} |
|
|
|
try { |
|
return findAuthor(); // succeed or fail |
|
} catch(error) { |
|
return findOtherAuther(); |
|
} finally { |
|
// always runs |
|
// doesn't affect the return value |
|
} |
|
``` |
|
|
|
Asynchronous example: |
|
|
|
```js |
|
findAuthor().catch(function(reason){ |
|
return findOtherAuther(); |
|
}).finally(function(){ |
|
// author was either found, or not |
|
}); |
|
``` |
|
|
|
@method finally |
|
@param {Function} callback |
|
@param {String} label optional string for labeling the promise. |
|
Useful for tooling. |
|
@return {Promise} |
|
*/ |
|
'finally': function(callback, label) { |
|
var constructor = this.constructor; |
|
|
|
return this.then(function(value) { |
|
return constructor.cast(callback()).then(function(){ |
|
return value; |
|
}); |
|
}, function(reason) { |
|
return constructor.cast(callback()).then(function(){ |
|
throw reason; |
|
}); |
|
}, label); |
|
} |
|
}; |
|
|
|
function invokeCallback(settled, promise, callback, detail) { |
|
var hasCallback = isFunction(callback), |
|
value, error, succeeded, failed; |
|
|
|
if (hasCallback) { |
|
try { |
|
value = callback(detail); |
|
succeeded = true; |
|
} catch(e) { |
|
failed = true; |
|
error = e; |
|
} |
|
} else { |
|
value = detail; |
|
succeeded = true; |
|
} |
|
|
|
if (handleThenable(promise, value)) { |
|
return; |
|
} else if (hasCallback && succeeded) { |
|
resolve(promise, value); |
|
} else if (failed) { |
|
reject(promise, error); |
|
} else if (settled === FULFILLED) { |
|
resolve(promise, value); |
|
} else if (settled === REJECTED) { |
|
reject(promise, value); |
|
} |
|
} |
|
|
|
function handleThenable(promise, value) { |
|
var then = null, |
|
resolved; |
|
|
|
try { |
|
if (promise === value) { |
|
throw new TypeError("A promises callback cannot return that same promise."); |
|
} |
|
|
|
if (objectOrFunction(value)) { |
|
then = value.then; |
|
|
|
if (isFunction(then)) { |
|
then.call(value, function(val) { |
|
if (resolved) { return true; } |
|
resolved = true; |
|
|
|
if (value !== val) { |
|
resolve(promise, val); |
|
} else { |
|
fulfill(promise, val); |
|
} |
|
}, function(val) { |
|
if (resolved) { return true; } |
|
resolved = true; |
|
|
|
reject(promise, val); |
|
}, 'derived from: ' + (promise._label || ' unknown promise')); |
|
|
|
return true; |
|
} |
|
} |
|
} catch (error) { |
|
if (resolved) { return true; } |
|
reject(promise, error); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
function resolve(promise, value) { |
|
if (promise === value) { |
|
fulfill(promise, value); |
|
} else if (!handleThenable(promise, value)) { |
|
fulfill(promise, value); |
|
} |
|
} |
|
|
|
function fulfill(promise, value) { |
|
if (promise._state !== PENDING) { return; } |
|
promise._state = SEALED; |
|
promise._detail = value; |
|
|
|
config.async(publishFulfillment, promise); |
|
} |
|
|
|
function reject(promise, reason) { |
|
if (promise._state !== PENDING) { return; } |
|
promise._state = SEALED; |
|
promise._detail = reason; |
|
|
|
config.async(publishRejection, promise); |
|
} |
|
|
|
function publishFulfillment(promise) { |
|
publish(promise, promise._state = FULFILLED); |
|
} |
|
|
|
function publishRejection(promise) { |
|
if (promise._onerror) { |
|
promise._onerror(promise._detail); |
|
} |
|
|
|
publish(promise, promise._state = REJECTED); |
|
} |
|
}); |
|
define("rsvp/promise/all", |
|
["../utils","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
var isArray = __dependency1__.isArray; |
|
var isNonThenable = __dependency1__.isNonThenable; |
|
|
|
/** |
|
`RSVP.Promise.all` accepts an array of promises, and returns a new promise which |
|
is fulfilled with an array of fulfillment values for the passed promises, or |
|
rejected with the reason of the first passed promise to be rejected. It casts all |
|
elements of the passed iterable to promises as it runs this algorithm. |
|
|
|
Example: |
|
|
|
```javascript |
|
var promise1 = RSVP.resolve(1); |
|
var promise2 = RSVP.resolve(2); |
|
var promise3 = RSVP.resolve(3); |
|
var promises = [ promise1, promise2, promise3 ]; |
|
|
|
RSVP.Promise.all(promises).then(function(array){ |
|
// The array here would be [ 1, 2, 3 ]; |
|
}); |
|
``` |
|
|
|
If any of the `promises` given to `RSVP.all` are rejected, the first promise |
|
that is rejected will be given as an argument to the returned promises's |
|
rejection handler. For example: |
|
|
|
Example: |
|
|
|
```javascript |
|
var promise1 = RSVP.resolve(1); |
|
var promise2 = RSVP.reject(new Error("2")); |
|
var promise3 = RSVP.reject(new Error("3")); |
|
var promises = [ promise1, promise2, promise3 ]; |
|
|
|
RSVP.Promise.all(promises).then(function(array){ |
|
// Code here never runs because there are rejected promises! |
|
}, function(error) { |
|
// error.message === "2" |
|
}); |
|
``` |
|
|
|
@method all |
|
@for Ember.RSVP.Promise |
|
@param {Array} entries array of promises |
|
@param {String} label optional string for labeling the promise. |
|
Useful for tooling. |
|
@return {Promise} promise that is fulfilled when all `promises` have been |
|
fulfilled, or rejected if any of them become rejected. |
|
@static |
|
*/ |
|
__exports__["default"] = function all(entries, label) { |
|
|
|
/*jshint validthis:true */ |
|
var Constructor = this; |
|
|
|
return new Constructor(function(resolve, reject) { |
|
if (!isArray(entries)) { |
|
throw new TypeError('You must pass an array to all.'); |
|
} |
|
|
|
var remaining = entries.length; |
|
var results = new Array(remaining); |
|
var entry, pending = true; |
|
|
|
if (remaining === 0) { |
|
resolve(results); |
|
return; |
|
} |
|
|
|
function fulfillmentAt(index) { |
|
return function(value) { |
|
results[index] = value; |
|
if (--remaining === 0) { |
|
resolve(results); |
|
} |
|
}; |
|
} |
|
|
|
function onRejection(reason) { |
|
remaining = 0; |
|
reject(reason); |
|
} |
|
|
|
for (var index = 0; index < entries.length; index++) { |
|
entry = entries[index]; |
|
if (isNonThenable(entry)) { |
|
results[index] = entry; |
|
if (--remaining === 0) { |
|
resolve(results); |
|
} |
|
} else { |
|
Constructor.cast(entry).then(fulfillmentAt(index), onRejection); |
|
} |
|
} |
|
}, label); |
|
}; |
|
}); |
|
define("rsvp/promise/cast", |
|
["exports"], |
|
function(__exports__) { |
|
"use strict"; |
|
/** |
|
`RSVP.Promise.cast` coerces its argument to a promise, or returns the |
|
argument if it is already a promise which shares a constructor with the caster. |
|
|
|
Example: |
|
|
|
```javascript |
|
var promise = RSVP.Promise.resolve(1); |
|
var casted = RSVP.Promise.cast(promise); |
|
|
|
console.log(promise === casted); // true |
|
``` |
|
|
|
In the case of a promise whose constructor does not match, it is assimilated. |
|
The resulting promise will fulfill or reject based on the outcome of the |
|
promise being casted. |
|
|
|
Example: |
|
|
|
```javascript |
|
var thennable = $.getJSON('/api/foo'); |
|
var casted = RSVP.Promise.cast(thennable); |
|
|
|
console.log(thennable === casted); // false |
|
console.log(casted instanceof RSVP.Promise) // true |
|
|
|
casted.then(function(data) { |
|
// data is the value getJSON fulfills with |
|
}); |
|
``` |
|
|
|
In the case of a non-promise, a promise which will fulfill with that value is |
|
returned. |
|
|
|
Example: |
|
|
|
```javascript |
|
var value = 1; // could be a number, boolean, string, undefined... |
|
var casted = RSVP.Promise.cast(value); |
|
|
|
console.log(value === casted); // false |
|
console.log(casted instanceof RSVP.Promise) // true |
|
|
|
casted.then(function(val) { |
|
val === value // => true |
|
}); |
|
``` |
|
|
|
`RSVP.Promise.cast` is similar to `RSVP.Promise.resolve`, but `RSVP.Promise.cast` differs in the |
|
following ways: |
|
|
|
* `RSVP.Promise.cast` serves as a memory-efficient way of getting a promise, when you |
|
have something that could either be a promise or a value. RSVP.resolve |
|
will have the same effect but will create a new promise wrapper if the |
|
argument is a promise. |
|
* `RSVP.Promise.cast` is a way of casting incoming thenables or promise subclasses to |
|
promises of the exact class specified, so that the resulting object's `then` is |
|
ensured to have the behavior of the constructor you are calling cast on (i.e., RSVP.Promise). |
|
|
|
@method cast |
|
@param {Object} object to be casted |
|
@param {String} label optional string for labeling the promise. |
|
Useful for tooling. |
|
@return {Promise} promise |
|
@static |
|
*/ |
|
|
|
__exports__["default"] = function cast(object, label) { |
|
/*jshint validthis:true */ |
|
var Constructor = this; |
|
|
|
if (object && typeof object === 'object' && object.constructor === Constructor) { |
|
return object; |
|
} |
|
|
|
return new Constructor(function(resolve) { |
|
resolve(object); |
|
}, label); |
|
}; |
|
}); |
|
define("rsvp/promise/race", |
|
["../utils","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
/* global toString */ |
|
|
|
var isArray = __dependency1__.isArray; |
|
var isFunction = __dependency1__.isFunction; |
|
var isNonThenable = __dependency1__.isNonThenable; |
|
|
|
/** |
|
`RSVP.Promise.race` returns a new promise which is settled in the same way as the |
|
first passed promise to settle. |
|
|
|
Example: |
|
|
|
```javascript |
|
var promise1 = new RSVP.Promise(function(resolve, reject){ |
|
setTimeout(function(){ |
|
resolve("promise 1"); |
|
}, 200); |
|
}); |
|
|
|
var promise2 = new RSVP.Promise(function(resolve, reject){ |
|
setTimeout(function(){ |
|
resolve("promise 2"); |
|
}, 100); |
|
}); |
|
|
|
RSVP.Promise.race([promise1, promise2]).then(function(result){ |
|
// result === "promise 2" because it was resolved before promise1 |
|
// was resolved. |
|
}); |
|
``` |
|
|
|
`RSVP.Promise.race` is deterministic in that only the state of the first |
|
settled promise matters. For example, even if other promises given to the |
|
`promises` array argument are resolved, but the first settled promise has |
|
become rejected before the other promises became fulfilled, the returned |
|
promise will become rejected: |
|
|
|
```javascript |
|
var promise1 = new RSVP.Promise(function(resolve, reject){ |
|
setTimeout(function(){ |
|
resolve("promise 1"); |
|
}, 200); |
|
}); |
|
|
|
var promise2 = new RSVP.Promise(function(resolve, reject){ |
|
setTimeout(function(){ |
|
reject(new Error("promise 2")); |
|
}, 100); |
|
}); |
|
|
|
RSVP.Promise.race([promise1, promise2]).then(function(result){ |
|
// Code here never runs |
|
}, function(reason){ |
|
// reason.message === "promise2" because promise 2 became rejected before |
|
// promise 1 became fulfilled |
|
}); |
|
``` |
|
|
|
An example real-world use case is implementing timeouts: |
|
|
|
```javascript |
|
RSVP.Promise.race([ajax('foo.json'), timeout(5000)]) |
|
``` |
|
|
|
@method race |
|
@param {Array} promises array of promises to observe |
|
@param {String} label optional string for describing the promise returned. |
|
Useful for tooling. |
|
@return {Promise} a promise which settles in the same way as the first passed |
|
promise to settle. |
|
@static |
|
*/ |
|
__exports__["default"] = function race(entries, label) { |
|
/*jshint validthis:true */ |
|
var Constructor = this, entry; |
|
|
|
return new Constructor(function(resolve, reject) { |
|
if (!isArray(entries)) { |
|
throw new TypeError('You must pass an array to race.'); |
|
} |
|
|
|
var pending = true; |
|
|
|
function onFulfillment(value) { if (pending) { pending = false; resolve(value); } } |
|
function onRejection(reason) { if (pending) { pending = false; reject(reason); } } |
|
|
|
for (var i = 0; i < entries.length; i++) { |
|
entry = entries[i]; |
|
if (isNonThenable(entry)) { |
|
pending = false; |
|
resolve(entry); |
|
return; |
|
} else { |
|
Constructor.cast(entry).then(onFulfillment, onRejection); |
|
} |
|
} |
|
}, label); |
|
}; |
|
}); |
|
define("rsvp/promise/reject", |
|
["exports"], |
|
function(__exports__) { |
|
"use strict"; |
|
/** |
|
`RSVP.Promise.reject` returns a promise rejected with the passed `reason`. |
|
It is shorthand for the following: |
|
|
|
```javascript |
|
var promise = new RSVP.Promise(function(resolve, reject){ |
|
reject(new Error('WHOOPS')); |
|
}); |
|
|
|
promise.then(function(value){ |
|
// Code here doesn't run because the promise is rejected! |
|
}, function(reason){ |
|
// reason.message === 'WHOOPS' |
|
}); |
|
``` |
|
|
|
Instead of writing the above, your code now simply becomes the following: |
|
|
|
```javascript |
|
var promise = RSVP.Promise.reject(new Error('WHOOPS')); |
|
|
|
promise.then(function(value){ |
|
// Code here doesn't run because the promise is rejected! |
|
}, function(reason){ |
|
// reason.message === 'WHOOPS' |
|
}); |
|
``` |
|
|
|
@method reject |
|
@param {Any} reason value that the returned promise will be rejected with. |
|
@param {String} label optional string for identifying the returned promise. |
|
Useful for tooling. |
|
@return {Promise} a promise rejected with the given `reason`. |
|
@static |
|
*/ |
|
__exports__["default"] = function reject(reason, label) { |
|
/*jshint validthis:true */ |
|
var Constructor = this; |
|
|
|
return new Constructor(function (resolve, reject) { |
|
reject(reason); |
|
}, label); |
|
}; |
|
}); |
|
define("rsvp/promise/resolve", |
|
["exports"], |
|
function(__exports__) { |
|
"use strict"; |
|
/** |
|
`RSVP.Promise.resolve` returns a promise that will become resolved with the |
|
passed `value`. It is shorthand for the following: |
|
|
|
```javascript |
|
var promise = new RSVP.Promise(function(resolve, reject){ |
|
resolve(1); |
|
}); |
|
|
|
promise.then(function(value){ |
|
// value === 1 |
|
}); |
|
``` |
|
|
|
Instead of writing the above, your code now simply becomes the following: |
|
|
|
```javascript |
|
var promise = RSVP.Promise.resolve(1); |
|
|
|
promise.then(function(value){ |
|
// value === 1 |
|
}); |
|
``` |
|
|
|
@method resolve |
|
@param {Any} value value that the returned promise will be resolved with |
|
@param {String} label optional string for identifying the returned promise. |
|
Useful for tooling. |
|
@return {Promise} a promise that will become fulfilled with the given |
|
`value` |
|
@static |
|
*/ |
|
__exports__["default"] = function resolve(value, label) { |
|
/*jshint validthis:true */ |
|
var Constructor = this; |
|
|
|
return new Constructor(function(resolve, reject) { |
|
resolve(value); |
|
}, label); |
|
}; |
|
}); |
|
define("rsvp/race", |
|
["./promise","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
var Promise = __dependency1__["default"]; |
|
|
|
/** |
|
This is a convenient alias for `RSVP.Promise.race`. |
|
|
|
@method race |
|
@param {Array} array Array of promises. |
|
@param {String} label An optional label. This is useful |
|
for tooling. |
|
@static |
|
*/ |
|
__exports__["default"] = function race(array, label) { |
|
return Promise.race(array, label); |
|
}; |
|
}); |
|
define("rsvp/reject", |
|
["./promise","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
var Promise = __dependency1__["default"]; |
|
|
|
/** |
|
This is a convenient alias for `RSVP.Promise.reject`. |
|
|
|
@method reject |
|
@for RSVP |
|
@param {Any} reason value that the returned promise will be rejected with. |
|
@param {String} label optional string for identifying the returned promise. |
|
Useful for tooling. |
|
@return {Promise} a promise rejected with the given `reason`. |
|
@static |
|
*/ |
|
__exports__["default"] = function reject(reason, label) { |
|
return Promise.reject(reason, label); |
|
}; |
|
}); |
|
define("rsvp/resolve", |
|
["./promise","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
var Promise = __dependency1__["default"]; |
|
|
|
/** |
|
This is a convenient alias for `RSVP.Promise.resolve`. |
|
|
|
@method resolve |
|
@for RSVP |
|
@param {Any} value value that the returned promise will be resolved with |
|
@param {String} label optional string for identifying the returned promise. |
|
Useful for tooling. |
|
@return {Promise} a promise that will become fulfilled with the given |
|
`value` |
|
@static |
|
*/ |
|
__exports__["default"] = function resolve(value, label) { |
|
return Promise.resolve(value, label); |
|
}; |
|
}); |
|
define("rsvp/rethrow", |
|
["exports"], |
|
function(__exports__) { |
|
"use strict"; |
|
/** |
|
`RSVP.rethrow` will rethrow an error on the next turn of the JavaScript event |
|
loop in order to aid debugging. |
|
|
|
Promises A+ specifies that any exceptions that occur with a promise must be |
|
caught by the promises implementation and bubbled to the last handler. For |
|
this reason, it is recommended that you always specify a second rejection |
|
handler function to `then`. However, `RSVP.rethrow` will throw the exception |
|
outside of the promise, so it bubbles up to your console if in the browser, |
|
or domain/cause uncaught exception in Node. `rethrow` will also throw the |
|
error again so the error can be handled by the promise per the spec. |
|
|
|
```javascript |
|
function throws(){ |
|
throw new Error('Whoops!'); |
|
} |
|
|
|
var promise = new RSVP.Promise(function(resolve, reject){ |
|
throws(); |
|
}); |
|
|
|
promise.catch(RSVP.rethrow).then(function(){ |
|
// Code here doesn't run because the promise became rejected due to an |
|
// error! |
|
}, function (err){ |
|
// handle the error here |
|
}); |
|
``` |
|
|
|
The 'Whoops' error will be thrown on the next turn of the event loop |
|
and you can watch for it in your console. You can also handle it using a |
|
rejection handler given to `.then` or `.catch` on the returned promise. |
|
|
|
@method rethrow |
|
@for RSVP |
|
@param {Error} reason reason the promise became rejected. |
|
@throws Error |
|
@static |
|
*/ |
|
__exports__["default"] = function rethrow(reason) { |
|
setTimeout(function() { |
|
throw reason; |
|
}); |
|
throw reason; |
|
}; |
|
}); |
|
define("rsvp/utils", |
|
["exports"], |
|
function(__exports__) { |
|
"use strict"; |
|
function objectOrFunction(x) { |
|
return typeof x === "function" || (typeof x === "object" && x !== null); |
|
} |
|
|
|
__exports__.objectOrFunction = objectOrFunction;function isFunction(x) { |
|
return typeof x === "function"; |
|
} |
|
|
|
__exports__.isFunction = isFunction;function isNonThenable(x) { |
|
return !objectOrFunction(x); |
|
} |
|
|
|
__exports__.isNonThenable = isNonThenable;function isArray(x) { |
|
return Object.prototype.toString.call(x) === "[object Array]"; |
|
} |
|
|
|
__exports__.isArray = isArray;// Date.now is not available in browsers < IE9 |
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now#Compatibility |
|
var now = Date.now || function() { return new Date().getTime(); }; |
|
__exports__.now = now; |
|
var keysOf = Object.keys || function(object) { |
|
var result = []; |
|
|
|
for (var prop in object) { |
|
result.push(prop); |
|
} |
|
|
|
return result; |
|
}; |
|
__exports__.keysOf = keysOf; |
|
}); |
|
define("rsvp", |
|
["./rsvp/promise","./rsvp/events","./rsvp/node","./rsvp/all","./rsvp/all_settled","./rsvp/race","./rsvp/hash","./rsvp/rethrow","./rsvp/defer","./rsvp/config","./rsvp/map","./rsvp/resolve","./rsvp/reject","./rsvp/filter","exports"], |
|
function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __exports__) { |
|
"use strict"; |
|
var Promise = __dependency1__["default"]; |
|
var EventTarget = __dependency2__["default"]; |
|
var denodeify = __dependency3__["default"]; |
|
var all = __dependency4__["default"]; |
|
var allSettled = __dependency5__["default"]; |
|
var race = __dependency6__["default"]; |
|
var hash = __dependency7__["default"]; |
|
var rethrow = __dependency8__["default"]; |
|
var defer = __dependency9__["default"]; |
|
var config = __dependency10__.config; |
|
var configure = __dependency10__.configure; |
|
var map = __dependency11__["default"]; |
|
var resolve = __dependency12__["default"]; |
|
var reject = __dependency13__["default"]; |
|
var filter = __dependency14__["default"]; |
|
|
|
function async(callback, arg) { |
|
config.async(callback, arg); |
|
} |
|
|
|
function on() { |
|
config.on.apply(config, arguments); |
|
} |
|
|
|
function off() { |
|
config.off.apply(config, arguments); |
|
} |
|
|
|
// Set up instrumentation through `window.__PROMISE_INTRUMENTATION__` |
|
if (typeof window !== 'undefined' && typeof window.__PROMISE_INSTRUMENTATION__ === 'object') { |
|
var callbacks = window.__PROMISE_INSTRUMENTATION__; |
|
configure('instrument', true); |
|
for (var eventName in callbacks) { |
|
if (callbacks.hasOwnProperty(eventName)) { |
|
on(eventName, callbacks[eventName]); |
|
} |
|
} |
|
} |
|
|
|
__exports__.Promise = Promise; |
|
__exports__.EventTarget = EventTarget; |
|
__exports__.all = all; |
|
__exports__.allSettled = allSettled; |
|
__exports__.race = race; |
|
__exports__.hash = hash; |
|
__exports__.rethrow = rethrow; |
|
__exports__.defer = defer; |
|
__exports__.denodeify = denodeify; |
|
__exports__.configure = configure; |
|
__exports__.on = on; |
|
__exports__.off = off; |
|
__exports__.resolve = resolve; |
|
__exports__.reject = reject; |
|
__exports__.async = async; |
|
__exports__.map = map; |
|
__exports__.filter = filter; |
|
}); |
|
|
|
})(); |
|
|
|
(function() { |
|
define("container/container", |
|
["container/inheriting_dict","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
var InheritingDict = __dependency1__["default"]; |
|
|
|
// A lightweight container that helps to assemble and decouple components. |
|
// Public api for the container is still in flux. |
|
// The public api, specified on the application namespace should be considered the stable api. |
|
function Container(parent) { |
|
this.parent = parent; |
|
this.children = []; |
|
|
|
this.resolver = parent && parent.resolver || function() {}; |
|
|
|
this.registry = new InheritingDict(parent && parent.registry); |
|
this.cache = new InheritingDict(parent && parent.cache); |
|
this.factoryCache = new InheritingDict(parent && parent.factoryCache); |
|
this.resolveCache = new InheritingDict(parent && parent.resolveCache); |
|
this.typeInjections = new InheritingDict(parent && parent.typeInjections); |
|
this.injections = {}; |
|
|
|
this.factoryTypeInjections = new InheritingDict(parent && parent.factoryTypeInjections); |
|
this.factoryInjections = {}; |
|
|
|
this._options = new InheritingDict(parent && parent._options); |
|
this._typeOptions = new InheritingDict(parent && parent._typeOptions); |
|
} |
|
|
|
Container.prototype = { |
|
|
|
/** |
|
@property parent |
|
@type Container |
|
@default null |
|
*/ |
|
parent: null, |
|
|
|
/** |
|
@property children |
|
@type Array |
|
@default [] |
|
*/ |
|
children: null, |
|
|
|
/** |
|
@property resolver |
|
@type function |
|
*/ |
|
resolver: null, |
|
|
|
/** |
|
@property registry |
|
@type InheritingDict |
|
*/ |
|
registry: null, |
|
|
|
/** |
|
@property cache |
|
@type InheritingDict |
|
*/ |
|
cache: null, |
|
|
|
/** |
|
@property typeInjections |
|
@type InheritingDict |
|
*/ |
|
typeInjections: null, |
|
|
|
/** |
|
@property injections |
|
@type Object |
|
@default {} |
|
*/ |
|
injections: null, |
|
|
|
/** |
|
@private |
|
|
|
@property _options |
|
@type InheritingDict |
|
@default null |
|
*/ |
|
_options: null, |
|
|
|
/** |
|
@private |
|
|
|
@property _typeOptions |
|
@type InheritingDict |
|
*/ |
|
_typeOptions: null, |
|
|
|
/** |
|
Returns a new child of the current container. These children are configured |
|
to correctly inherit from the current container. |
|
|
|
@method child |
|
@return {Container} |
|
*/ |
|
child: function() { |
|
var container = new Container(this); |
|
this.children.push(container); |
|
return container; |
|
}, |
|
|
|
/** |
|
Sets a key-value pair on the current container. If a parent container, |
|
has the same key, once set on a child, the parent and child will diverge |
|
as expected. |
|
|
|
@method set |
|
@param {Object} object |
|
@param {String} key |
|
@param {any} value |
|
*/ |
|
set: function(object, key, value) { |
|
object[key] = value; |
|
}, |
|
|
|
/** |
|
Registers a factory for later injection. |
|
|
|
Example: |
|
|
|
```javascript |
|
var container = new Container(); |
|
|
|
container.register('model:user', Person, {singleton: false }); |
|
container.register('fruit:favorite', Orange); |
|
container.register('communication:main', Email, {singleton: false}); |
|
``` |
|
|
|
@method register |
|
@param {String} fullName |
|
@param {Function} factory |
|
@param {Object} options |
|
*/ |
|
register: function(fullName, factory, options) { |
|
validateFullName(fullName); |
|
|
|
if (factory === undefined) { |
|
throw new TypeError('Attempting to register an unknown factory: `' + fullName + '`'); |
|
} |
|
|
|
var normalizedName = this.normalize(fullName); |
|
|
|
if (this.cache.has(normalizedName)) { |
|
throw new Error('Cannot re-register: `' + fullName +'`, as it has already been looked up.'); |
|
} |
|
|
|
this.registry.set(normalizedName, factory); |
|
this._options.set(normalizedName, options || {}); |
|
}, |
|
|
|
/** |
|
Unregister a fullName |
|
|
|
```javascript |
|
var container = new Container(); |
|
container.register('model:user', User); |
|
|
|
container.lookup('model:user') instanceof User //=> true |
|
|
|
container.unregister('model:user') |
|
container.lookup('model:user') === undefined //=> true |
|
``` |
|
|
|
@method unregister |
|
@param {String} fullName |
|
*/ |
|
unregister: function(fullName) { |
|
validateFullName(fullName); |
|
|
|
var normalizedName = this.normalize(fullName); |
|
|
|
this.registry.remove(normalizedName); |
|
this.cache.remove(normalizedName); |
|
this.factoryCache.remove(normalizedName); |
|
this.resolveCache.remove(normalizedName); |
|
this._options.remove(normalizedName); |
|
}, |
|
|
|
/** |
|
Given a fullName return the corresponding factory. |
|
|
|
By default `resolve` will retrieve the factory from |
|
its container's registry. |
|
|
|
```javascript |
|
var container = new Container(); |
|
container.register('api:twitter', Twitter); |
|
|
|
container.resolve('api:twitter') // => Twitter |
|
``` |
|
|
|
Optionally the container can be provided with a custom resolver. |
|
If provided, `resolve` will first provide the custom resolver |
|
the oppertunity to resolve the fullName, otherwise it will fallback |
|
to the registry. |
|
|
|
```javascript |
|
var container = new Container(); |
|
container.resolver = function(fullName) { |
|
// lookup via the module system of choice |
|
}; |
|
|
|
// the twitter factory is added to the module system |
|
container.resolve('api:twitter') // => Twitter |
|
``` |
|
|
|
@method resolve |
|
@param {String} fullName |
|
@return {Function} fullName's factory |
|
*/ |
|
resolve: function(fullName) { |
|
validateFullName(fullName); |
|
|
|
var normalizedName = this.normalize(fullName); |
|
var cached = this.resolveCache.get(normalizedName); |
|
|
|
if (cached) { return cached; } |
|
|
|
var resolved = this.resolver(normalizedName) || this.registry.get(normalizedName); |
|
|
|
this.resolveCache.set(normalizedName, resolved); |
|
|
|
return resolved; |
|
}, |
|
|
|
/** |
|
A hook that can be used to describe how the resolver will |
|
attempt to find the factory. |
|
|
|
For example, the default Ember `.describe` returns the full |
|
class name (including namespace) where Ember's resolver expects |
|
to find the `fullName`. |
|
|
|
@method describe |
|
@param {String} fullName |
|
@return {string} described fullName |
|
*/ |
|
describe: function(fullName) { |
|
return fullName; |
|
}, |
|
|
|
/** |
|
A hook to enable custom fullName normalization behaviour |
|
|
|
@method normalize |
|
@param {String} fullName |
|
@return {string} normalized fullName |
|
*/ |
|
normalize: function(fullName) { |
|
return fullName; |
|
}, |
|
|
|
/** |
|
@method makeToString |
|
|
|
@param {any} factory |
|
@param {string} fullName |
|
@return {function} toString function |
|
*/ |
|
makeToString: function(factory, fullName) { |
|
return factory.toString(); |
|
}, |
|
|
|
/** |
|
Given a fullName return a corresponding instance. |
|
|
|
The default behaviour is for lookup to return a singleton instance. |
|
The singleton is scoped to the container, allowing multiple containers |
|
to all have their own locally scoped singletons. |
|
|
|
```javascript |
|
var container = new Container(); |
|
container.register('api:twitter', Twitter); |
|
|
|
var twitter = container.lookup('api:twitter'); |
|
|
|
twitter instanceof Twitter; // => true |
|
|
|
// by default the container will return singletons |
|
var twitter2 = container.lookup('api:twitter'); |
|
twitter instanceof Twitter; // => true |
|
|
|
twitter === twitter2; //=> true |
|
``` |
|
|
|
If singletons are not wanted an optional flag can be provided at lookup. |
|
|
|
```javascript |
|
var container = new Container(); |
|
container.register('api:twitter', Twitter); |
|
|
|
var twitter = container.lookup('api:twitter', { singleton: false }); |
|
var twitter2 = container.lookup('api:twitter', { singleton: false }); |
|
|
|
twitter === twitter2; //=> false |
|
``` |
|
|
|
@method lookup |
|
@param {String} fullName |
|
@param {Object} options |
|
@return {any} |
|
*/ |
|
lookup: function(fullName, options) { |
|
validateFullName(fullName); |
|
return lookup(this, this.normalize(fullName), options); |
|
}, |
|
|
|
/** |
|
Given a fullName return the corresponding factory. |
|
|
|
@method lookupFactory |
|
@param {String} fullName |
|
@return {any} |
|
*/ |
|
lookupFactory: function(fullName) { |
|
validateFullName(fullName); |
|
return factoryFor(this, this.normalize(fullName)); |
|
}, |
|
|
|
/** |
|
Given a fullName check if the container is aware of its factory |
|
or singleton instance. |
|
|
|
@method has |
|
@param {String} fullName |
|
@return {Boolean} |
|
*/ |
|
has: function(fullName) { |
|
validateFullName(fullName); |
|
return has(this, this.normalize(fullName)); |
|
}, |
|
|
|
/** |
|
Allow registering options for all factories of a type. |
|
|
|
```javascript |
|
var container = new Container(); |
|
|
|
// if all of type `connection` must not be singletons |
|
container.optionsForType('connection', { singleton: false }); |
|
|
|
container.register('connection:twitter', TwitterConnection); |
|
container.register('connection:facebook', FacebookConnection); |
|
|
|
var twitter = container.lookup('connection:twitter'); |
|
var twitter2 = container.lookup('connection:twitter'); |
|
|
|
twitter === twitter2; // => false |
|
|
|
var facebook = container.lookup('connection:facebook'); |
|
var facebook2 = container.lookup('connection:facebook'); |
|
|
|
facebook === facebook2; // => false |
|
``` |
|
|
|
@method optionsForType |
|
@param {String} type |
|
@param {Object} options |
|
*/ |
|
optionsForType: function(type, options) { |
|
if (this.parent) { illegalChildOperation('optionsForType'); } |
|
|
|
this._typeOptions.set(type, options); |
|
}, |
|
|
|
/** |
|
@method options |
|
@param {String} type |
|
@param {Object} options |
|
*/ |
|
options: function(type, options) { |
|
this.optionsForType(type, options); |
|
}, |
|
|
|
/** |
|
Used only via `injection`. |
|
|
|
Provides a specialized form of injection, specifically enabling |
|
all objects of one type to be injected with a reference to another |
|
object. |
|
|
|
For example, provided each object of type `controller` needed a `router`. |
|
one would do the following: |
|
|
|
```javascript |
|
var container = new Container(); |
|
|
|
container.register('router:main', Router); |
|
container.register('controller:user', UserController); |
|
container.register('controller:post', PostController); |
|
|
|
container.typeInjection('controller', 'router', 'router:main'); |
|
|
|
var user = container.lookup('controller:user'); |
|
var post = container.lookup('controller:post'); |
|
|
|
user.router instanceof Router; //=> true |
|
post.router instanceof Router; //=> true |
|
|
|
// both controllers share the same router |
|
user.router === post.router; //=> true |
|
``` |
|
|
|
@private |
|
@method typeInjection |
|
@param {String} type |
|
@param {String} property |
|
@param {String} fullName |
|
*/ |
|
typeInjection: function(type, property, fullName) { |
|
validateFullName(fullName); |
|
if (this.parent) { illegalChildOperation('typeInjection'); } |
|
|
|
addTypeInjection(this.typeInjections, type, property, fullName); |
|
}, |
|
|
|
/** |
|
Defines injection rules. |
|
|
|
These rules are used to inject dependencies onto objects when they |
|
are instantiated. |
|
|
|
Two forms of injections are possible: |
|
|
|
* Injecting one fullName on another fullName |
|
* Injecting one fullName on a type |
|
|
|
Example: |
|
|
|
```javascript |
|
var container = new Container(); |
|
|
|
container.register('source:main', Source); |
|
container.register('model:user', User); |
|
container.register('model:post', Post); |
|
|
|
// injecting one fullName on another fullName |
|
// eg. each user model gets a post model |
|
container.injection('model:user', 'post', 'model:post'); |
|
|
|
// injecting one fullName on another type |
|
container.injection('model', 'source', 'source:main'); |
|
|
|
var user = container.lookup('model:user'); |
|
var post = container.lookup('model:post'); |
|
|
|
user.source instanceof Source; //=> true |
|
post.source instanceof Source; //=> true |
|
|
|
user.post instanceof Post; //=> true |
|
|
|
// and both models share the same source |
|
user.source === post.source; //=> true |
|
``` |
|
|
|
@method injection |
|
@param {String} factoryName |
|
@param {String} property |
|
@param {String} injectionName |
|
*/ |
|
injection: function(fullName, property, injectionName) { |
|
if (this.parent) { illegalChildOperation('injection'); } |
|
|
|
validateFullName(injectionName); |
|
var normalizedInjectionName = this.normalize(injectionName); |
|
|
|
if (fullName.indexOf(':') === -1) { |
|
return this.typeInjection(fullName, property, normalizedInjectionName); |
|
} |
|
|
|
validateFullName(fullName); |
|
var normalizedName = this.normalize(fullName); |
|
|
|
addInjection(this.injections, normalizedName, property, normalizedInjectionName); |
|
}, |
|
|
|
|
|
/** |
|
Used only via `factoryInjection`. |
|
|
|
Provides a specialized form of injection, specifically enabling |
|
all factory of one type to be injected with a reference to another |
|
object. |
|
|
|
For example, provided each factory of type `model` needed a `store`. |
|
one would do the following: |
|
|
|
```javascript |
|
var container = new Container(); |
|
|
|
container.register('store:main', SomeStore); |
|
|
|
container.factoryTypeInjection('model', 'store', 'store:main'); |
|
|
|
var store = container.lookup('store:main'); |
|
var UserFactory = container.lookupFactory('model:user'); |
|
|
|
UserFactory.store instanceof SomeStore; //=> true |
|
``` |
|
|
|
@private |
|
@method factoryTypeInjection |
|
@param {String} type |
|
@param {String} property |
|
@param {String} fullName |
|
*/ |
|
factoryTypeInjection: function(type, property, fullName) { |
|
if (this.parent) { illegalChildOperation('factoryTypeInjection'); } |
|
|
|
addTypeInjection(this.factoryTypeInjections, type, property, this.normalize(fullName)); |
|
}, |
|
|
|
/** |
|
Defines factory injection rules. |
|
|
|
Similar to regular injection rules, but are run against factories, via |
|
`Container#lookupFactory`. |
|
|
|
These rules are used to inject objects onto factories when they |
|
are looked up. |
|
|
|
Two forms of injections are possible: |
|
|
|
* Injecting one fullName on another fullName |
|
* Injecting one fullName on a type |
|
|
|
Example: |
|
|
|
```javascript |
|
var container = new Container(); |
|
|
|
container.register('store:main', Store); |
|
container.register('store:secondary', OtherStore); |
|
container.register('model:user', User); |
|
container.register('model:post', Post); |
|
|
|
// injecting one fullName on another type |
|
container.factoryInjection('model', 'store', 'store:main'); |
|
|
|
// injecting one fullName on another fullName |
|
container.factoryInjection('model:post', 'secondaryStore', 'store:secondary'); |
|
|
|
var UserFactory = container.lookupFactory('model:user'); |
|
var PostFactory = container.lookupFactory('model:post'); |
|
var store = container.lookup('store:main'); |
|
|
|
UserFactory.store instanceof Store; //=> true |
|
UserFactory.secondaryStore instanceof OtherStore; //=> false |
|
|
|
PostFactory.store instanceof Store; //=> true |
|
PostFactory.secondaryStore instanceof OtherStore; //=> true |
|
|
|
// and both models share the same source instance |
|
UserFactory.store === PostFactory.store; //=> true |
|
``` |
|
|
|
@method factoryInjection |
|
@param {String} factoryName |
|
@param {String} property |
|
@param {String} injectionName |
|
*/ |
|
factoryInjection: function(fullName, property, injectionName) { |
|
if (this.parent) { illegalChildOperation('injection'); } |
|
|
|
var normalizedName = this.normalize(fullName); |
|
var normalizedInjectionName = this.normalize(injectionName); |
|
|
|
validateFullName(injectionName); |
|
|
|
if (fullName.indexOf(':') === -1) { |
|
return this.factoryTypeInjection(normalizedName, property, normalizedInjectionName); |
|
} |
|
|
|
validateFullName(fullName); |
|
|
|
addInjection(this.factoryInjections, normalizedName, property, normalizedInjectionName); |
|
}, |
|
|
|
/** |
|
A depth first traversal, destroying the container, its descendant containers and all |
|
their managed objects. |
|
|
|
@method destroy |
|
*/ |
|
destroy: function() { |
|
for (var i=0, l=this.children.length; i<l; i++) { |
|
this.children[i].destroy(); |
|
} |
|
|
|
this.children = []; |
|
|
|
eachDestroyable(this, function(item) { |
|
item.destroy(); |
|
}); |
|
|
|
this.parent = undefined; |
|
this.isDestroyed = true; |
|
}, |
|
|
|
/** |
|
@method reset |
|
*/ |
|
reset: function() { |
|
for (var i=0, l=this.children.length; i<l; i++) { |
|
resetCache(this.children[i]); |
|
} |
|
resetCache(this); |
|
} |
|
}; |
|
|
|
function has(container, fullName){ |
|
if (container.cache.has(fullName)) { |
|
return true; |
|
} |
|
|
|
return !!container.resolve(fullName); |
|
} |
|
|
|
function lookup(container, fullName, options) { |
|
options = options || {}; |
|
|
|
if (container.cache.has(fullName) && options.singleton !== false) { |
|
return container.cache.get(fullName); |
|
} |
|
|
|
var value = instantiate(container, fullName); |
|
|
|
if (value === undefined) { return; } |
|
|
|
if (isSingleton(container, fullName) && options.singleton !== false) { |
|
container.cache.set(fullName, value); |
|
} |
|
|
|
return value; |
|
} |
|
|
|
function illegalChildOperation(operation) { |
|
throw new Error(operation + " is not currently supported on child containers"); |
|
} |
|
|
|
function isSingleton(container, fullName) { |
|
var singleton = option(container, fullName, 'singleton'); |
|
|
|
return singleton !== false; |
|
} |
|
|
|
function buildInjections(container, injections) { |
|
var hash = {}; |
|
|
|
if (!injections) { return hash; } |
|
|
|
var injection, injectable; |
|
|
|
for (var i=0, l=injections.length; i<l; i++) { |
|
injection = injections[i]; |
|
injectable = lookup(container, injection.fullName); |
|
|
|
if (injectable !== undefined) { |
|
hash[injection.property] = injectable; |
|
} else { |
|
throw new Error('Attempting to inject an unknown injection: `' + injection.fullName + '`'); |
|
} |
|
} |
|
|
|
return hash; |
|
} |
|
|
|
function option(container, fullName, optionName) { |
|
var options = container._options.get(fullName); |
|
|
|
if (options && options[optionName] !== undefined) { |
|
return options[optionName]; |
|
} |
|
|
|
var type = fullName.split(":")[0]; |
|
options = container._typeOptions.get(type); |
|
|
|
if (options) { |
|
return options[optionName]; |
|
} |
|
} |
|
|
|
function factoryFor(container, fullName) { |
|
var name = fullName; |
|
var factory = container.resolve(name); |
|
var injectedFactory; |
|
var cache = container.factoryCache; |
|
var type = fullName.split(":")[0]; |
|
|
|
if (factory === undefined) { return; } |
|
|
|
if (cache.has(fullName)) { |
|
return cache.get(fullName); |
|
} |
|
|
|
if (!factory || typeof factory.extend !== 'function' || (!Ember.MODEL_FACTORY_INJECTIONS && type === 'model')) { |
|
// TODO: think about a 'safe' merge style extension |
|
// for now just fallback to create time injection |
|
return factory; |
|
} else { |
|
|
|
var injections = injectionsFor(container, fullName); |
|
var factoryInjections = factoryInjectionsFor(container, fullName); |
|
|
|
factoryInjections._toString = container.makeToString(factory, fullName); |
|
|
|
injectedFactory = factory.extend(injections); |
|
injectedFactory.reopenClass(factoryInjections); |
|
|
|
cache.set(fullName, injectedFactory); |
|
|
|
return injectedFactory; |
|
} |
|
} |
|
|
|
function injectionsFor(container, fullName) { |
|
var splitName = fullName.split(":"), |
|
type = splitName[0], |
|
injections = []; |
|
|
|
injections = injections.concat(container.typeInjections.get(type) || []); |
|
injections = injections.concat(container.injections[fullName] || []); |
|
|
|
injections = buildInjections(container, injections); |
|
injections._debugContainerKey = fullName; |
|
injections.container = container; |
|
|
|
return injections; |
|
} |
|
|
|
function factoryInjectionsFor(container, fullName) { |
|
var splitName = fullName.split(":"), |
|
type = splitName[0], |
|
factoryInjections = []; |
|
|
|
factoryInjections = factoryInjections.concat(container.factoryTypeInjections.get(type) || []); |
|
factoryInjections = factoryInjections.concat(container.factoryInjections[fullName] || []); |
|
|
|
factoryInjections = buildInjections(container, factoryInjections); |
|
factoryInjections._debugContainerKey = fullName; |
|
|
|
return factoryInjections; |
|
} |
|
|
|
function instantiate(container, fullName) { |
|
var factory = factoryFor(container, fullName); |
|
|
|
if (option(container, fullName, 'instantiate') === false) { |
|
return factory; |
|
} |
|
|
|
if (factory) { |
|
if (typeof factory.extend === 'function') { |
|
// assume the factory was extendable and is already injected |
|
return factory.create(); |
|
} else { |
|
// assume the factory was extendable |
|
// to create time injections |
|
// TODO: support new'ing for instantiation and merge injections for pure JS Functions |
|
return factory.create(injectionsFor(container, fullName)); |
|
} |
|
} |
|
} |
|
|
|
function eachDestroyable(container, callback) { |
|
container.cache.eachLocal(function(key, value) { |
|
if (option(container, key, 'instantiate') === false) { return; } |
|
callback(value); |
|
}); |
|
} |
|
|
|
function resetCache(container) { |
|
container.cache.eachLocal(function(key, value) { |
|
if (option(container, key, 'instantiate') === false) { return; } |
|
value.destroy(); |
|
}); |
|
container.cache.dict = {}; |
|
} |
|
|
|
function addTypeInjection(rules, type, property, fullName) { |
|
var injections = rules.get(type); |
|
|
|
if (!injections) { |
|
injections = []; |
|
rules.set(type, injections); |
|
} |
|
|
|
injections.push({ |
|
property: property, |
|
fullName: fullName |
|
}); |
|
} |
|
|
|
var VALID_FULL_NAME_REGEXP = /^[^:]+.+:[^:]+$/; |
|
function validateFullName(fullName) { |
|
if (!VALID_FULL_NAME_REGEXP.test(fullName)) { |
|
throw new TypeError('Invalid Fullname, expected: `type:name` got: ' + fullName); |
|
} |
|
} |
|
|
|
function addInjection(rules, factoryName, property, injectionName) { |
|
var injections = rules[factoryName] = rules[factoryName] || []; |
|
injections.push({ property: property, fullName: injectionName }); |
|
} |
|
|
|
__exports__["default"] = Container; |
|
}); |
|
define("container/inheriting_dict", |
|
["exports"], |
|
function(__exports__) { |
|
"use strict"; |
|
// A safe and simple inheriting object. |
|
function InheritingDict(parent) { |
|
this.parent = parent; |
|
this.dict = {}; |
|
} |
|
|
|
InheritingDict.prototype = { |
|
|
|
/** |
|
@property parent |
|
@type InheritingDict |
|
@default null |
|
*/ |
|
|
|
parent: null, |
|
|
|
/** |
|
Object used to store the current nodes data. |
|
|
|
@property dict |
|
@type Object |
|
@default Object |
|
*/ |
|
dict: null, |
|
|
|
/** |
|
Retrieve the value given a key, if the value is present at the current |
|
level use it, otherwise walk up the parent hierarchy and try again. If |
|
no matching key is found, return undefined. |
|
|
|
@method get |
|
@param {String} key |
|
@return {any} |
|
*/ |
|
get: function(key) { |
|
var dict = this.dict; |
|
|
|
if (dict.hasOwnProperty(key)) { |
|
return dict[key]; |
|
} |
|
|
|
if (this.parent) { |
|
return this.parent.get(key); |
|
} |
|
}, |
|
|
|
/** |
|
Set the given value for the given key, at the current level. |
|
|
|
@method set |
|
@param {String} key |
|
@param {Any} value |
|
*/ |
|
set: function(key, value) { |
|
this.dict[key] = value; |
|
}, |
|
|
|
/** |
|
Delete the given key |
|
|
|
@method remove |
|
@param {String} key |
|
*/ |
|
remove: function(key) { |
|
delete this.dict[key]; |
|
}, |
|
|
|
/** |
|
Check for the existence of given a key, if the key is present at the current |
|
level return true, otherwise walk up the parent hierarchy and try again. If |
|
no matching key is found, return false. |
|
|
|
@method has |
|
@param {String} key |
|
@return {Boolean} |
|
*/ |
|
has: function(key) { |
|
var dict = this.dict; |
|
|
|
if (dict.hasOwnProperty(key)) { |
|
return true; |
|
} |
|
|
|
if (this.parent) { |
|
return this.parent.has(key); |
|
} |
|
|
|
return false; |
|
}, |
|
|
|
/** |
|
Iterate and invoke a callback for each local key-value pair. |
|
|
|
@method eachLocal |
|
@param {Function} callback |
|
@param {Object} binding |
|
*/ |
|
eachLocal: function(callback, binding) { |
|
var dict = this.dict; |
|
|
|
for (var prop in dict) { |
|
if (dict.hasOwnProperty(prop)) { |
|
callback.call(binding, prop, dict[prop]); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
__exports__["default"] = InheritingDict; |
|
}); |
|
define("container", |
|
["container/container","exports"], |
|
function(__dependency1__, __exports__) { |
|
"use strict"; |
|
/** |
|
Public api for the container is still in flux. |
|
The public api, specified on the application namespace should be considered the stable api. |
|
// @module container |
|
@private |
|
*/ |
|
|
|
/* |
|
Flag to enable/disable model factory injections (disabled by default) |
|
If model factory injections are enabled, models should not be |
|
accessed globally (only through `container.lookupFactory('model:modelName'))`); |
|
*/ |
|
Ember.MODEL_FACTORY_INJECTIONS = false || !!Ember.ENV.MODEL_FACTORY_INJECTIONS; |
|
|
|
var Container = __dependency1__["default"]; |
|
|
|
__exports__["default"] = Container; |
|
}); |
|
})(); |
|
|
|
(function() { |
|
/*globals ENV */ |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var indexOf = Ember.EnumerableUtils.indexOf; |
|
|
|
/** |
|
This will compare two javascript values of possibly different types. |
|
It will tell you which one is greater than the other by returning: |
|
|
|
- -1 if the first is smaller than the second, |
|
- 0 if both are equal, |
|
- 1 if the first is greater than the second. |
|
|
|
The order is calculated based on `Ember.ORDER_DEFINITION`, if types are different. |
|
In case they have the same type an appropriate comparison for this type is made. |
|
|
|
```javascript |
|
Ember.compare('hello', 'hello'); // 0 |
|
Ember.compare('abc', 'dfg'); // -1 |
|
Ember.compare(2, 1); // 1 |
|
``` |
|
|
|
@method compare |
|
@for Ember |
|
@param {Object} v First value to compare |
|
@param {Object} w Second value to compare |
|
@return {Number} -1 if v < w, 0 if v = w and 1 if v > w. |
|
*/ |
|
Ember.compare = function compare(v, w) { |
|
if (v === w) { return 0; } |
|
|
|
var type1 = Ember.typeOf(v); |
|
var type2 = Ember.typeOf(w); |
|
|
|
var Comparable = Ember.Comparable; |
|
if (Comparable) { |
|
if (type1==='instance' && Comparable.detect(v.constructor)) { |
|
return v.constructor.compare(v, w); |
|
} |
|
|
|
if (type2 === 'instance' && Comparable.detect(w.constructor)) { |
|
return 1-w.constructor.compare(w, v); |
|
} |
|
} |
|
|
|
// If we haven't yet generated a reverse-mapping of Ember.ORDER_DEFINITION, |
|
// do so now. |
|
var mapping = Ember.ORDER_DEFINITION_MAPPING; |
|
if (!mapping) { |
|
var order = Ember.ORDER_DEFINITION; |
|
mapping = Ember.ORDER_DEFINITION_MAPPING = {}; |
|
var idx, len; |
|
for (idx = 0, len = order.length; idx < len; ++idx) { |
|
mapping[order[idx]] = idx; |
|
} |
|
|
|
// We no longer need Ember.ORDER_DEFINITION. |
|
delete Ember.ORDER_DEFINITION; |
|
} |
|
|
|
var type1Index = mapping[type1]; |
|
var type2Index = mapping[type2]; |
|
|
|
if (type1Index < type2Index) { return -1; } |
|
if (type1Index > type2Index) { return 1; } |
|
|
|
// types are equal - so we have to check values now |
|
switch (type1) { |
|
case 'boolean': |
|
case 'number': |
|
if (v < w) { return -1; } |
|
if (v > w) { return 1; } |
|
return 0; |
|
|
|
case 'string': |
|
var comp = v.localeCompare(w); |
|
if (comp < 0) { return -1; } |
|
if (comp > 0) { return 1; } |
|
return 0; |
|
|
|
case 'array': |
|
var vLen = v.length; |
|
var wLen = w.length; |
|
var l = Math.min(vLen, wLen); |
|
var r = 0; |
|
var i = 0; |
|
while (r === 0 && i < l) { |
|
r = compare(v[i],w[i]); |
|
i++; |
|
} |
|
if (r !== 0) { return r; } |
|
|
|
// all elements are equal now |
|
// shorter array should be ordered first |
|
if (vLen < wLen) { return -1; } |
|
if (vLen > wLen) { return 1; } |
|
// arrays are equal now |
|
return 0; |
|
|
|
case 'instance': |
|
if (Ember.Comparable && Ember.Comparable.detect(v)) { |
|
return v.compare(v, w); |
|
} |
|
return 0; |
|
|
|
case 'date': |
|
var vNum = v.getTime(); |
|
var wNum = w.getTime(); |
|
if (vNum < wNum) { return -1; } |
|
if (vNum > wNum) { return 1; } |
|
return 0; |
|
|
|
default: |
|
return 0; |
|
} |
|
}; |
|
|
|
function _copy(obj, deep, seen, copies) { |
|
var ret, loc, key; |
|
|
|
// primitive data types are immutable, just return them. |
|
if ('object' !== typeof obj || obj===null) return obj; |
|
|
|
// avoid cyclical loops |
|
if (deep && (loc=indexOf(seen, obj))>=0) return copies[loc]; |
|
|
|
Ember.assert('Cannot clone an Ember.Object that does not implement Ember.Copyable', !(obj instanceof Ember.Object) || (Ember.Copyable && Ember.Copyable.detect(obj))); |
|
|
|
// IMPORTANT: this specific test will detect a native array only. Any other |
|
// object will need to implement Copyable. |
|
if (Ember.typeOf(obj) === 'array') { |
|
ret = obj.slice(); |
|
if (deep) { |
|
loc = ret.length; |
|
while(--loc>=0) ret[loc] = _copy(ret[loc], deep, seen, copies); |
|
} |
|
} else if (Ember.Copyable && Ember.Copyable.detect(obj)) { |
|
ret = obj.copy(deep, seen, copies); |
|
} else if (obj instanceof Date) { |
|
ret = new Date(obj.getTime()); |
|
} else { |
|
ret = {}; |
|
for(key in obj) { |
|
if (!obj.hasOwnProperty(key)) continue; |
|
|
|
// Prevents browsers that don't respect non-enumerability from |
|
// copying internal Ember properties |
|
if (key.substring(0,2) === '__') continue; |
|
|
|
ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key]; |
|
} |
|
} |
|
|
|
if (deep) { |
|
seen.push(obj); |
|
copies.push(ret); |
|
} |
|
|
|
return ret; |
|
} |
|
|
|
/** |
|
Creates a clone of the passed object. This function can take just about |
|
any type of object and create a clone of it, including primitive values |
|
(which are not actually cloned because they are immutable). |
|
|
|
If the passed object implements the `clone()` method, then this function |
|
will simply call that method and return the result. |
|
|
|
@method copy |
|
@for Ember |
|
@param {Object} obj The object to clone |
|
@param {Boolean} deep If true, a deep copy of the object is made |
|
@return {Object} The cloned object |
|
*/ |
|
Ember.copy = function(obj, deep) { |
|
// fast paths |
|
if ('object' !== typeof obj || obj===null) return obj; // can't copy primitives |
|
if (Ember.Copyable && Ember.Copyable.detect(obj)) return obj.copy(deep); |
|
return _copy(obj, deep, deep ? [] : null, deep ? [] : null); |
|
}; |
|
|
|
/** |
|
Compares two objects, returning true if they are logically equal. This is |
|
a deeper comparison than a simple triple equal. For sets it will compare the |
|
internal objects. For any other object that implements `isEqual()` it will |
|
respect that method. |
|
|
|
```javascript |
|
Ember.isEqual('hello', 'hello'); // true |
|
Ember.isEqual(1, 2); // false |
|
Ember.isEqual([4,2], [4,2]); // false |
|
``` |
|
|
|
@method isEqual |
|
@for Ember |
|
@param {Object} a first object to compare |
|
@param {Object} b second object to compare |
|
@return {Boolean} |
|
*/ |
|
Ember.isEqual = function(a, b) { |
|
if (a && 'function'===typeof a.isEqual) return a.isEqual(b); |
|
return a === b; |
|
}; |
|
|
|
// Used by Ember.compare |
|
Ember.ORDER_DEFINITION = Ember.ENV.ORDER_DEFINITION || [ |
|
'undefined', |
|
'null', |
|
'boolean', |
|
'number', |
|
'string', |
|
'array', |
|
'object', |
|
'instance', |
|
'function', |
|
'class', |
|
'date' |
|
]; |
|
|
|
/** |
|
Returns all of the keys defined on an object or hash. This is useful |
|
when inspecting objects for debugging. On browsers that support it, this |
|
uses the native `Object.keys` implementation. |
|
|
|
@method keys |
|
@for Ember |
|
@param {Object} obj |
|
@return {Array} Array containing keys of obj |
|
*/ |
|
Ember.keys = Object.keys; |
|
|
|
if (!Ember.keys || Ember.create.isSimulated) { |
|
var prototypeProperties = [ |
|
'constructor', |
|
'hasOwnProperty', |
|
'isPrototypeOf', |
|
'propertyIsEnumerable', |
|
'valueOf', |
|
'toLocaleString', |
|
'toString' |
|
], |
|
pushPropertyName = function(obj, array, key) { |
|
// Prevents browsers that don't respect non-enumerability from |
|
// copying internal Ember properties |
|
if (key.substring(0,2) === '__') return; |
|
if (key === '_super') return; |
|
if (indexOf(array, key) >= 0) return; |
|
if (!obj.hasOwnProperty(key)) return; |
|
|
|
array.push(key); |
|
}; |
|
|
|
Ember.keys = function(obj) { |
|
var ret = [], key; |
|
for (key in obj) { |
|
pushPropertyName(obj, ret, key); |
|
} |
|
|
|
// IE8 doesn't enumerate property that named the same as prototype properties. |
|
for (var i = 0, l = prototypeProperties.length; i < l; i++) { |
|
key = prototypeProperties[i]; |
|
|
|
pushPropertyName(obj, ret, key); |
|
} |
|
|
|
return ret; |
|
}; |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var STRING_DASHERIZE_REGEXP = (/[ _]/g); |
|
var STRING_DASHERIZE_CACHE = {}; |
|
var STRING_DECAMELIZE_REGEXP = (/([a-z\d])([A-Z])/g); |
|
var STRING_CAMELIZE_REGEXP = (/(\-|_|\.|\s)+(.)?/g); |
|
var STRING_UNDERSCORE_REGEXP_1 = (/([a-z\d])([A-Z]+)/g); |
|
var STRING_UNDERSCORE_REGEXP_2 = (/\-|\s+/g); |
|
var STRING_PARAMETERIZE_REGEXP_1 = (/[_|\/|\s]+/g); |
|
var STRING_PARAMETERIZE_REGEXP_2 = (/[^a-z0-9\-]+/gi); |
|
var STRING_PARAMETERIZE_REGEXP_3 = (/[\-]+/g); |
|
var STRING_PARAMETERIZE_REGEXP_4 = (/^-+|-+$/g); |
|
|
|
/** |
|
Defines the hash of localized strings for the current language. Used by |
|
the `Ember.String.loc()` helper. To localize, add string values to this |
|
hash. |
|
|
|
@property STRINGS |
|
@for Ember |
|
@type Hash |
|
*/ |
|
Ember.STRINGS = {}; |
|
|
|
/** |
|
Defines string helper methods including string formatting and localization. |
|
Unless `Ember.EXTEND_PROTOTYPES.String` is `false` these methods will also be |
|
added to the `String.prototype` as well. |
|
|
|
@class String |
|
@namespace Ember |
|
@static |
|
*/ |
|
Ember.String = { |
|
|
|
/** |
|
Apply formatting options to the string. This will look for occurrences |
|
of "%@" in your string and substitute them with the arguments you pass into |
|
this method. If you want to control the specific order of replacement, |
|
you can add a number after the key as well to indicate which argument |
|
you want to insert. |
|
|
|
Ordered insertions are most useful when building loc strings where values |
|
you need to insert may appear in different orders. |
|
|
|
```javascript |
|
"Hello %@ %@".fmt('John', 'Doe'); // "Hello John Doe" |
|
"Hello %@2, %@1".fmt('John', 'Doe'); // "Hello Doe, John" |
|
``` |
|
|
|
@method fmt |
|
@param {String} str The string to format |
|
@param {Array} formats An array of parameters to interpolate into string. |
|
@return {String} formatted string |
|
*/ |
|
fmt: function(str, formats) { |
|
// first, replace any ORDERED replacements. |
|
var idx = 0; // the current index for non-numerical replacements |
|
return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { |
|
argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; |
|
s = formats[argIndex]; |
|
return (s === null) ? '(null)' : (s === undefined) ? '' : Ember.inspect(s); |
|
}) ; |
|
}, |
|
|
|
/** |
|
Formats the passed string, but first looks up the string in the localized |
|
strings hash. This is a convenient way to localize text. See |
|
`Ember.String.fmt()` for more information on formatting. |
|
|
|
Note that it is traditional but not required to prefix localized string |
|
keys with an underscore or other character so you can easily identify |
|
localized strings. |
|
|
|
```javascript |
|
Ember.STRINGS = { |
|
'_Hello World': 'Bonjour le monde', |
|
'_Hello %@ %@': 'Bonjour %@ %@' |
|
}; |
|
|
|
Ember.String.loc("_Hello World"); // 'Bonjour le monde'; |
|
Ember.String.loc("_Hello %@ %@", ["John", "Smith"]); // "Bonjour John Smith"; |
|
``` |
|
|
|
@method loc |
|
@param {String} str The string to format |
|
@param {Array} formats Optional array of parameters to interpolate into string. |
|
@return {String} formatted string |
|
*/ |
|
loc: function(str, formats) { |
|
str = Ember.STRINGS[str] || str; |
|
return Ember.String.fmt(str, formats) ; |
|
}, |
|
|
|
/** |
|
Splits a string into separate units separated by spaces, eliminating any |
|
empty strings in the process. This is a convenience method for split that |
|
is mostly useful when applied to the `String.prototype`. |
|
|
|
```javascript |
|
Ember.String.w("alpha beta gamma").forEach(function(key) { |
|
console.log(key); |
|
}); |
|
|
|
// > alpha |
|
// > beta |
|
// > gamma |
|
``` |
|
|
|
@method w |
|
@param {String} str The string to split |
|
@return {String} split string |
|
*/ |
|
w: function(str) { return str.split(/\s+/); }, |
|
|
|
/** |
|
Converts a camelized string into all lower case separated by underscores. |
|
|
|
```javascript |
|
'innerHTML'.decamelize(); // 'inner_html' |
|
'action_name'.decamelize(); // 'action_name' |
|
'css-class-name'.decamelize(); // 'css-class-name' |
|
'my favorite items'.decamelize(); // 'my favorite items' |
|
``` |
|
|
|
@method decamelize |
|
@param {String} str The string to decamelize. |
|
@return {String} the decamelized string. |
|
*/ |
|
decamelize: function(str) { |
|
return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); |
|
}, |
|
|
|
/** |
|
Replaces underscores, spaces, or camelCase with dashes. |
|
|
|
```javascript |
|
'innerHTML'.dasherize(); // 'inner-html' |
|
'action_name'.dasherize(); // 'action-name' |
|
'css-class-name'.dasherize(); // 'css-class-name' |
|
'my favorite items'.dasherize(); // 'my-favorite-items' |
|
``` |
|
|
|
@method dasherize |
|
@param {String} str The string to dasherize. |
|
@return {String} the dasherized string. |
|
*/ |
|
dasherize: function(str) { |
|
var cache = STRING_DASHERIZE_CACHE, |
|
hit = cache.hasOwnProperty(str), |
|
ret; |
|
|
|
if (hit) { |
|
return cache[str]; |
|
} else { |
|
ret = Ember.String.decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-'); |
|
cache[str] = ret; |
|
} |
|
|
|
return ret; |
|
}, |
|
|
|
/** |
|
Returns the lowerCamelCase form of a string. |
|
|
|
```javascript |
|
'innerHTML'.camelize(); // 'innerHTML' |
|
'action_name'.camelize(); // 'actionName' |
|
'css-class-name'.camelize(); // 'cssClassName' |
|
'my favorite items'.camelize(); // 'myFavoriteItems' |
|
'My Favorite Items'.camelize(); // 'myFavoriteItems' |
|
``` |
|
|
|
@method camelize |
|
@param {String} str The string to camelize. |
|
@return {String} the camelized string. |
|
*/ |
|
camelize: function(str) { |
|
return str.replace(STRING_CAMELIZE_REGEXP, function(match, separator, chr) { |
|
return chr ? chr.toUpperCase() : ''; |
|
}).replace(/^([A-Z])/, function(match, separator, chr) { |
|
return match.toLowerCase(); |
|
}); |
|
}, |
|
|
|
/** |
|
Returns the UpperCamelCase form of a string. |
|
|
|
```javascript |
|
'innerHTML'.classify(); // 'InnerHTML' |
|
'action_name'.classify(); // 'ActionName' |
|
'css-class-name'.classify(); // 'CssClassName' |
|
'my favorite items'.classify(); // 'MyFavoriteItems' |
|
``` |
|
|
|
@method classify |
|
@param {String} str the string to classify |
|
@return {String} the classified string |
|
*/ |
|
classify: function(str) { |
|
var parts = str.split("."), |
|
out = []; |
|
|
|
for (var i=0, l=parts.length; i<l; i++) { |
|
var camelized = Ember.String.camelize(parts[i]); |
|
out.push(camelized.charAt(0).toUpperCase() + camelized.substr(1)); |
|
} |
|
|
|
return out.join("."); |
|
}, |
|
|
|
/** |
|
More general than decamelize. Returns the lower\_case\_and\_underscored |
|
form of a string. |
|
|
|
```javascript |
|
'innerHTML'.underscore(); // 'inner_html' |
|
'action_name'.underscore(); // 'action_name' |
|
'css-class-name'.underscore(); // 'css_class_name' |
|
'my favorite items'.underscore(); // 'my_favorite_items' |
|
``` |
|
|
|
@method underscore |
|
@param {String} str The string to underscore. |
|
@return {String} the underscored string. |
|
*/ |
|
underscore: function(str) { |
|
return str.replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2'). |
|
replace(STRING_UNDERSCORE_REGEXP_2, '_').toLowerCase(); |
|
}, |
|
|
|
/** |
|
Returns the Capitalized form of a string |
|
|
|
```javascript |
|
'innerHTML'.capitalize() // 'InnerHTML' |
|
'action_name'.capitalize() // 'Action_name' |
|
'css-class-name'.capitalize() // 'Css-class-name' |
|
'my favorite items'.capitalize() // 'My favorite items' |
|
``` |
|
|
|
@method capitalize |
|
@param {String} str The string to capitalize. |
|
@return {String} The capitalized string. |
|
*/ |
|
capitalize: function(str) { |
|
return str.charAt(0).toUpperCase() + str.substr(1); |
|
} |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
|
|
|
|
var fmt = Ember.String.fmt, |
|
w = Ember.String.w, |
|
loc = Ember.String.loc, |
|
camelize = Ember.String.camelize, |
|
decamelize = Ember.String.decamelize, |
|
dasherize = Ember.String.dasherize, |
|
underscore = Ember.String.underscore, |
|
capitalize = Ember.String.capitalize, |
|
classify = Ember.String.classify; |
|
|
|
if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { |
|
|
|
/** |
|
See [Ember.String.fmt](/api/classes/Ember.String.html#method_fmt). |
|
|
|
@method fmt |
|
@for String |
|
*/ |
|
String.prototype.fmt = function() { |
|
return fmt(this, arguments); |
|
}; |
|
|
|
/** |
|
See [Ember.String.w](/api/classes/Ember.String.html#method_w). |
|
|
|
@method w |
|
@for String |
|
*/ |
|
String.prototype.w = function() { |
|
return w(this); |
|
}; |
|
|
|
/** |
|
See [Ember.String.loc](/api/classes/Ember.String.html#method_loc). |
|
|
|
@method loc |
|
@for String |
|
*/ |
|
String.prototype.loc = function() { |
|
return loc(this, arguments); |
|
}; |
|
|
|
/** |
|
See [Ember.String.camelize](/api/classes/Ember.String.html#method_camelize). |
|
|
|
@method camelize |
|
@for String |
|
*/ |
|
String.prototype.camelize = function() { |
|
return camelize(this); |
|
}; |
|
|
|
/** |
|
See [Ember.String.decamelize](/api/classes/Ember.String.html#method_decamelize). |
|
|
|
@method decamelize |
|
@for String |
|
*/ |
|
String.prototype.decamelize = function() { |
|
return decamelize(this); |
|
}; |
|
|
|
/** |
|
See [Ember.String.dasherize](/api/classes/Ember.String.html#method_dasherize). |
|
|
|
@method dasherize |
|
@for String |
|
*/ |
|
String.prototype.dasherize = function() { |
|
return dasherize(this); |
|
}; |
|
|
|
/** |
|
See [Ember.String.underscore](/api/classes/Ember.String.html#method_underscore). |
|
|
|
@method underscore |
|
@for String |
|
*/ |
|
String.prototype.underscore = function() { |
|
return underscore(this); |
|
}; |
|
|
|
/** |
|
See [Ember.String.classify](/api/classes/Ember.String.html#method_classify). |
|
|
|
@method classify |
|
@for String |
|
*/ |
|
String.prototype.classify = function() { |
|
return classify(this); |
|
}; |
|
|
|
/** |
|
See [Ember.String.capitalize](/api/classes/Ember.String.html#method_capitalize). |
|
|
|
@method capitalize |
|
@for String |
|
*/ |
|
String.prototype.capitalize = function() { |
|
return capitalize(this); |
|
}; |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var get = Ember.get, |
|
set = Ember.set, |
|
slice = Array.prototype.slice, |
|
getProperties = Ember.getProperties; |
|
|
|
/** |
|
## Overview |
|
|
|
This mixin provides properties and property observing functionality, core |
|
features of the Ember object model. |
|
|
|
Properties and observers allow one object to observe changes to a |
|
property on another object. This is one of the fundamental ways that |
|
models, controllers and views communicate with each other in an Ember |
|
application. |
|
|
|
Any object that has this mixin applied can be used in observer |
|
operations. That includes `Ember.Object` and most objects you will |
|
interact with as you write your Ember application. |
|
|
|
Note that you will not generally apply this mixin to classes yourself, |
|
but you will use the features provided by this module frequently, so it |
|
is important to understand how to use it. |
|
|
|
## Using `get()` and `set()` |
|
|
|
Because of Ember's support for bindings and observers, you will always |
|
access properties using the get method, and set properties using the |
|
set method. This allows the observing objects to be notified and |
|
computed properties to be handled properly. |
|
|
|
More documentation about `get` and `set` are below. |
|
|
|
## Observing Property Changes |
|
|
|
You typically observe property changes simply by adding the `observes` |
|
call to the end of your method declarations in classes that you write. |
|
For example: |
|
|
|
```javascript |
|
Ember.Object.extend({ |
|
valueObserver: function() { |
|
// Executes whenever the "value" property changes |
|
}.observes('value') |
|
}); |
|
``` |
|
|
|
Although this is the most common way to add an observer, this capability |
|
is actually built into the `Ember.Object` class on top of two methods |
|
defined in this mixin: `addObserver` and `removeObserver`. You can use |
|
these two methods to add and remove observers yourself if you need to |
|
do so at runtime. |
|
|
|
To add an observer for a property, call: |
|
|
|
```javascript |
|
object.addObserver('propertyKey', targetObject, targetAction) |
|
``` |
|
|
|
This will call the `targetAction` method on the `targetObject` whenever |
|
the value of the `propertyKey` changes. |
|
|
|
Note that if `propertyKey` is a computed property, the observer will be |
|
called when any of the property dependencies are changed, even if the |
|
resulting value of the computed property is unchanged. This is necessary |
|
because computed properties are not computed until `get` is called. |
|
|
|
@class Observable |
|
@namespace Ember |
|
*/ |
|
Ember.Observable = Ember.Mixin.create({ |
|
|
|
/** |
|
Retrieves the value of a property from the object. |
|
|
|
This method is usually similar to using `object[keyName]` or `object.keyName`, |
|
however it supports both computed properties and the unknownProperty |
|
handler. |
|
|
|
Because `get` unifies the syntax for accessing all these kinds |
|
of properties, it can make many refactorings easier, such as replacing a |
|
simple property with a computed property, or vice versa. |
|
|
|
### Computed Properties |
|
|
|
Computed properties are methods defined with the `property` modifier |
|
declared at the end, such as: |
|
|
|
```javascript |
|
fullName: function() { |
|
return this.get('firstName') + ' ' + this.get('lastName'); |
|
}.property('firstName', 'lastName') |
|
``` |
|
|
|
When you call `get` on a computed property, the function will be |
|
called and the return value will be returned instead of the function |
|
itself. |
|
|
|
### Unknown Properties |
|
|
|
Likewise, if you try to call `get` on a property whose value is |
|
`undefined`, the `unknownProperty()` method will be called on the object. |
|
If this method returns any value other than `undefined`, it will be returned |
|
instead. This allows you to implement "virtual" properties that are |
|
not defined upfront. |
|
|
|
@method get |
|
@param {String} keyName The property to retrieve |
|
@return {Object} The property value or undefined. |
|
*/ |
|
get: function(keyName) { |
|
return get(this, keyName); |
|
}, |
|
|
|
/** |
|
To get multiple properties at once, call `getProperties` |
|
with a list of strings or an array: |
|
|
|
```javascript |
|
record.getProperties('firstName', 'lastName', 'zipCode'); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } |
|
``` |
|
|
|
is equivalent to: |
|
|
|
```javascript |
|
record.getProperties(['firstName', 'lastName', 'zipCode']); // { firstName: 'John', lastName: 'Doe', zipCode: '10011' } |
|
``` |
|
|
|
@method getProperties |
|
@param {String...|Array} list of keys to get |
|
@return {Hash} |
|
*/ |
|
getProperties: function() { |
|
return getProperties.apply(null, [this].concat(slice.call(arguments))); |
|
}, |
|
|
|
/** |
|
Sets the provided key or path to the value. |
|
|
|
This method is generally very similar to calling `object[key] = value` or |
|
`object.key = value`, except that it provides support for computed |
|
properties, the `setUnknownProperty()` method and property observers. |
|
|
|
### Computed Properties |
|
|
|
If you try to set a value on a key that has a computed property handler |
|
defined (see the `get()` method for an example), then `set()` will call |
|
that method, passing both the value and key instead of simply changing |
|
the value itself. This is useful for those times when you need to |
|
implement a property that is composed of one or more member |
|
properties. |
|
|
|
### Unknown Properties |
|
|
|
If you try to set a value on a key that is undefined in the target |
|
object, then the `setUnknownProperty()` handler will be called instead. This |
|
gives you an opportunity to implement complex "virtual" properties that |
|
are not predefined on the object. If `setUnknownProperty()` returns |
|
undefined, then `set()` will simply set the value on the object. |
|
|
|
### Property Observers |
|
|
|
In addition to changing the property, `set()` will also register a property |
|
change with the object. Unless you have placed this call inside of a |
|
`beginPropertyChanges()` and `endPropertyChanges(),` any "local" observers |
|
(i.e. observer methods declared on the same object), will be called |
|
immediately. Any "remote" observers (i.e. observer methods declared on |
|
another object) will be placed in a queue and called at a later time in a |
|
coalesced manner. |
|
|
|
### Chaining |
|
|
|
In addition to property changes, `set()` returns the value of the object |
|
itself so you can do chaining like this: |
|
|
|
```javascript |
|
record.set('firstName', 'Charles').set('lastName', 'Jolley'); |
|
``` |
|
|
|
@method set |
|
@param {String} keyName The property to set |
|
@param {Object} value The value to set or `null`. |
|
@return {Ember.Observable} |
|
*/ |
|
set: function(keyName, value) { |
|
set(this, keyName, value); |
|
return this; |
|
}, |
|
|
|
|
|
/** |
|
Sets a list of properties at once. These properties are set inside |
|
a single `beginPropertyChanges` and `endPropertyChanges` batch, so |
|
observers will be buffered. |
|
|
|
```javascript |
|
record.setProperties({ firstName: 'Charles', lastName: 'Jolley' }); |
|
``` |
|
|
|
@method setProperties |
|
@param {Hash} hash the hash of keys and values to set |
|
@return {Ember.Observable} |
|
*/ |
|
setProperties: function(hash) { |
|
return Ember.setProperties(this, hash); |
|
}, |
|
|
|
/** |
|
Begins a grouping of property changes. |
|
|
|
You can use this method to group property changes so that notifications |
|
will not be sent until the changes are finished. If you plan to make a |
|
large number of changes to an object at one time, you should call this |
|
method at the beginning of the changes to begin deferring change |
|
notifications. When you are done making changes, call |
|
`endPropertyChanges()` to deliver the deferred change notifications and end |
|
deferring. |
|
|
|
@method beginPropertyChanges |
|
@return {Ember.Observable} |
|
*/ |
|
beginPropertyChanges: function() { |
|
Ember.beginPropertyChanges(); |
|
return this; |
|
}, |
|
|
|
/** |
|
Ends a grouping of property changes. |
|
|
|
You can use this method to group property changes so that notifications |
|
will not be sent until the changes are finished. If you plan to make a |
|
large number of changes to an object at one time, you should call |
|
`beginPropertyChanges()` at the beginning of the changes to defer change |
|
notifications. When you are done making changes, call this method to |
|
deliver the deferred change notifications and end deferring. |
|
|
|
@method endPropertyChanges |
|
@return {Ember.Observable} |
|
*/ |
|
endPropertyChanges: function() { |
|
Ember.endPropertyChanges(); |
|
return this; |
|
}, |
|
|
|
/** |
|
Notify the observer system that a property is about to change. |
|
|
|
Sometimes you need to change a value directly or indirectly without |
|
actually calling `get()` or `set()` on it. In this case, you can use this |
|
method and `propertyDidChange()` instead. Calling these two methods |
|
together will notify all observers that the property has potentially |
|
changed value. |
|
|
|
Note that you must always call `propertyWillChange` and `propertyDidChange` |
|
as a pair. If you do not, it may get the property change groups out of |
|
order and cause notifications to be delivered more often than you would |
|
like. |
|
|
|
@method propertyWillChange |
|
@param {String} keyName The property key that is about to change. |
|
@return {Ember.Observable} |
|
*/ |
|
propertyWillChange: function(keyName) { |
|
Ember.propertyWillChange(this, keyName); |
|
return this; |
|
}, |
|
|
|
/** |
|
Notify the observer system that a property has just changed. |
|
|
|
Sometimes you need to change a value directly or indirectly without |
|
actually calling `get()` or `set()` on it. In this case, you can use this |
|
method and `propertyWillChange()` instead. Calling these two methods |
|
together will notify all observers that the property has potentially |
|
changed value. |
|
|
|
Note that you must always call `propertyWillChange` and `propertyDidChange` |
|
as a pair. If you do not, it may get the property change groups out of |
|
order and cause notifications to be delivered more often than you would |
|
like. |
|
|
|
@method propertyDidChange |
|
@param {String} keyName The property key that has just changed. |
|
@return {Ember.Observable} |
|
*/ |
|
propertyDidChange: function(keyName) { |
|
Ember.propertyDidChange(this, keyName); |
|
return this; |
|
}, |
|
|
|
/** |
|
Convenience method to call `propertyWillChange` and `propertyDidChange` in |
|
succession. |
|
|
|
@method notifyPropertyChange |
|
@param {String} keyName The property key to be notified about. |
|
@return {Ember.Observable} |
|
*/ |
|
notifyPropertyChange: function(keyName) { |
|
this.propertyWillChange(keyName); |
|
this.propertyDidChange(keyName); |
|
return this; |
|
}, |
|
|
|
addBeforeObserver: function(key, target, method) { |
|
Ember.addBeforeObserver(this, key, target, method); |
|
}, |
|
|
|
/** |
|
Adds an observer on a property. |
|
|
|
This is the core method used to register an observer for a property. |
|
|
|
Once you call this method, any time the key's value is set, your observer |
|
will be notified. Note that the observers are triggered any time the |
|
value is set, regardless of whether it has actually changed. Your |
|
observer should be prepared to handle that. |
|
|
|
You can also pass an optional context parameter to this method. The |
|
context will be passed to your observer method whenever it is triggered. |
|
Note that if you add the same target/method pair on a key multiple times |
|
with different context parameters, your observer will only be called once |
|
with the last context you passed. |
|
|
|
### Observer Methods |
|
|
|
Observer methods you pass should generally have the following signature if |
|
you do not pass a `context` parameter: |
|
|
|
```javascript |
|
fooDidChange: function(sender, key, value, rev) { }; |
|
``` |
|
|
|
The sender is the object that changed. The key is the property that |
|
changes. The value property is currently reserved and unused. The rev |
|
is the last property revision of the object when it changed, which you can |
|
use to detect if the key value has really changed or not. |
|
|
|
If you pass a `context` parameter, the context will be passed before the |
|
revision like so: |
|
|
|
```javascript |
|
fooDidChange: function(sender, key, value, context, rev) { }; |
|
``` |
|
|
|
Usually you will not need the value, context or revision parameters at |
|
the end. In this case, it is common to write observer methods that take |
|
only a sender and key value as parameters or, if you aren't interested in |
|
any of these values, to write an observer that has no parameters at all. |
|
|
|
@method addObserver |
|
@param {String} key The key to observer |
|
@param {Object} target The target object to invoke |
|
@param {String|Function} method The method to invoke. |
|
@return {Ember.Object} self |
|
*/ |
|
addObserver: function(key, target, method) { |
|
Ember.addObserver(this, key, target, method); |
|
}, |
|
|
|
/** |
|
Remove an observer you have previously registered on this object. Pass |
|
the same key, target, and method you passed to `addObserver()` and your |
|
target will no longer receive notifications. |
|
|
|
@method removeObserver |
|
@param {String} key The key to observer |
|
@param {Object} target The target object to invoke |
|
@param {String|Function} method The method to invoke. |
|
@return {Ember.Observable} receiver |
|
*/ |
|
removeObserver: function(key, target, method) { |
|
Ember.removeObserver(this, key, target, method); |
|
}, |
|
|
|
/** |
|
Returns `true` if the object currently has observers registered for a |
|
particular key. You can use this method to potentially defer performing |
|
an expensive action until someone begins observing a particular property |
|
on the object. |
|
|
|
@method hasObserverFor |
|
@param {String} key Key to check |
|
@return {Boolean} |
|
*/ |
|
hasObserverFor: function(key) { |
|
return Ember.hasListeners(this, key+':change'); |
|
}, |
|
|
|
/** |
|
Retrieves the value of a property, or a default value in the case that the |
|
property returns `undefined`. |
|
|
|
```javascript |
|
person.getWithDefault('lastName', 'Doe'); |
|
``` |
|
|
|
@method getWithDefault |
|
@param {String} keyName The name of the property to retrieve |
|
@param {Object} defaultValue The value to return if the property value is undefined |
|
@return {Object} The property value or the defaultValue. |
|
*/ |
|
getWithDefault: function(keyName, defaultValue) { |
|
return Ember.getWithDefault(this, keyName, defaultValue); |
|
}, |
|
|
|
/** |
|
Set the value of a property to the current value plus some amount. |
|
|
|
```javascript |
|
person.incrementProperty('age'); |
|
team.incrementProperty('score', 2); |
|
``` |
|
|
|
@method incrementProperty |
|
@param {String} keyName The name of the property to increment |
|
@param {Number} increment The amount to increment by. Defaults to 1 |
|
@return {Number} The new property value |
|
*/ |
|
incrementProperty: function(keyName, increment) { |
|
if (Ember.isNone(increment)) { increment = 1; } |
|
Ember.assert("Must pass a numeric value to incrementProperty", (!isNaN(parseFloat(increment)) && isFinite(increment))); |
|
set(this, keyName, (get(this, keyName) || 0) + increment); |
|
return get(this, keyName); |
|
}, |
|
|
|
/** |
|
Set the value of a property to the current value minus some amount. |
|
|
|
```javascript |
|
player.decrementProperty('lives'); |
|
orc.decrementProperty('health', 5); |
|
``` |
|
|
|
@method decrementProperty |
|
@param {String} keyName The name of the property to decrement |
|
@param {Number} decrement The amount to decrement by. Defaults to 1 |
|
@return {Number} The new property value |
|
*/ |
|
decrementProperty: function(keyName, decrement) { |
|
if (Ember.isNone(decrement)) { decrement = 1; } |
|
Ember.assert("Must pass a numeric value to decrementProperty", (!isNaN(parseFloat(decrement)) && isFinite(decrement))); |
|
set(this, keyName, (get(this, keyName) || 0) - decrement); |
|
return get(this, keyName); |
|
}, |
|
|
|
/** |
|
Set the value of a boolean property to the opposite of it's |
|
current value. |
|
|
|
```javascript |
|
starship.toggleProperty('warpDriveEngaged'); |
|
``` |
|
|
|
@method toggleProperty |
|
@param {String} keyName The name of the property to toggle |
|
@return {Object} The new property value |
|
*/ |
|
toggleProperty: function(keyName) { |
|
set(this, keyName, !get(this, keyName)); |
|
return get(this, keyName); |
|
}, |
|
|
|
/** |
|
Returns the cached value of a computed property, if it exists. |
|
This allows you to inspect the value of a computed property |
|
without accidentally invoking it if it is intended to be |
|
generated lazily. |
|
|
|
@method cacheFor |
|
@param {String} keyName |
|
@return {Object} The cached value of the computed property, if any |
|
*/ |
|
cacheFor: function(keyName) { |
|
return Ember.cacheFor(this, keyName); |
|
}, |
|
|
|
// intended for debugging purposes |
|
observersForKey: function(keyName) { |
|
return Ember.observersFor(this, keyName); |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
|
|
// NOTE: this object should never be included directly. Instead use `Ember.Object`. |
|
// We only define this separately so that `Ember.Set` can depend on it. |
|
|
|
|
|
var set = Ember.set, get = Ember.get, |
|
o_create = Ember.create, |
|
o_defineProperty = Ember.platform.defineProperty, |
|
GUID_KEY = Ember.GUID_KEY, |
|
guidFor = Ember.guidFor, |
|
generateGuid = Ember.generateGuid, |
|
meta = Ember.meta, |
|
META_KEY = Ember.META_KEY, |
|
rewatch = Ember.rewatch, |
|
finishChains = Ember.finishChains, |
|
sendEvent = Ember.sendEvent, |
|
destroy = Ember.destroy, |
|
schedule = Ember.run.schedule, |
|
Mixin = Ember.Mixin, |
|
applyMixin = Mixin._apply, |
|
finishPartial = Mixin.finishPartial, |
|
reopen = Mixin.prototype.reopen, |
|
MANDATORY_SETTER = Ember.ENV.MANDATORY_SETTER, |
|
indexOf = Ember.EnumerableUtils.indexOf; |
|
|
|
var undefinedDescriptor = { |
|
configurable: true, |
|
writable: true, |
|
enumerable: false, |
|
value: undefined |
|
}; |
|
|
|
var nullDescriptor = { |
|
configurable: true, |
|
writable: true, |
|
enumerable: false, |
|
value: null |
|
}; |
|
|
|
function makeCtor() { |
|
|
|
// Note: avoid accessing any properties on the object since it makes the |
|
// method a lot faster. This is glue code so we want it to be as fast as |
|
// possible. |
|
|
|
var wasApplied = false, initMixins, initProperties; |
|
|
|
var Class = function() { |
|
if (!wasApplied) { |
|
Class.proto(); // prepare prototype... |
|
} |
|
o_defineProperty(this, GUID_KEY, nullDescriptor); |
|
o_defineProperty(this, '__nextSuper', undefinedDescriptor); |
|
var m = meta(this), proto = m.proto; |
|
m.proto = this; |
|
if (initMixins) { |
|
// capture locally so we can clear the closed over variable |
|
var mixins = initMixins; |
|
initMixins = null; |
|
this.reopen.apply(this, mixins); |
|
} |
|
if (initProperties) { |
|
// capture locally so we can clear the closed over variable |
|
var props = initProperties; |
|
initProperties = null; |
|
|
|
var concatenatedProperties = this.concatenatedProperties; |
|
|
|
for (var i = 0, l = props.length; i < l; i++) { |
|
var properties = props[i]; |
|
|
|
Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin)); |
|
|
|
if (typeof properties !== 'object' && properties !== undefined) { |
|
throw new Ember.Error("Ember.Object.create only accepts objects."); |
|
} |
|
|
|
if (!properties) { continue; } |
|
|
|
var keyNames = Ember.keys(properties); |
|
|
|
for (var j = 0, ll = keyNames.length; j < ll; j++) { |
|
var keyName = keyNames[j]; |
|
if (!properties.hasOwnProperty(keyName)) { continue; } |
|
|
|
var value = properties[keyName], |
|
IS_BINDING = Ember.IS_BINDING; |
|
|
|
if (IS_BINDING.test(keyName)) { |
|
var bindings = m.bindings; |
|
if (!bindings) { |
|
bindings = m.bindings = {}; |
|
} else if (!m.hasOwnProperty('bindings')) { |
|
bindings = m.bindings = o_create(m.bindings); |
|
} |
|
bindings[keyName] = value; |
|
} |
|
|
|
var desc = m.descs[keyName]; |
|
|
|
Ember.assert("Ember.Object.create no longer supports defining computed properties. Define computed properties using extend() or reopen() before calling create().", !(value instanceof Ember.ComputedProperty)); |
|
Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1)); |
|
Ember.assert("`actions` must be provided at extend time, not at create " + |
|
"time, when Ember.ActionHandler is used (i.e. views, " + |
|
"controllers & routes).", !((keyName === 'actions') && Ember.ActionHandler.detect(this))); |
|
|
|
if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) { |
|
var baseValue = this[keyName]; |
|
|
|
if (baseValue) { |
|
if ('function' === typeof baseValue.concat) { |
|
value = baseValue.concat(value); |
|
} else { |
|
value = Ember.makeArray(baseValue).concat(value); |
|
} |
|
} else { |
|
value = Ember.makeArray(value); |
|
} |
|
} |
|
|
|
if (desc) { |
|
desc.set(this, keyName, value); |
|
} else { |
|
if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) { |
|
this.setUnknownProperty(keyName, value); |
|
} else if (MANDATORY_SETTER) { |
|
Ember.defineProperty(this, keyName, null, value); // setup mandatory setter |
|
} else { |
|
this[keyName] = value; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
finishPartial(this, m); |
|
this.init.apply(this, arguments); |
|
m.proto = proto; |
|
finishChains(this); |
|
sendEvent(this, "init"); |
|
}; |
|
|
|
Class.toString = Mixin.prototype.toString; |
|
Class.willReopen = function() { |
|
if (wasApplied) { |
|
Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin); |
|
} |
|
|
|
wasApplied = false; |
|
}; |
|
Class._initMixins = function(args) { initMixins = args; }; |
|
Class._initProperties = function(args) { initProperties = args; }; |
|
|
|
Class.proto = function() { |
|
var superclass = Class.superclass; |
|
if (superclass) { superclass.proto(); } |
|
|
|
if (!wasApplied) { |
|
wasApplied = true; |
|
Class.PrototypeMixin.applyPartial(Class.prototype); |
|
rewatch(Class.prototype); |
|
} |
|
|
|
return this.prototype; |
|
}; |
|
|
|
return Class; |
|
|
|
} |
|
|
|
/** |
|
@class CoreObject |
|
@namespace Ember |
|
*/ |
|
var CoreObject = makeCtor(); |
|
CoreObject.toString = function() { return "Ember.CoreObject"; }; |
|
|
|
CoreObject.PrototypeMixin = Mixin.create({ |
|
reopen: function() { |
|
applyMixin(this, arguments, true); |
|
return this; |
|
}, |
|
|
|
/** |
|
An overridable method called when objects are instantiated. By default, |
|
does nothing unless it is overridden during class definition. |
|
|
|
Example: |
|
|
|
```javascript |
|
App.Person = Ember.Object.extend({ |
|
init: function() { |
|
alert('Name is ' + this.get('name')); |
|
} |
|
}); |
|
|
|
var steve = App.Person.create({ |
|
name: "Steve" |
|
}); |
|
|
|
// alerts 'Name is Steve'. |
|
``` |
|
|
|
NOTE: If you do override `init` for a framework class like `Ember.View` or |
|
`Ember.ArrayController`, be sure to call `this._super()` in your |
|
`init` declaration! If you don't, Ember may not have an opportunity to |
|
do important setup work, and you'll see strange behavior in your |
|
application. |
|
|
|
@method init |
|
*/ |
|
init: function() {}, |
|
|
|
/** |
|
Defines the properties that will be concatenated from the superclass |
|
(instead of overridden). |
|
|
|
By default, when you extend an Ember class a property defined in |
|
the subclass overrides a property with the same name that is defined |
|
in the superclass. However, there are some cases where it is preferable |
|
to build up a property's value by combining the superclass' property |
|
value with the subclass' value. An example of this in use within Ember |
|
is the `classNames` property of `Ember.View`. |
|
|
|
Here is some sample code showing the difference between a concatenated |
|
property and a normal one: |
|
|
|
```javascript |
|
App.BarView = Ember.View.extend({ |
|
someNonConcatenatedProperty: ['bar'], |
|
classNames: ['bar'] |
|
}); |
|
|
|
App.FooBarView = App.BarView.extend({ |
|
someNonConcatenatedProperty: ['foo'], |
|
classNames: ['foo'], |
|
}); |
|
|
|
var fooBarView = App.FooBarView.create(); |
|
fooBarView.get('someNonConcatenatedProperty'); // ['foo'] |
|
fooBarView.get('classNames'); // ['ember-view', 'bar', 'foo'] |
|
``` |
|
|
|
This behavior extends to object creation as well. Continuing the |
|
above example: |
|
|
|
```javascript |
|
var view = App.FooBarView.create({ |
|
someNonConcatenatedProperty: ['baz'], |
|
classNames: ['baz'] |
|
}) |
|
view.get('someNonConcatenatedProperty'); // ['baz'] |
|
view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] |
|
``` |
|
Adding a single property that is not an array will just add it in the array: |
|
|
|
```javascript |
|
var view = App.FooBarView.create({ |
|
classNames: 'baz' |
|
}) |
|
view.get('classNames'); // ['ember-view', 'bar', 'foo', 'baz'] |
|
``` |
|
|
|
Using the `concatenatedProperties` property, we can tell to Ember that mix |
|
the content of the properties. |
|
|
|
In `Ember.View` the `classNameBindings` and `attributeBindings` properties |
|
are also concatenated, in addition to `classNames`. |
|
|
|
This feature is available for you to use throughout the Ember object model, |
|
although typical app developers are likely to use it infrequently. Since |
|
it changes expectations about behavior of properties, you should properly |
|
document its usage in each individual concatenated property (to not |
|
mislead your users to think they can override the property in a subclass). |
|
|
|
@property concatenatedProperties |
|
@type Array |
|
@default null |
|
*/ |
|
concatenatedProperties: null, |
|
|
|
/** |
|
Destroyed object property flag. |
|
|
|
if this property is `true` the observers and bindings were already |
|
removed by the effect of calling the `destroy()` method. |
|
|
|
@property isDestroyed |
|
@default false |
|
*/ |
|
isDestroyed: false, |
|
|
|
/** |
|
Destruction scheduled flag. The `destroy()` method has been called. |
|
|
|
The object stays intact until the end of the run loop at which point |
|
the `isDestroyed` flag is set. |
|
|
|
@property isDestroying |
|
@default false |
|
*/ |
|
isDestroying: false, |
|
|
|
/** |
|
Destroys an object by setting the `isDestroyed` flag and removing its |
|
metadata, which effectively destroys observers and bindings. |
|
|
|
If you try to set a property on a destroyed object, an exception will be |
|
raised. |
|
|
|
Note that destruction is scheduled for the end of the run loop and does not |
|
happen immediately. It will set an isDestroying flag immediately. |
|
|
|
@method destroy |
|
@return {Ember.Object} receiver |
|
*/ |
|
destroy: function() { |
|
if (this.isDestroying) { return; } |
|
this.isDestroying = true; |
|
|
|
schedule('actions', this, this.willDestroy); |
|
schedule('destroy', this, this._scheduledDestroy); |
|
return this; |
|
}, |
|
|
|
/** |
|
Override to implement teardown. |
|
|
|
@method willDestroy |
|
*/ |
|
willDestroy: Ember.K, |
|
|
|
/** |
|
Invoked by the run loop to actually destroy the object. This is |
|
scheduled for execution by the `destroy` method. |
|
|
|
@private |
|
@method _scheduledDestroy |
|
*/ |
|
_scheduledDestroy: function() { |
|
if (this.isDestroyed) { return; } |
|
destroy(this); |
|
this.isDestroyed = true; |
|
}, |
|
|
|
bind: function(to, from) { |
|
if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } |
|
from.to(to).connect(this); |
|
return from; |
|
}, |
|
|
|
/** |
|
Returns a string representation which attempts to provide more information |
|
than Javascript's `toString` typically does, in a generic way for all Ember |
|
objects. |
|
|
|
```javascript |
|
App.Person = Em.Object.extend() |
|
person = App.Person.create() |
|
person.toString() //=> "<App.Person:ember1024>" |
|
``` |
|
|
|
If the object's class is not defined on an Ember namespace, it will |
|
indicate it is a subclass of the registered superclass: |
|
|
|
```javascript |
|
Student = App.Person.extend() |
|
student = Student.create() |
|
student.toString() //=> "<(subclass of App.Person):ember1025>" |
|
``` |
|
|
|
If the method `toStringExtension` is defined, its return value will be |
|
included in the output. |
|
|
|
```javascript |
|
App.Teacher = App.Person.extend({ |
|
toStringExtension: function() { |
|
return this.get('fullName'); |
|
} |
|
}); |
|
teacher = App.Teacher.create() |
|
teacher.toString(); //=> "<App.Teacher:ember1026:Tom Dale>" |
|
``` |
|
|
|
@method toString |
|
@return {String} string representation |
|
*/ |
|
toString: function toString() { |
|
var hasToStringExtension = typeof this.toStringExtension === 'function', |
|
extension = hasToStringExtension ? ":" + this.toStringExtension() : ''; |
|
var ret = '<'+this.constructor.toString()+':'+guidFor(this)+extension+'>'; |
|
this.toString = makeToString(ret); |
|
return ret; |
|
} |
|
}); |
|
|
|
CoreObject.PrototypeMixin.ownerConstructor = CoreObject; |
|
|
|
function makeToString(ret) { |
|
return function() { return ret; }; |
|
} |
|
|
|
if (Ember.config.overridePrototypeMixin) { |
|
Ember.config.overridePrototypeMixin(CoreObject.PrototypeMixin); |
|
} |
|
|
|
CoreObject.__super__ = null; |
|
|
|
var ClassMixin = Mixin.create({ |
|
|
|
ClassMixin: Ember.required(), |
|
|
|
PrototypeMixin: Ember.required(), |
|
|
|
isClass: true, |
|
|
|
isMethod: false, |
|
|
|
/** |
|
Creates a new subclass. |
|
|
|
```javascript |
|
App.Person = Ember.Object.extend({ |
|
say: function(thing) { |
|
alert(thing); |
|
} |
|
}); |
|
``` |
|
|
|
This defines a new subclass of Ember.Object: `App.Person`. It contains one method: `say()`. |
|
|
|
You can also create a subclass from any existing class by calling its `extend()` method. For example, you might want to create a subclass of Ember's built-in `Ember.View` class: |
|
|
|
```javascript |
|
App.PersonView = Ember.View.extend({ |
|
tagName: 'li', |
|
classNameBindings: ['isAdministrator'] |
|
}); |
|
``` |
|
|
|
When defining a subclass, you can override methods but still access the implementation of your parent class by calling the special `_super()` method: |
|
|
|
```javascript |
|
App.Person = Ember.Object.extend({ |
|
say: function(thing) { |
|
var name = this.get('name'); |
|
alert(name + ' says: ' + thing); |
|
} |
|
}); |
|
|
|
App.Soldier = App.Person.extend({ |
|
say: function(thing) { |
|
this._super(thing + ", sir!"); |
|
}, |
|
march: function(numberOfHours) { |
|
alert(this.get('name') + ' marches for ' + numberOfHours + ' hours.') |
|
} |
|
}); |
|
|
|
var yehuda = App.Soldier.create({ |
|
name: "Yehuda Katz" |
|
}); |
|
|
|
yehuda.say("Yes"); // alerts "Yehuda Katz says: Yes, sir!" |
|
``` |
|
|
|
The `create()` on line #17 creates an *instance* of the `App.Soldier` class. The `extend()` on line #8 creates a *subclass* of `App.Person`. Any instance of the `App.Person` class will *not* have the `march()` method. |
|
|
|
You can also pass `Ember.Mixin` classes to add additional properties to the subclass. |
|
|
|
```javascript |
|
App.Person = Ember.Object.extend({ |
|
say: function(thing) { |
|
alert(this.get('name') + ' says: ' + thing); |
|
} |
|
}); |
|
|
|
App.SingingMixin = Ember.Mixin.create({ |
|
sing: function(thing){ |
|
alert(this.get('name') + ' sings: la la la ' + thing); |
|
} |
|
}); |
|
|
|
App.BroadwayStar = App.Person.extend(App.SingingMixin, { |
|
dance: function() { |
|
alert(this.get('name') + ' dances: tap tap tap tap '); |
|
} |
|
}); |
|
``` |
|
|
|
The `App.BroadwayStar` class contains three methods: `say()`, `sing()`, and `dance()`. |
|
|
|
@method extend |
|
@static |
|
|
|
@param {Ember.Mixin} [mixins]* One or more Ember.Mixin classes |
|
@param {Object} [arguments]* Object containing values to use within the new class |
|
*/ |
|
extend: function() { |
|
var Class = makeCtor(), proto; |
|
Class.ClassMixin = Mixin.create(this.ClassMixin); |
|
Class.PrototypeMixin = Mixin.create(this.PrototypeMixin); |
|
|
|
Class.ClassMixin.ownerConstructor = Class; |
|
Class.PrototypeMixin.ownerConstructor = Class; |
|
|
|
reopen.apply(Class.PrototypeMixin, arguments); |
|
|
|
Class.superclass = this; |
|
Class.__super__ = this.prototype; |
|
|
|
proto = Class.prototype = o_create(this.prototype); |
|
proto.constructor = Class; |
|
generateGuid(proto); |
|
meta(proto).proto = proto; // this will disable observers on prototype |
|
|
|
Class.ClassMixin.apply(Class); |
|
return Class; |
|
}, |
|
|
|
/** |
|
Equivalent to doing `extend(arguments).create()`. |
|
If possible use the normal `create` method instead. |
|
|
|
@method createWithMixins |
|
@static |
|
@param [arguments]* |
|
*/ |
|
createWithMixins: function() { |
|
var C = this; |
|
if (arguments.length>0) { this._initMixins(arguments); } |
|
return new C(); |
|
}, |
|
|
|
/** |
|
Creates an instance of a class. Accepts either no arguments, or an object |
|
containing values to initialize the newly instantiated object with. |
|
|
|
```javascript |
|
App.Person = Ember.Object.extend({ |
|
helloWorld: function() { |
|
alert("Hi, my name is " + this.get('name')); |
|
} |
|
}); |
|
|
|
var tom = App.Person.create({ |
|
name: 'Tom Dale' |
|
}); |
|
|
|
tom.helloWorld(); // alerts "Hi, my name is Tom Dale". |
|
``` |
|
|
|
`create` will call the `init` function if defined during |
|
`Ember.AnyObject.extend` |
|
|
|
If no arguments are passed to `create`, it will not set values to the new |
|
instance during initialization: |
|
|
|
```javascript |
|
var noName = App.Person.create(); |
|
noName.helloWorld(); // alerts undefined |
|
``` |
|
|
|
NOTE: For performance reasons, you cannot declare methods or computed |
|
properties during `create`. You should instead declare methods and computed |
|
properties when using `extend` or use the `createWithMixins` shorthand. |
|
|
|
@method create |
|
@static |
|
@param [arguments]* |
|
*/ |
|
create: function() { |
|
var C = this; |
|
if (arguments.length>0) { this._initProperties(arguments); } |
|
return new C(); |
|
}, |
|
|
|
/** |
|
Augments a constructor's prototype with additional |
|
properties and functions: |
|
|
|
```javascript |
|
MyObject = Ember.Object.extend({ |
|
name: 'an object' |
|
}); |
|
|
|
o = MyObject.create(); |
|
o.get('name'); // 'an object' |
|
|
|
MyObject.reopen({ |
|
say: function(msg){ |
|
console.log(msg); |
|
} |
|
}) |
|
|
|
o2 = MyObject.create(); |
|
o2.say("hello"); // logs "hello" |
|
|
|
o.say("goodbye"); // logs "goodbye" |
|
``` |
|
|
|
To add functions and properties to the constructor itself, |
|
see `reopenClass` |
|
|
|
@method reopen |
|
*/ |
|
reopen: function() { |
|
this.willReopen(); |
|
reopen.apply(this.PrototypeMixin, arguments); |
|
return this; |
|
}, |
|
|
|
/** |
|
Augments a constructor's own properties and functions: |
|
|
|
```javascript |
|
MyObject = Ember.Object.extend({ |
|
name: 'an object' |
|
}); |
|
|
|
MyObject.reopenClass({ |
|
canBuild: false |
|
}); |
|
|
|
MyObject.canBuild; // false |
|
o = MyObject.create(); |
|
``` |
|
|
|
In other words, this creates static properties and functions for the class. These are only available on the class |
|
and not on any instance of that class. |
|
|
|
```javascript |
|
App.Person = Ember.Object.extend({ |
|
name : "", |
|
sayHello : function(){ |
|
alert("Hello. My name is " + this.get('name')); |
|
} |
|
}); |
|
|
|
App.Person.reopenClass({ |
|
species : "Homo sapiens", |
|
createPerson: function(newPersonsName){ |
|
return App.Person.create({ |
|
name:newPersonsName |
|
}); |
|
} |
|
}); |
|
|
|
var tom = App.Person.create({ |
|
name : "Tom Dale" |
|
}); |
|
var yehuda = App.Person.createPerson("Yehuda Katz"); |
|
|
|
tom.sayHello(); // "Hello. My name is Tom Dale" |
|
yehuda.sayHello(); // "Hello. My name is Yehuda Katz" |
|
alert(App.Person.species); // "Homo sapiens" |
|
``` |
|
|
|
Note that `species` and `createPerson` are *not* valid on the `tom` and `yehuda` |
|
variables. They are only valid on `App.Person`. |
|
|
|
To add functions and properties to instances of |
|
a constructor by extending the constructor's prototype |
|
see `reopen` |
|
|
|
@method reopenClass |
|
*/ |
|
reopenClass: function() { |
|
reopen.apply(this.ClassMixin, arguments); |
|
applyMixin(this, arguments, false); |
|
return this; |
|
}, |
|
|
|
detect: function(obj) { |
|
if ('function' !== typeof obj) { return false; } |
|
while(obj) { |
|
if (obj===this) { return true; } |
|
obj = obj.superclass; |
|
} |
|
return false; |
|
}, |
|
|
|
detectInstance: function(obj) { |
|
return obj instanceof this; |
|
}, |
|
|
|
/** |
|
In some cases, you may want to annotate computed properties with additional |
|
metadata about how they function or what values they operate on. For |
|
example, computed property functions may close over variables that are then |
|
no longer available for introspection. |
|
|
|
You can pass a hash of these values to a computed property like this: |
|
|
|
```javascript |
|
person: function() { |
|
var personId = this.get('personId'); |
|
return App.Person.create({ id: personId }); |
|
}.property().meta({ type: App.Person }) |
|
``` |
|
|
|
Once you've done this, you can retrieve the values saved to the computed |
|
property from your class like this: |
|
|
|
```javascript |
|
MyClass.metaForProperty('person'); |
|
``` |
|
|
|
This will return the original hash that was passed to `meta()`. |
|
|
|
@method metaForProperty |
|
@param key {String} property name |
|
*/ |
|
metaForProperty: function(key) { |
|
var meta = this.proto()[META_KEY], |
|
desc = meta && meta.descs[key]; |
|
|
|
Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty); |
|
return desc._meta || {}; |
|
}, |
|
|
|
/** |
|
Iterate over each computed property for the class, passing its name |
|
and any associated metadata (see `metaForProperty`) to the callback. |
|
|
|
@method eachComputedProperty |
|
@param {Function} callback |
|
@param {Object} binding |
|
*/ |
|
eachComputedProperty: function(callback, binding) { |
|
var proto = this.proto(), |
|
descs = meta(proto).descs, |
|
empty = {}, |
|
property; |
|
|
|
for (var name in descs) { |
|
property = descs[name]; |
|
|
|
if (property instanceof Ember.ComputedProperty) { |
|
callback.call(binding || this, name, property._meta || empty); |
|
} |
|
} |
|
} |
|
|
|
}); |
|
|
|
ClassMixin.ownerConstructor = CoreObject; |
|
|
|
if (Ember.config.overrideClassMixin) { |
|
Ember.config.overrideClassMixin(ClassMixin); |
|
} |
|
|
|
CoreObject.ClassMixin = ClassMixin; |
|
ClassMixin.apply(CoreObject); |
|
|
|
Ember.CoreObject = CoreObject; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
/** |
|
`Ember.Object` is the main base class for all Ember objects. It is a subclass |
|
of `Ember.CoreObject` with the `Ember.Observable` mixin applied. For details, |
|
see the documentation for each of these. |
|
|
|
@class Object |
|
@namespace Ember |
|
@extends Ember.CoreObject |
|
@uses Ember.Observable |
|
*/ |
|
Ember.Object = Ember.CoreObject.extend(Ember.Observable); |
|
Ember.Object.toString = function() { return "Ember.Object"; }; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var get = Ember.get, indexOf = Ember.ArrayPolyfills.indexOf; |
|
|
|
/** |
|
A Namespace is an object usually used to contain other objects or methods |
|
such as an application or framework. Create a namespace anytime you want |
|
to define one of these new containers. |
|
|
|
# Example Usage |
|
|
|
```javascript |
|
MyFramework = Ember.Namespace.create({ |
|
VERSION: '1.0.0' |
|
}); |
|
``` |
|
|
|
@class Namespace |
|
@namespace Ember |
|
@extends Ember.Object |
|
*/ |
|
var Namespace = Ember.Namespace = Ember.Object.extend({ |
|
isNamespace: true, |
|
|
|
init: function() { |
|
Ember.Namespace.NAMESPACES.push(this); |
|
Ember.Namespace.PROCESSED = false; |
|
}, |
|
|
|
toString: function() { |
|
var name = get(this, 'name'); |
|
if (name) { return name; } |
|
|
|
findNamespaces(); |
|
return this[Ember.GUID_KEY+'_name']; |
|
}, |
|
|
|
nameClasses: function() { |
|
processNamespace([this.toString()], this, {}); |
|
}, |
|
|
|
destroy: function() { |
|
var namespaces = Ember.Namespace.NAMESPACES; |
|
|
|
Ember.lookup[this.toString()] = undefined; |
|
delete Ember.Namespace.NAMESPACES_BY_ID[this.toString()]; |
|
namespaces.splice(indexOf.call(namespaces, this), 1); |
|
this._super(); |
|
} |
|
}); |
|
|
|
Namespace.reopenClass({ |
|
NAMESPACES: [Ember], |
|
NAMESPACES_BY_ID: {}, |
|
PROCESSED: false, |
|
processAll: processAllNamespaces, |
|
byName: function(name) { |
|
if (!Ember.BOOTED) { |
|
processAllNamespaces(); |
|
} |
|
|
|
return NAMESPACES_BY_ID[name]; |
|
} |
|
}); |
|
|
|
var NAMESPACES_BY_ID = Namespace.NAMESPACES_BY_ID; |
|
|
|
var hasOwnProp = ({}).hasOwnProperty, |
|
guidFor = Ember.guidFor; |
|
|
|
function processNamespace(paths, root, seen) { |
|
var idx = paths.length; |
|
|
|
NAMESPACES_BY_ID[paths.join('.')] = root; |
|
|
|
// Loop over all of the keys in the namespace, looking for classes |
|
for(var key in root) { |
|
if (!hasOwnProp.call(root, key)) { continue; } |
|
var obj = root[key]; |
|
|
|
// If we are processing the `Ember` namespace, for example, the |
|
// `paths` will start with `["Ember"]`. Every iteration through |
|
// the loop will update the **second** element of this list with |
|
// the key, so processing `Ember.View` will make the Array |
|
// `['Ember', 'View']`. |
|
paths[idx] = key; |
|
|
|
// If we have found an unprocessed class |
|
if (obj && obj.toString === classToString) { |
|
// Replace the class' `toString` with the dot-separated path |
|
// and set its `NAME_KEY` |
|
obj.toString = makeToString(paths.join('.')); |
|
obj[NAME_KEY] = paths.join('.'); |
|
|
|
// Support nested namespaces |
|
} else if (obj && obj.isNamespace) { |
|
// Skip aliased namespaces |
|
if (seen[guidFor(obj)]) { continue; } |
|
seen[guidFor(obj)] = true; |
|
|
|
// Process the child namespace |
|
processNamespace(paths, obj, seen); |
|
} |
|
} |
|
|
|
paths.length = idx; // cut out last item |
|
} |
|
|
|
function findNamespaces() { |
|
var Namespace = Ember.Namespace, lookup = Ember.lookup, obj, isNamespace; |
|
|
|
if (Namespace.PROCESSED) { return; } |
|
|
|
for (var prop in lookup) { |
|
// These don't raise exceptions but can cause warnings |
|
if (prop === "parent" || prop === "top" || prop === "frameElement" || prop === "webkitStorageInfo") { continue; } |
|
|
|
// get(window.globalStorage, 'isNamespace') would try to read the storage for domain isNamespace and cause exception in Firefox. |
|
// globalStorage is a storage obsoleted by the WhatWG storage specification. See https://developer.mozilla.org/en/DOM/Storage#globalStorage |
|
if (prop === "globalStorage" && lookup.StorageList && lookup.globalStorage instanceof lookup.StorageList) { continue; } |
|
// Unfortunately, some versions of IE don't support window.hasOwnProperty |
|
if (lookup.hasOwnProperty && !lookup.hasOwnProperty(prop)) { continue; } |
|
|
|
// At times we are not allowed to access certain properties for security reasons. |
|
// There are also times where even if we can access them, we are not allowed to access their properties. |
|
try { |
|
obj = Ember.lookup[prop]; |
|
isNamespace = obj && obj.isNamespace; |
|
} catch (e) { |
|
continue; |
|
} |
|
|
|
if (isNamespace) { |
|
Ember.deprecate("Namespaces should not begin with lowercase.", /^[A-Z]/.test(prop)); |
|
obj[NAME_KEY] = prop; |
|
} |
|
} |
|
} |
|
|
|
var NAME_KEY = Ember.NAME_KEY = Ember.GUID_KEY + '_name'; |
|
|
|
function superClassString(mixin) { |
|
var superclass = mixin.superclass; |
|
if (superclass) { |
|
if (superclass[NAME_KEY]) { return superclass[NAME_KEY]; } |
|
else { return superClassString(superclass); } |
|
} else { |
|
return; |
|
} |
|
} |
|
|
|
function classToString() { |
|
if (!Ember.BOOTED && !this[NAME_KEY]) { |
|
processAllNamespaces(); |
|
} |
|
|
|
var ret; |
|
|
|
if (this[NAME_KEY]) { |
|
ret = this[NAME_KEY]; |
|
} else if (this._toString) { |
|
ret = this._toString; |
|
} else { |
|
var str = superClassString(this); |
|
if (str) { |
|
ret = "(subclass of " + str + ")"; |
|
} else { |
|
ret = "(unknown mixin)"; |
|
} |
|
this.toString = makeToString(ret); |
|
} |
|
|
|
return ret; |
|
} |
|
|
|
function processAllNamespaces() { |
|
var unprocessedNamespaces = !Namespace.PROCESSED, |
|
unprocessedMixins = Ember.anyUnprocessedMixins; |
|
|
|
if (unprocessedNamespaces) { |
|
findNamespaces(); |
|
Namespace.PROCESSED = true; |
|
} |
|
|
|
if (unprocessedNamespaces || unprocessedMixins) { |
|
var namespaces = Namespace.NAMESPACES, namespace; |
|
for (var i=0, l=namespaces.length; i<l; i++) { |
|
namespace = namespaces[i]; |
|
processNamespace([namespace.toString()], namespace, {}); |
|
} |
|
|
|
Ember.anyUnprocessedMixins = false; |
|
} |
|
} |
|
|
|
function makeToString(ret) { |
|
return function() { return ret; }; |
|
} |
|
|
|
Ember.Mixin.prototype.toString = classToString; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var get = Ember.get, |
|
set = Ember.set, |
|
fmt = Ember.String.fmt, |
|
addBeforeObserver = Ember.addBeforeObserver, |
|
addObserver = Ember.addObserver, |
|
removeBeforeObserver = Ember.removeBeforeObserver, |
|
removeObserver = Ember.removeObserver, |
|
propertyWillChange = Ember.propertyWillChange, |
|
propertyDidChange = Ember.propertyDidChange, |
|
meta = Ember.meta, |
|
defineProperty = Ember.defineProperty; |
|
|
|
function contentPropertyWillChange(content, contentKey) { |
|
var key = contentKey.slice(8); // remove "content." |
|
if (key in this) { return; } // if shadowed in proxy |
|
propertyWillChange(this, key); |
|
} |
|
|
|
function contentPropertyDidChange(content, contentKey) { |
|
var key = contentKey.slice(8); // remove "content." |
|
if (key in this) { return; } // if shadowed in proxy |
|
propertyDidChange(this, key); |
|
} |
|
|
|
/** |
|
`Ember.ObjectProxy` forwards all properties not defined by the proxy itself |
|
to a proxied `content` object. |
|
|
|
```javascript |
|
object = Ember.Object.create({ |
|
name: 'Foo' |
|
}); |
|
|
|
proxy = Ember.ObjectProxy.create({ |
|
content: object |
|
}); |
|
|
|
// Access and change existing properties |
|
proxy.get('name') // 'Foo' |
|
proxy.set('name', 'Bar'); |
|
object.get('name') // 'Bar' |
|
|
|
// Create new 'description' property on `object` |
|
proxy.set('description', 'Foo is a whizboo baz'); |
|
object.get('description') // 'Foo is a whizboo baz' |
|
``` |
|
|
|
While `content` is unset, setting a property to be delegated will throw an |
|
Error. |
|
|
|
```javascript |
|
proxy = Ember.ObjectProxy.create({ |
|
content: null, |
|
flag: null |
|
}); |
|
proxy.set('flag', true); |
|
proxy.get('flag'); // true |
|
proxy.get('foo'); // undefined |
|
proxy.set('foo', 'data'); // throws Error |
|
``` |
|
|
|
Delegated properties can be bound to and will change when content is updated. |
|
|
|
Computed properties on the proxy itself can depend on delegated properties. |
|
|
|
```javascript |
|
ProxyWithComputedProperty = Ember.ObjectProxy.extend({ |
|
fullName: function () { |
|
var firstName = this.get('firstName'), |
|
lastName = this.get('lastName'); |
|
if (firstName && lastName) { |
|
return firstName + ' ' + lastName; |
|
} |
|
return firstName || lastName; |
|
}.property('firstName', 'lastName') |
|
}); |
|
|
|
proxy = ProxyWithComputedProperty.create(); |
|
|
|
proxy.get('fullName'); // undefined |
|
proxy.set('content', { |
|
firstName: 'Tom', lastName: 'Dale' |
|
}); // triggers property change for fullName on proxy |
|
|
|
proxy.get('fullName'); // 'Tom Dale' |
|
``` |
|
|
|
@class ObjectProxy |
|
@namespace Ember |
|
@extends Ember.Object |
|
*/ |
|
Ember.ObjectProxy = Ember.Object.extend({ |
|
/** |
|
The object whose properties will be forwarded. |
|
|
|
@property content |
|
@type Ember.Object |
|
@default null |
|
*/ |
|
content: null, |
|
_contentDidChange: Ember.observer('content', function() { |
|
Ember.assert("Can't set ObjectProxy's content to itself", this.get('content') !== this); |
|
}), |
|
|
|
isTruthy: Ember.computed.bool('content'), |
|
|
|
_debugContainerKey: null, |
|
|
|
willWatchProperty: function (key) { |
|
var contentKey = 'content.' + key; |
|
addBeforeObserver(this, contentKey, null, contentPropertyWillChange); |
|
addObserver(this, contentKey, null, contentPropertyDidChange); |
|
}, |
|
|
|
didUnwatchProperty: function (key) { |
|
var contentKey = 'content.' + key; |
|
removeBeforeObserver(this, contentKey, null, contentPropertyWillChange); |
|
removeObserver(this, contentKey, null, contentPropertyDidChange); |
|
}, |
|
|
|
unknownProperty: function (key) { |
|
var content = get(this, 'content'); |
|
if (content) { |
|
return get(content, key); |
|
} |
|
}, |
|
|
|
setUnknownProperty: function (key, value) { |
|
var m = meta(this); |
|
if (m.proto === this) { |
|
// if marked as prototype then just defineProperty |
|
// rather than delegate |
|
defineProperty(this, key, null, value); |
|
return value; |
|
} |
|
|
|
var content = get(this, 'content'); |
|
Ember.assert(fmt("Cannot delegate set('%@', %@) to the 'content' property of object proxy %@: its 'content' is undefined.", [key, value, this]), content); |
|
return set(content, key, value); |
|
} |
|
|
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
// .......................................................... |
|
// HELPERS |
|
// |
|
|
|
var get = Ember.get, set = Ember.set; |
|
var a_slice = Array.prototype.slice; |
|
var a_indexOf = Ember.EnumerableUtils.indexOf; |
|
|
|
var contexts = []; |
|
|
|
function popCtx() { |
|
return contexts.length===0 ? {} : contexts.pop(); |
|
} |
|
|
|
function pushCtx(ctx) { |
|
contexts.push(ctx); |
|
return null; |
|
} |
|
|
|
function iter(key, value) { |
|
var valueProvided = arguments.length === 2; |
|
|
|
function i(item) { |
|
var cur = get(item, key); |
|
return valueProvided ? value===cur : !!cur; |
|
} |
|
return i ; |
|
} |
|
|
|
/** |
|
This mixin defines the common interface implemented by enumerable objects |
|
in Ember. Most of these methods follow the standard Array iteration |
|
API defined up to JavaScript 1.8 (excluding language-specific features that |
|
cannot be emulated in older versions of JavaScript). |
|
|
|
This mixin is applied automatically to the Array class on page load, so you |
|
can use any of these methods on simple arrays. If Array already implements |
|
one of these methods, the mixin will not override them. |
|
|
|
## Writing Your Own Enumerable |
|
|
|
To make your own custom class enumerable, you need two items: |
|
|
|
1. You must have a length property. This property should change whenever |
|
the number of items in your enumerable object changes. If you use this |
|
with an `Ember.Object` subclass, you should be sure to change the length |
|
property using `set().` |
|
|
|
2. You must implement `nextObject().` See documentation. |
|
|
|
Once you have these two methods implemented, apply the `Ember.Enumerable` mixin |
|
to your class and you will be able to enumerate the contents of your object |
|
like any other collection. |
|
|
|
## Using Ember Enumeration with Other Libraries |
|
|
|
Many other libraries provide some kind of iterator or enumeration like |
|
facility. This is often where the most common API conflicts occur. |
|
Ember's API is designed to be as friendly as possible with other |
|
libraries by implementing only methods that mostly correspond to the |
|
JavaScript 1.8 API. |
|
|
|
@class Enumerable |
|
@namespace Ember |
|
@since Ember 0.9 |
|
*/ |
|
Ember.Enumerable = Ember.Mixin.create({ |
|
|
|
/** |
|
Implement this method to make your class enumerable. |
|
|
|
This method will be call repeatedly during enumeration. The index value |
|
will always begin with 0 and increment monotonically. You don't have to |
|
rely on the index value to determine what object to return, but you should |
|
always check the value and start from the beginning when you see the |
|
requested index is 0. |
|
|
|
The `previousObject` is the object that was returned from the last call |
|
to `nextObject` for the current iteration. This is a useful way to |
|
manage iteration if you are tracing a linked list, for example. |
|
|
|
Finally the context parameter will always contain a hash you can use as |
|
a "scratchpad" to maintain any other state you need in order to iterate |
|
properly. The context object is reused and is not reset between |
|
iterations so make sure you setup the context with a fresh state whenever |
|
the index parameter is 0. |
|
|
|
Generally iterators will continue to call `nextObject` until the index |
|
reaches the your current length-1. If you run out of data before this |
|
time for some reason, you should simply return undefined. |
|
|
|
The default implementation of this method simply looks up the index. |
|
This works great on any Array-like objects. |
|
|
|
@method nextObject |
|
@param {Number} index the current index of the iteration |
|
@param {Object} previousObject the value returned by the last call to |
|
`nextObject`. |
|
@param {Object} context a context object you can use to maintain state. |
|
@return {Object} the next object in the iteration or undefined |
|
*/ |
|
nextObject: Ember.required(Function), |
|
|
|
/** |
|
Helper method returns the first object from a collection. This is usually |
|
used by bindings and other parts of the framework to extract a single |
|
object if the enumerable contains only one item. |
|
|
|
If you override this method, you should implement it so that it will |
|
always return the same value each time it is called. If your enumerable |
|
contains only one object, this method should always return that object. |
|
If your enumerable is empty, this method should return `undefined`. |
|
|
|
```javascript |
|
var arr = ["a", "b", "c"]; |
|
arr.get('firstObject'); // "a" |
|
|
|
var arr = []; |
|
arr.get('firstObject'); // undefined |
|
``` |
|
|
|
@property firstObject |
|
@return {Object} the object or undefined |
|
*/ |
|
firstObject: Ember.computed(function() { |
|
if (get(this, 'length')===0) return undefined ; |
|
|
|
// handle generic enumerables |
|
var context = popCtx(), ret; |
|
ret = this.nextObject(0, null, context); |
|
pushCtx(context); |
|
return ret ; |
|
}).property('[]'), |
|
|
|
/** |
|
Helper method returns the last object from a collection. If your enumerable |
|
contains only one object, this method should always return that object. |
|
If your enumerable is empty, this method should return `undefined`. |
|
|
|
```javascript |
|
var arr = ["a", "b", "c"]; |
|
arr.get('lastObject'); // "c" |
|
|
|
var arr = []; |
|
arr.get('lastObject'); // undefined |
|
``` |
|
|
|
@property lastObject |
|
@return {Object} the last object or undefined |
|
*/ |
|
lastObject: Ember.computed(function() { |
|
var len = get(this, 'length'); |
|
if (len===0) return undefined ; |
|
var context = popCtx(), idx=0, cur, last = null; |
|
do { |
|
last = cur; |
|
cur = this.nextObject(idx++, last, context); |
|
} while (cur !== undefined); |
|
pushCtx(context); |
|
return last; |
|
}).property('[]'), |
|
|
|
/** |
|
Returns `true` if the passed object can be found in the receiver. The |
|
default version will iterate through the enumerable until the object |
|
is found. You may want to override this with a more efficient version. |
|
|
|
```javascript |
|
var arr = ["a", "b", "c"]; |
|
arr.contains("a"); // true |
|
arr.contains("z"); // false |
|
``` |
|
|
|
@method contains |
|
@param {Object} obj The object to search for. |
|
@return {Boolean} `true` if object is found in enumerable. |
|
*/ |
|
contains: function(obj) { |
|
return this.find(function(item) { return item===obj; }) !== undefined; |
|
}, |
|
|
|
/** |
|
Iterates through the enumerable, calling the passed function on each |
|
item. This method corresponds to the `forEach()` method defined in |
|
JavaScript 1.6. |
|
|
|
The callback method you provide should have the following signature (all |
|
parameters are optional): |
|
|
|
```javascript |
|
function(item, index, enumerable); |
|
``` |
|
|
|
- `item` is the current item in the iteration. |
|
- `index` is the current index in the iteration. |
|
- `enumerable` is the enumerable object itself. |
|
|
|
Note that in addition to a callback, you can also pass an optional target |
|
object that will be set as `this` on the context. This is a good way |
|
to give your iterator function access to the current object. |
|
|
|
@method forEach |
|
@param {Function} callback The callback to execute |
|
@param {Object} [target] The target object to use |
|
@return {Object} receiver |
|
*/ |
|
forEach: function(callback, target) { |
|
if (typeof callback !== "function") throw new TypeError() ; |
|
var len = get(this, 'length'), last = null, context = popCtx(); |
|
|
|
if (target === undefined) target = null; |
|
|
|
for(var idx=0;idx<len;idx++) { |
|
var next = this.nextObject(idx, last, context) ; |
|
callback.call(target, next, idx, this); |
|
last = next ; |
|
} |
|
last = null ; |
|
context = pushCtx(context); |
|
return this ; |
|
}, |
|
|
|
/** |
|
Alias for `mapBy` |
|
|
|
@method getEach |
|
@param {String} key name of the property |
|
@return {Array} The mapped array. |
|
*/ |
|
getEach: function(key) { |
|
return this.mapBy(key); |
|
}, |
|
|
|
/** |
|
Sets the value on the named property for each member. This is more |
|
efficient than using other methods defined on this helper. If the object |
|
implements Ember.Observable, the value will be changed to `set(),` otherwise |
|
it will be set directly. `null` objects are skipped. |
|
|
|
@method setEach |
|
@param {String} key The key to set |
|
@param {Object} value The object to set |
|
@return {Object} receiver |
|
*/ |
|
setEach: function(key, value) { |
|
return this.forEach(function(item) { |
|
set(item, key, value); |
|
}); |
|
}, |
|
|
|
/** |
|
Maps all of the items in the enumeration to another value, returning |
|
a new array. This method corresponds to `map()` defined in JavaScript 1.6. |
|
|
|
The callback method you provide should have the following signature (all |
|
parameters are optional): |
|
|
|
```javascript |
|
function(item, index, enumerable); |
|
``` |
|
|
|
- `item` is the current item in the iteration. |
|
- `index` is the current index in the iteration. |
|
- `enumerable` is the enumerable object itself. |
|
|
|
It should return the mapped value. |
|
|
|
Note that in addition to a callback, you can also pass an optional target |
|
object that will be set as `this` on the context. This is a good way |
|
to give your iterator function access to the current object. |
|
|
|
@method map |
|
@param {Function} callback The callback to execute |
|
@param {Object} [target] The target object to use |
|
@return {Array} The mapped array. |
|
*/ |
|
map: function(callback, target) { |
|
var ret = Ember.A(); |
|
this.forEach(function(x, idx, i) { |
|
ret[idx] = callback.call(target, x, idx,i); |
|
}); |
|
return ret ; |
|
}, |
|
|
|
/** |
|
Similar to map, this specialized function returns the value of the named |
|
property on all items in the enumeration. |
|
|
|
@method mapBy |
|
@param {String} key name of the property |
|
@return {Array} The mapped array. |
|
*/ |
|
mapBy: function(key) { |
|
return this.map(function(next) { |
|
return get(next, key); |
|
}); |
|
}, |
|
|
|
/** |
|
Similar to map, this specialized function returns the value of the named |
|
property on all items in the enumeration. |
|
|
|
@method mapProperty |
|
@param {String} key name of the property |
|
@return {Array} The mapped array. |
|
@deprecated Use `mapBy` instead |
|
*/ |
|
|
|
mapProperty: Ember.aliasMethod('mapBy'), |
|
|
|
/** |
|
Returns an array with all of the items in the enumeration that the passed |
|
function returns true for. This method corresponds to `filter()` defined in |
|
JavaScript 1.6. |
|
|
|
The callback method you provide should have the following signature (all |
|
parameters are optional): |
|
|
|
```javascript |
|
function(item, index, enumerable); |
|
``` |
|
|
|
- `item` is the current item in the iteration. |
|
- `index` is the current index in the iteration. |
|
- `enumerable` is the enumerable object itself. |
|
|
|
It should return the `true` to include the item in the results, `false` |
|
otherwise. |
|
|
|
Note that in addition to a callback, you can also pass an optional target |
|
object that will be set as `this` on the context. This is a good way |
|
to give your iterator function access to the current object. |
|
|
|
@method filter |
|
@param {Function} callback The callback to execute |
|
@param {Object} [target] The target object to use |
|
@return {Array} A filtered array. |
|
*/ |
|
filter: function(callback, target) { |
|
var ret = Ember.A(); |
|
this.forEach(function(x, idx, i) { |
|
if (callback.call(target, x, idx, i)) ret.push(x); |
|
}); |
|
return ret ; |
|
}, |
|
|
|
/** |
|
Returns an array with all of the items in the enumeration where the passed |
|
function returns false for. This method is the inverse of filter(). |
|
|
|
The callback method you provide should have the following signature (all |
|
parameters are optional): |
|
|
|
```javascript |
|
function(item, index, enumerable); |
|
``` |
|
|
|
- *item* is the current item in the iteration. |
|
- *index* is the current index in the iteration |
|
- *enumerable* is the enumerable object itself. |
|
|
|
It should return the a falsey value to include the item in the results. |
|
|
|
Note that in addition to a callback, you can also pass an optional target |
|
object that will be set as "this" on the context. This is a good way |
|
to give your iterator function access to the current object. |
|
|
|
@method reject |
|
@param {Function} callback The callback to execute |
|
@param {Object} [target] The target object to use |
|
@return {Array} A rejected array. |
|
*/ |
|
reject: function(callback, target) { |
|
return this.filter(function() { |
|
return !(callback.apply(target, arguments)); |
|
}); |
|
}, |
|
|
|
/** |
|
Returns an array with just the items with the matched property. You |
|
can pass an optional second argument with the target value. Otherwise |
|
this will match any property that evaluates to `true`. |
|
|
|
@method filterBy |
|
@param {String} key the property to test |
|
@param {*} [value] optional value to test against. |
|
@return {Array} filtered array |
|
*/ |
|
filterBy: function(key, value) { |
|
return this.filter(iter.apply(this, arguments)); |
|
}, |
|
|
|
/** |
|
Returns an array with just the items with the matched property. You |
|
can pass an optional second argument with the target value. Otherwise |
|
this will match any property that evaluates to `true`. |
|
|
|
@method filterProperty |
|
@param {String} key the property to test |
|
@param {String} [value] optional value to test against. |
|
@return {Array} filtered array |
|
@deprecated Use `filterBy` instead |
|
*/ |
|
filterProperty: Ember.aliasMethod('filterBy'), |
|
|
|
/** |
|
Returns an array with the items that do not have truthy values for |
|
key. You can pass an optional second argument with the target value. Otherwise |
|
this will match any property that evaluates to false. |
|
|
|
@method rejectBy |
|
@param {String} key the property to test |
|
@param {String} [value] optional value to test against. |
|
@return {Array} rejected array |
|
*/ |
|
rejectBy: function(key, value) { |
|
var exactValue = function(item) { return get(item, key) === value; }, |
|
hasValue = function(item) { return !!get(item, key); }, |
|
use = (arguments.length === 2 ? exactValue : hasValue); |
|
|
|
return this.reject(use); |
|
}, |
|
|
|
/** |
|
Returns an array with the items that do not have truthy values for |
|
key. You can pass an optional second argument with the target value. Otherwise |
|
this will match any property that evaluates to false. |
|
|
|
@method rejectProperty |
|
@param {String} key the property to test |
|
@param {String} [value] optional value to test against. |
|
@return {Array} rejected array |
|
@deprecated Use `rejectBy` instead |
|
*/ |
|
rejectProperty: Ember.aliasMethod('rejectBy'), |
|
|
|
/** |
|
Returns the first item in the array for which the callback returns true. |
|
This method works similar to the `filter()` method defined in JavaScript 1.6 |
|
except that it will stop working on the array once a match is found. |
|
|
|
The callback method you provide should have the following signature (all |
|
parameters are optional): |
|
|
|
```javascript |
|
function(item, index, enumerable); |
|
``` |
|
|
|
- `item` is the current item in the iteration. |
|
- `index` is the current index in the iteration. |
|
- `enumerable` is the enumerable object itself. |
|
|
|
It should return the `true` to include the item in the results, `false` |
|
otherwise. |
|
|
|
Note that in addition to a callback, you can also pass an optional target |
|
object that will be set as `this` on the context. This is a good way |
|
to give your iterator function access to the current object. |
|
|
|
@method find |
|
@param {Function} callback The callback to execute |
|
@param {Object} [target] The target object to use |
|
@return {Object} Found item or `undefined`. |
|
*/ |
|
find: function(callback, target) { |
|
var len = get(this, 'length') ; |
|
if (target === undefined) target = null; |
|
|
|
var last = null, next, found = false, ret ; |
|
var context = popCtx(); |
|
for(var idx=0;idx<len && !found;idx++) { |
|
next = this.nextObject(idx, last, context) ; |
|
if (found = callback.call(target, next, idx, this)) ret = next ; |
|
last = next ; |
|
} |
|
next = last = null ; |
|
context = pushCtx(context); |
|
return ret ; |
|
}, |
|
|
|
/** |
|
Returns the first item with a property matching the passed value. You |
|
can pass an optional second argument with the target value. Otherwise |
|
this will match any property that evaluates to `true`. |
|
|
|
This method works much like the more generic `find()` method. |
|
|
|
@method findBy |
|
@param {String} key the property to test |
|
@param {String} [value] optional value to test against. |
|
@return {Object} found item or `undefined` |
|
*/ |
|
findBy: function(key, value) { |
|
return this.find(iter.apply(this, arguments)); |
|
}, |
|
|
|
/** |
|
Returns the first item with a property matching the passed value. You |
|
can pass an optional second argument with the target value. Otherwise |
|
this will match any property that evaluates to `true`. |
|
|
|
This method works much like the more generic `find()` method. |
|
|
|
@method findProperty |
|
@param {String} key the property to test |
|
@param {String} [value] optional value to test against. |
|
@return {Object} found item or `undefined` |
|
@deprecated Use `findBy` instead |
|
*/ |
|
findProperty: Ember.aliasMethod('findBy'), |
|
|
|
/** |
|
Returns `true` if the passed function returns true for every item in the |
|
enumeration. This corresponds with the `every()` method in JavaScript 1.6. |
|
|
|
The callback method you provide should have the following signature (all |
|
parameters are optional): |
|
|
|
```javascript |
|
function(item, index, enumerable); |
|
``` |
|
|
|
- `item` is the current item in the iteration. |
|
- `index` is the current index in the iteration. |
|
- `enumerable` is the enumerable object itself. |
|
|
|
It should return the `true` or `false`. |
|
|
|
Note that in addition to a callback, you can also pass an optional target |
|
object that will be set as `this` on the context. This is a good way |
|
to give your iterator function access to the current object. |
|
|
|
Example Usage: |
|
|
|
```javascript |
|
if (people.every(isEngineer)) { Paychecks.addBigBonus(); } |
|
``` |
|
|
|
@method every |
|
@param {Function} callback The callback to execute |
|
@param {Object} [target] The target object to use |
|
@return {Boolean} |
|
*/ |
|
every: function(callback, target) { |
|
return !this.find(function(x, idx, i) { |
|
return !callback.call(target, x, idx, i); |
|
}); |
|
}, |
|
|
|
/** |
|
@method everyBy |
|
@param {String} key the property to test |
|
@param {String} [value] optional value to test against. |
|
@deprecated Use `isEvery` instead |
|
@return {Boolean} |
|
*/ |
|
everyBy: Ember.aliasMethod('isEvery'), |
|
|
|
/** |
|
@method everyProperty |
|
@param {String} key the property to test |
|
@param {String} [value] optional value to test against. |
|
@deprecated Use `isEvery` instead |
|
@return {Boolean} |
|
*/ |
|
everyProperty: Ember.aliasMethod('isEvery'), |
|
|
|
/** |
|
Returns `true` if the passed property resolves to `true` for all items in |
|
the enumerable. This method is often simpler/faster than using a callback. |
|
|
|
@method isEvery |
|
@param {String} key the property to test |
|
@param {String} [value] optional value to test against. |
|
@return {Boolean} |
|
*/ |
|
isEvery: function(key, value) { |
|
return this.every(iter.apply(this, arguments)); |
|
}, |
|
|
|
/** |
|
Returns `true` if the passed function returns true for any item in the |
|
enumeration. This corresponds with the `some()` method in JavaScript 1.6. |
|
|
|
The callback method you provide should have the following signature (all |
|
parameters are optional): |
|
|
|
```javascript |
|
function(item, index, enumerable); |
|
``` |
|
|
|
- `item` is the current item in the iteration. |
|
- `index` is the current index in the iteration. |
|
- `enumerable` is the enumerable object itself. |
|
|
|
It should return the `true` to include the item in the results, `false` |
|
otherwise. |
|
|
|
Note that in addition to a callback, you can also pass an optional target |
|
object that will be set as `this` on the context. This is a good way |
|
to give your iterator function access to the current object. |
|
|
|
Usage Example: |
|
|
|
```javascript |
|
if (people.any(isManager)) { Paychecks.addBiggerBonus(); } |
|
``` |
|
|
|
@method any |
|
@param {Function} callback The callback to execute |
|
@param {Object} [target] The target object to use |
|
@return {Boolean} `true` if the passed function returns `true` for any item |
|
*/ |
|
any: function(callback, target) { |
|
var len = get(this, 'length'), |
|
context = popCtx(), |
|
found = false, |
|
last = null, |
|
next, idx; |
|
|
|
if (target === undefined) { target = null; } |
|
|
|
for (idx = 0; idx < len && !found; idx++) { |
|
next = this.nextObject(idx, last, context); |
|
found = callback.call(target, next, idx, this); |
|
last = next; |
|
} |
|
|
|
next = last = null; |
|
context = pushCtx(context); |
|
return found; |
|
}, |
|
|
|
/** |
|
Returns `true` if the passed function returns true for any item in the |
|
enumeration. This corresponds with the `some()` method in JavaScript 1.6. |
|
|
|
The callback method you provide should have the following signature (all |
|
parameters are optional): |
|
|
|
```javascript |
|
function(item, index, enumerable); |
|
``` |
|
|
|
- `item` is the current item in the iteration. |
|
- `index` is the current index in the iteration. |
|
- `enumerable` is the enumerable object itself. |
|
|
|
It should return the `true` to include the item in the results, `false` |
|
otherwise. |
|
|
|
Note that in addition to a callback, you can also pass an optional target |
|
object that will be set as `this` on the context. This is a good way |
|
to give your iterator function access to the current object. |
|
|
|
Usage Example: |
|
|
|
```javascript |
|
if (people.some(isManager)) { Paychecks.addBiggerBonus(); } |
|
``` |
|
|
|
@method some |
|
@param {Function} callback The callback to execute |
|
@param {Object} [target] The target object to use |
|
@return {Boolean} `true` if the passed function returns `true` for any item |
|
@deprecated Use `any` instead |
|
*/ |
|
some: Ember.aliasMethod('any'), |
|
|
|
/** |
|
Returns `true` if the passed property resolves to `true` for any item in |
|
the enumerable. This method is often simpler/faster than using a callback. |
|
|
|
@method isAny |
|
@param {String} key the property to test |
|
@param {String} [value] optional value to test against. |
|
@return {Boolean} `true` if the passed function returns `true` for any item |
|
*/ |
|
isAny: function(key, value) { |
|
return this.any(iter.apply(this, arguments)); |
|
}, |
|
|
|
/** |
|
@method anyBy |
|
@param {String} key the property to test |
|
@param {String} [value] optional value to test against. |
|
@return {Boolean} `true` if the passed function returns `true` for any item |
|
@deprecated Use `isAny` instead |
|
*/ |
|
anyBy: Ember.aliasMethod('isAny'), |
|
|
|
/** |
|
@method someProperty |
|
@param {String} key the property to test |
|
@param {String} [value] optional value to test against. |
|
@return {Boolean} `true` if the passed function returns `true` for any item |
|
@deprecated Use `isAny` instead |
|
*/ |
|
someProperty: Ember.aliasMethod('isAny'), |
|
|
|
/** |
|
This will combine the values of the enumerator into a single value. It |
|
is a useful way to collect a summary value from an enumeration. This |
|
corresponds to the `reduce()` method defined in JavaScript 1.8. |
|
|
|
The callback method you provide should have the following signature (all |
|
parameters are optional): |
|
|
|
```javascript |
|
function(previousValue, item, index, enumerable); |
|
``` |
|
|
|
- `previousValue` is the value returned by the last call to the iterator. |
|
- `item` is the current item in the iteration. |
|
- `index` is the current index in the iteration. |
|
- `enumerable` is the enumerable object itself. |
|
|
|
Return the new cumulative value. |
|
|
|
In addition to the callback you can also pass an `initialValue`. An error |
|
will be raised if you do not pass an initial value and the enumerator is |
|
empty. |
|
|
|
Note that unlike the other methods, this method does not allow you to |
|
pass a target object to set as this for the callback. It's part of the |
|
spec. Sorry. |
|
|
|
@method reduce |
|
@param {Function} callback The callback to execute |
|
@param {Object} initialValue Initial value for the reduce |
|
@param {String} reducerProperty internal use only. |
|
@return {Object} The reduced value. |
|
*/ |
|
reduce: function(callback, initialValue, reducerProperty) { |
|
if (typeof callback !== "function") { throw new TypeError(); } |
|
|
|
var ret = initialValue; |
|
|
|
this.forEach(function(item, i) { |
|
ret = callback(ret, item, i, this, reducerProperty); |
|
}, this); |
|
|
|
return ret; |
|
}, |
|
|
|
/** |
|
Invokes the named method on every object in the receiver that |
|
implements it. This method corresponds to the implementation in |
|
Prototype 1.6. |
|
|
|
@method invoke |
|
@param {String} methodName the name of the method |
|
@param {Object...} args optional arguments to pass as well. |
|
@return {Array} return values from calling invoke. |
|
*/ |
|
invoke: function(methodName) { |
|
var args, ret = Ember.A(); |
|
if (arguments.length>1) args = a_slice.call(arguments, 1); |
|
|
|
this.forEach(function(x, idx) { |
|
var method = x && x[methodName]; |
|
if ('function' === typeof method) { |
|
ret[idx] = args ? method.apply(x, args) : x[methodName](); |
|
} |
|
}, this); |
|
|
|
return ret; |
|
}, |
|
|
|
/** |
|
Simply converts the enumerable into a genuine array. The order is not |
|
guaranteed. Corresponds to the method implemented by Prototype. |
|
|
|
@method toArray |
|
@return {Array} the enumerable as an array. |
|
*/ |
|
toArray: function() { |
|
var ret = Ember.A(); |
|
this.forEach(function(o, idx) { ret[idx] = o; }); |
|
return ret ; |
|
}, |
|
|
|
/** |
|
Returns a copy of the array with all null and undefined elements removed. |
|
|
|
```javascript |
|
var arr = ["a", null, "c", undefined]; |
|
arr.compact(); // ["a", "c"] |
|
``` |
|
|
|
@method compact |
|
@return {Array} the array without null and undefined elements. |
|
*/ |
|
compact: function() { |
|
return this.filter(function(value) { return value != null; }); |
|
}, |
|
|
|
/** |
|
Returns a new enumerable that excludes the passed value. The default |
|
implementation returns an array regardless of the receiver type unless |
|
the receiver does not contain the value. |
|
|
|
```javascript |
|
var arr = ["a", "b", "a", "c"]; |
|
arr.without("a"); // ["b", "c"] |
|
``` |
|
|
|
@method without |
|
@param {Object} value |
|
@return {Ember.Enumerable} |
|
*/ |
|
without: function(value) { |
|
if (!this.contains(value)) return this; // nothing to do |
|
var ret = Ember.A(); |
|
this.forEach(function(k) { |
|
if (k !== value) ret[ret.length] = k; |
|
}) ; |
|
return ret ; |
|
}, |
|
|
|
/** |
|
Returns a new enumerable that contains only unique values. The default |
|
implementation returns an array regardless of the receiver type. |
|
|
|
```javascript |
|
var arr = ["a", "a", "b", "b"]; |
|
arr.uniq(); // ["a", "b"] |
|
``` |
|
|
|
@method uniq |
|
@return {Ember.Enumerable} |
|
*/ |
|
uniq: function() { |
|
var ret = Ember.A(); |
|
this.forEach(function(k) { |
|
if (a_indexOf(ret, k)<0) ret.push(k); |
|
}); |
|
return ret; |
|
}, |
|
|
|
/** |
|
This property will trigger anytime the enumerable's content changes. |
|
You can observe this property to be notified of changes to the enumerables |
|
content. |
|
|
|
For plain enumerables, this property is read only. `Ember.Array` overrides |
|
this method. |
|
|
|
@property [] |
|
@type Ember.Array |
|
@return this |
|
*/ |
|
'[]': Ember.computed(function(key, value) { |
|
return this; |
|
}), |
|
|
|
// .......................................................... |
|
// ENUMERABLE OBSERVERS |
|
// |
|
|
|
/** |
|
Registers an enumerable observer. Must implement `Ember.EnumerableObserver` |
|
mixin. |
|
|
|
@method addEnumerableObserver |
|
@param {Object} target |
|
@param {Hash} [opts] |
|
@return this |
|
*/ |
|
addEnumerableObserver: function(target, opts) { |
|
var willChange = (opts && opts.willChange) || 'enumerableWillChange', |
|
didChange = (opts && opts.didChange) || 'enumerableDidChange'; |
|
|
|
var hasObservers = get(this, 'hasEnumerableObservers'); |
|
if (!hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); |
|
Ember.addListener(this, '@enumerable:before', target, willChange); |
|
Ember.addListener(this, '@enumerable:change', target, didChange); |
|
if (!hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); |
|
return this; |
|
}, |
|
|
|
/** |
|
Removes a registered enumerable observer. |
|
|
|
@method removeEnumerableObserver |
|
@param {Object} target |
|
@param {Hash} [opts] |
|
@return this |
|
*/ |
|
removeEnumerableObserver: function(target, opts) { |
|
var willChange = (opts && opts.willChange) || 'enumerableWillChange', |
|
didChange = (opts && opts.didChange) || 'enumerableDidChange'; |
|
|
|
var hasObservers = get(this, 'hasEnumerableObservers'); |
|
if (hasObservers) Ember.propertyWillChange(this, 'hasEnumerableObservers'); |
|
Ember.removeListener(this, '@enumerable:before', target, willChange); |
|
Ember.removeListener(this, '@enumerable:change', target, didChange); |
|
if (hasObservers) Ember.propertyDidChange(this, 'hasEnumerableObservers'); |
|
return this; |
|
}, |
|
|
|
/** |
|
Becomes true whenever the array currently has observers watching changes |
|
on the array. |
|
|
|
@property hasEnumerableObservers |
|
@type Boolean |
|
*/ |
|
hasEnumerableObservers: Ember.computed(function() { |
|
return Ember.hasListeners(this, '@enumerable:change') || Ember.hasListeners(this, '@enumerable:before'); |
|
}), |
|
|
|
|
|
/** |
|
Invoke this method just before the contents of your enumerable will |
|
change. You can either omit the parameters completely or pass the objects |
|
to be removed or added if available or just a count. |
|
|
|
@method enumerableContentWillChange |
|
@param {Ember.Enumerable|Number} removing An enumerable of the objects to |
|
be removed or the number of items to be removed. |
|
@param {Ember.Enumerable|Number} adding An enumerable of the objects to be |
|
added or the number of items to be added. |
|
@chainable |
|
*/ |
|
enumerableContentWillChange: function(removing, adding) { |
|
|
|
var removeCnt, addCnt, hasDelta; |
|
|
|
if ('number' === typeof removing) removeCnt = removing; |
|
else if (removing) removeCnt = get(removing, 'length'); |
|
else removeCnt = removing = -1; |
|
|
|
if ('number' === typeof adding) addCnt = adding; |
|
else if (adding) addCnt = get(adding,'length'); |
|
else addCnt = adding = -1; |
|
|
|
hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; |
|
|
|
if (removing === -1) removing = null; |
|
if (adding === -1) adding = null; |
|
|
|
Ember.propertyWillChange(this, '[]'); |
|
if (hasDelta) Ember.propertyWillChange(this, 'length'); |
|
Ember.sendEvent(this, '@enumerable:before', [this, removing, adding]); |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Invoke this method when the contents of your enumerable has changed. |
|
This will notify any observers watching for content changes. If your are |
|
implementing an ordered enumerable (such as an array), also pass the |
|
start and end values where the content changed so that it can be used to |
|
notify range observers. |
|
|
|
@method enumerableContentDidChange |
|
@param {Ember.Enumerable|Number} removing An enumerable of the objects to |
|
be removed or the number of items to be removed. |
|
@param {Ember.Enumerable|Number} adding An enumerable of the objects to |
|
be added or the number of items to be added. |
|
@chainable |
|
*/ |
|
enumerableContentDidChange: function(removing, adding) { |
|
var removeCnt, addCnt, hasDelta; |
|
|
|
if ('number' === typeof removing) removeCnt = removing; |
|
else if (removing) removeCnt = get(removing, 'length'); |
|
else removeCnt = removing = -1; |
|
|
|
if ('number' === typeof adding) addCnt = adding; |
|
else if (adding) addCnt = get(adding, 'length'); |
|
else addCnt = adding = -1; |
|
|
|
hasDelta = addCnt<0 || removeCnt<0 || addCnt-removeCnt!==0; |
|
|
|
if (removing === -1) removing = null; |
|
if (adding === -1) adding = null; |
|
|
|
Ember.sendEvent(this, '@enumerable:change', [this, removing, adding]); |
|
if (hasDelta) Ember.propertyDidChange(this, 'length'); |
|
Ember.propertyDidChange(this, '[]'); |
|
|
|
return this ; |
|
}, |
|
|
|
/** |
|
Converts the enumerable into an array and sorts by the keys |
|
specified in the argument. |
|
|
|
You may provide multiple arguments to sort by multiple properties. |
|
|
|
@method sortBy |
|
@param {String} property name(s) to sort on |
|
@return {Array} The sorted array. |
|
*/ |
|
sortBy: function() { |
|
var sortKeys = arguments; |
|
return this.toArray().sort(function(a, b){ |
|
for(var i = 0; i < sortKeys.length; i++) { |
|
var key = sortKeys[i], |
|
propA = get(a, key), |
|
propB = get(b, key); |
|
// return 1 or -1 else continue to the next sortKey |
|
var compareValue = Ember.compare(propA, propB); |
|
if (compareValue) { return compareValue; } |
|
} |
|
return 0; |
|
}); |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
// .......................................................... |
|
// HELPERS |
|
// |
|
|
|
var get = Ember.get, set = Ember.set, isNone = Ember.isNone, map = Ember.EnumerableUtils.map, cacheFor = Ember.cacheFor; |
|
|
|
// .......................................................... |
|
// ARRAY |
|
// |
|
/** |
|
This mixin implements Observer-friendly Array-like behavior. It is not a |
|
concrete implementation, but it can be used up by other classes that want |
|
to appear like arrays. |
|
|
|
For example, ArrayProxy and ArrayController are both concrete classes that can |
|
be instantiated to implement array-like behavior. Both of these classes use |
|
the Array Mixin by way of the MutableArray mixin, which allows observable |
|
changes to be made to the underlying array. |
|
|
|
Unlike `Ember.Enumerable,` this mixin defines methods specifically for |
|
collections that provide index-ordered access to their contents. When you |
|
are designing code that needs to accept any kind of Array-like object, you |
|
should use these methods instead of Array primitives because these will |
|
properly notify observers of changes to the array. |
|
|
|
Although these methods are efficient, they do add a layer of indirection to |
|
your application so it is a good idea to use them only when you need the |
|
flexibility of using both true JavaScript arrays and "virtual" arrays such |
|
as controllers and collections. |
|
|
|
You can use the methods defined in this module to access and modify array |
|
contents in a KVO-friendly way. You can also be notified whenever the |
|
membership of an array changes by changing the syntax of the property to |
|
`.observes('*myProperty.[]')`. |
|
|
|
To support `Ember.Array` in your own class, you must override two |
|
primitives to use it: `replace()` and `objectAt()`. |
|
|
|
Note that the Ember.Array mixin also incorporates the `Ember.Enumerable` |
|
mixin. All `Ember.Array`-like objects are also enumerable. |
|
|
|
@class Array |
|
@namespace Ember |
|
@uses Ember.Enumerable |
|
@since Ember 0.9.0 |
|
*/ |
|
Ember.Array = Ember.Mixin.create(Ember.Enumerable, { |
|
|
|
/** |
|
Your array must support the `length` property. Your replace methods should |
|
set this property whenever it changes. |
|
|
|
@property {Number} length |
|
*/ |
|
length: Ember.required(), |
|
|
|
/** |
|
Returns the object at the given `index`. If the given `index` is negative |
|
or is greater or equal than the array length, returns `undefined`. |
|
|
|
This is one of the primitives you must implement to support `Ember.Array`. |
|
If your object supports retrieving the value of an array item using `get()` |
|
(i.e. `myArray.get(0)`), then you do not need to implement this method |
|
yourself. |
|
|
|
```javascript |
|
var arr = ['a', 'b', 'c', 'd']; |
|
arr.objectAt(0); // "a" |
|
arr.objectAt(3); // "d" |
|
arr.objectAt(-1); // undefined |
|
arr.objectAt(4); // undefined |
|
arr.objectAt(5); // undefined |
|
``` |
|
|
|
@method objectAt |
|
@param {Number} idx The index of the item to return. |
|
@return {*} item at index or undefined |
|
*/ |
|
objectAt: function(idx) { |
|
if ((idx < 0) || (idx>=get(this, 'length'))) return undefined ; |
|
return get(this, idx); |
|
}, |
|
|
|
/** |
|
This returns the objects at the specified indexes, using `objectAt`. |
|
|
|
```javascript |
|
var arr = ['a', 'b', 'c', 'd']; |
|
arr.objectsAt([0, 1, 2]); // ["a", "b", "c"] |
|
arr.objectsAt([2, 3, 4]); // ["c", "d", undefined] |
|
``` |
|
|
|
@method objectsAt |
|
@param {Array} indexes An array of indexes of items to return. |
|
@return {Array} |
|
*/ |
|
objectsAt: function(indexes) { |
|
var self = this; |
|
return map(indexes, function(idx) { return self.objectAt(idx); }); |
|
}, |
|
|
|
// overrides Ember.Enumerable version |
|
nextObject: function(idx) { |
|
return this.objectAt(idx); |
|
}, |
|
|
|
/** |
|
This is the handler for the special array content property. If you get |
|
this property, it will return this. If you set this property it a new |
|
array, it will replace the current content. |
|
|
|
This property overrides the default property defined in `Ember.Enumerable`. |
|
|
|
@property [] |
|
@return this |
|
*/ |
|
'[]': Ember.computed(function(key, value) { |
|
if (value !== undefined) this.replace(0, get(this, 'length'), value) ; |
|
return this ; |
|
}), |
|
|
|
firstObject: Ember.computed(function() { |
|
return this.objectAt(0); |
|
}), |
|
|
|
lastObject: Ember.computed(function() { |
|
return this.objectAt(get(this, 'length')-1); |
|
}), |
|
|
|
// optimized version from Enumerable |
|
contains: function(obj) { |
|
return this.indexOf(obj) >= 0; |
|
}, |
|
|
|
// Add any extra methods to Ember.Array that are native to the built-in Array. |
|
/** |
|
Returns a new array that is a slice of the receiver. This implementation |
|
uses the observable array methods to retrieve the objects for the new |
|
slice. |
|
|
|
```javascript |
|
var arr = ['red', 'green', 'blue']; |
|
arr.slice(0); // ['red', 'green', 'blue'] |
|
arr.slice(0, 2); // ['red', 'green'] |
|
arr.slice(1, 100); // ['green', 'blue'] |
|
``` |
|
|
|
@method slice |
|
@param {Integer} beginIndex (Optional) index to begin slicing from. |
|
@param {Integer} endIndex (Optional) index to end the slice at (but not included). |
|
@return {Array} New array with specified slice |
|
*/ |
|
slice: function(beginIndex, endIndex) { |
|
var ret = Ember.A(); |
|
var length = get(this, 'length') ; |
|
if (isNone(beginIndex)) beginIndex = 0 ; |
|
if (isNone(endIndex) || (endIndex > length)) endIndex = length ; |
|
|
|
if (beginIndex < 0) beginIndex = length + beginIndex; |
|
if (endIndex < 0) endIndex = length + endIndex; |
|
|
|
while(beginIndex < endIndex) { |
|
ret[ret.length] = this.objectAt(beginIndex++) ; |
|
} |
|
return ret ; |
|
}, |
|
|
|
/** |
|
Returns the index of the given object's first occurrence. |
|
If no `startAt` argument is given, the starting location to |
|
search is 0. If it's negative, will count backward from |
|
the end of the array. Returns -1 if no match is found. |
|
|
|
```javascript |
|
var arr = ["a", "b", "c", "d", "a"]; |
|
arr.indexOf("a"); // 0 |
|
arr.indexOf("z"); // -1 |
|
arr.indexOf("a", 2); // 4 |
|
arr.indexOf("a", -1); // 4 |
|
arr.indexOf("b", 3); // -1 |
|
arr.indexOf("a", 100); // -1 |
|
``` |
|
|
|
@method indexOf |
|
@param {Object} object the item to search for |
|
@param {Number} startAt optional starting location to search, default 0 |
|
@return {Number} index or -1 if not found |
|
*/ |
|
indexOf: function(object, startAt) { |
|
var idx, len = get(this, 'length'); |
|
|
|
if (startAt === undefined) startAt = 0; |
|
if (startAt < 0) startAt += len; |
|
|
|
for(idx=startAt;idx<len;idx++) { |
|
if (this.objectAt(idx) === object) return idx ; |
|
} |
|
return -1; |
|
}, |
|
|
|
/** |
|
Returns the index of the given object's last occurrence. |
|
If no `startAt` argument is given, the search starts from |
|
the last position. If it's negative, will count backward |
|
from the end of the array. Returns -1 if no match is found. |
|
|
|
```javascript |
|
var arr = ["a", "b", "c", "d", "a"]; |
|
arr.lastIndexOf("a"); // 4 |
|
arr.lastIndexOf("z"); // -1 |
|
arr.lastIndexOf("a", 2); // 0 |
|
arr.lastIndexOf("a", -1); // 4 |
|
arr.lastIndexOf("b", 3); // 1 |
|
arr.lastIndexOf("a", 100); // 4 |
|
``` |
|
|
|
@method lastIndexOf |
|
@param {Object} object the item to search for |
|
@param {Number} startAt optional starting location to search, default 0 |
|
@return {Number} index or -1 if not found |
|
*/ |
|
lastIndexOf: function(object, startAt) { |
|
var idx, len = get(this, 'length'); |
|
|
|
if (startAt === undefined || startAt >= len) startAt = len-1; |
|
if (startAt < 0) startAt += len; |
|
|
|
for(idx=startAt;idx>=0;idx--) { |
|
if (this.objectAt(idx) === object) return idx ; |
|
} |
|
return -1; |
|
}, |
|
|
|
// .......................................................... |
|
// ARRAY OBSERVERS |
|
// |
|
|
|
/** |
|
Adds an array observer to the receiving array. The array observer object |
|
normally must implement two methods: |
|
|
|
* `arrayWillChange(observedObj, start, removeCount, addCount)` - This method will be |
|
called just before the array is modified. |
|
* `arrayDidChange(observedObj, start, removeCount, addCount)` - This method will be |
|
called just after the array is modified. |
|
|
|
Both callbacks will be passed the observed object, starting index of the |
|
change as well a a count of the items to be removed and added. You can use |
|
these callbacks to optionally inspect the array during the change, clear |
|
caches, or do any other bookkeeping necessary. |
|
|
|
In addition to passing a target, you can also include an options hash |
|
which you can use to override the method names that will be invoked on the |
|
target. |
|
|
|
@method addArrayObserver |
|
@param {Object} target The observer object. |
|
@param {Hash} opts Optional hash of configuration options including |
|
`willChange` and `didChange` option. |
|
@return {Ember.Array} receiver |
|
*/ |
|
addArrayObserver: function(target, opts) { |
|
var willChange = (opts && opts.willChange) || 'arrayWillChange', |
|
didChange = (opts && opts.didChange) || 'arrayDidChange'; |
|
|
|
var hasObservers = get(this, 'hasArrayObservers'); |
|
if (!hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); |
|
Ember.addListener(this, '@array:before', target, willChange); |
|
Ember.addListener(this, '@array:change', target, didChange); |
|
if (!hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); |
|
return this; |
|
}, |
|
|
|
/** |
|
Removes an array observer from the object if the observer is current |
|
registered. Calling this method multiple times with the same object will |
|
have no effect. |
|
|
|
@method removeArrayObserver |
|
@param {Object} target The object observing the array. |
|
@param {Hash} opts Optional hash of configuration options including |
|
`willChange` and `didChange` option. |
|
@return {Ember.Array} receiver |
|
*/ |
|
removeArrayObserver: function(target, opts) { |
|
var willChange = (opts && opts.willChange) || 'arrayWillChange', |
|
didChange = (opts && opts.didChange) || 'arrayDidChange'; |
|
|
|
var hasObservers = get(this, 'hasArrayObservers'); |
|
if (hasObservers) Ember.propertyWillChange(this, 'hasArrayObservers'); |
|
Ember.removeListener(this, '@array:before', target, willChange); |
|
Ember.removeListener(this, '@array:change', target, didChange); |
|
if (hasObservers) Ember.propertyDidChange(this, 'hasArrayObservers'); |
|
return this; |
|
}, |
|
|
|
/** |
|
Becomes true whenever the array currently has observers watching changes |
|
on the array. |
|
|
|
@property {Boolean} hasArrayObservers |
|
*/ |
|
hasArrayObservers: Ember.computed(function() { |
|
return Ember.hasListeners(this, '@array:change') || Ember.hasListeners(this, '@array:before'); |
|
}), |
|
|
|
/** |
|
If you are implementing an object that supports `Ember.Array`, call this |
|
method just before the array content changes to notify any observers and |
|
invalidate any related properties. Pass the starting index of the change |
|
as well as a delta of the amounts to change. |
|
|
|
@method arrayContentWillChange |
|
@param {Number} startIdx The starting index in the array that will change. |
|
@param {Number} removeAmt The number of items that will be removed. If you |
|
pass `null` assumes 0 |
|
@param {Number} addAmt The number of items that will be added. If you |
|
pass `null` assumes 0. |
|
@return {Ember.Array} receiver |
|
*/ |
|
arrayContentWillChange: function(startIdx, removeAmt, addAmt) { |
|
|
|
// if no args are passed assume everything changes |
|
if (startIdx===undefined) { |
|
startIdx = 0; |
|
removeAmt = addAmt = -1; |
|
} else { |
|
if (removeAmt === undefined) removeAmt=-1; |
|
if (addAmt === undefined) addAmt=-1; |
|
} |
|
|
|
// Make sure the @each proxy is set up if anyone is observing @each |
|
if (Ember.isWatching(this, '@each')) { get(this, '@each'); } |
|
|
|
Ember.sendEvent(this, '@array:before', [this, startIdx, removeAmt, addAmt]); |
|
|
|
var removing, lim; |
|
if (startIdx>=0 && removeAmt>=0 && get(this, 'hasEnumerableObservers')) { |
|
removing = []; |
|
lim = startIdx+removeAmt; |
|
for(var idx=startIdx;idx<lim;idx++) removing.push(this.objectAt(idx)); |
|
} else { |
|
removing = removeAmt; |
|
} |
|
|
|
this.enumerableContentWillChange(removing, addAmt); |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
If you are implementing an object that supports `Ember.Array`, call this |
|
method just after the array content changes to notify any observers and |
|
invalidate any related properties. Pass the starting index of the change |
|
as well as a delta of the amounts to change. |
|
|
|
@method arrayContentDidChange |
|
@param {Number} startIdx The starting index in the array that did change. |
|
@param {Number} removeAmt The number of items that were removed. If you |
|
pass `null` assumes 0 |
|
@param {Number} addAmt The number of items that were added. If you |
|
pass `null` assumes 0. |
|
@return {Ember.Array} receiver |
|
*/ |
|
arrayContentDidChange: function(startIdx, removeAmt, addAmt) { |
|
|
|
// if no args are passed assume everything changes |
|
if (startIdx===undefined) { |
|
startIdx = 0; |
|
removeAmt = addAmt = -1; |
|
} else { |
|
if (removeAmt === undefined) removeAmt=-1; |
|
if (addAmt === undefined) addAmt=-1; |
|
} |
|
|
|
var adding, lim; |
|
if (startIdx>=0 && addAmt>=0 && get(this, 'hasEnumerableObservers')) { |
|
adding = []; |
|
lim = startIdx+addAmt; |
|
for(var idx=startIdx;idx<lim;idx++) adding.push(this.objectAt(idx)); |
|
} else { |
|
adding = addAmt; |
|
} |
|
|
|
this.enumerableContentDidChange(removeAmt, adding); |
|
Ember.sendEvent(this, '@array:change', [this, startIdx, removeAmt, addAmt]); |
|
|
|
var length = get(this, 'length'), |
|
cachedFirst = cacheFor(this, 'firstObject'), |
|
cachedLast = cacheFor(this, 'lastObject'); |
|
if (this.objectAt(0) !== cachedFirst) { |
|
Ember.propertyWillChange(this, 'firstObject'); |
|
Ember.propertyDidChange(this, 'firstObject'); |
|
} |
|
if (this.objectAt(length-1) !== cachedLast) { |
|
Ember.propertyWillChange(this, 'lastObject'); |
|
Ember.propertyDidChange(this, 'lastObject'); |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
// .......................................................... |
|
// ENUMERATED PROPERTIES |
|
// |
|
|
|
/** |
|
Returns a special object that can be used to observe individual properties |
|
on the array. Just get an equivalent property on this object and it will |
|
return an enumerable that maps automatically to the named key on the |
|
member objects. |
|
|
|
If you merely want to watch for any items being added or removed to the array, |
|
use the `[]` property instead of `@each`. |
|
|
|
@property @each |
|
*/ |
|
'@each': Ember.computed(function() { |
|
if (!this.__each) this.__each = new Ember.EachProxy(this); |
|
return this.__each; |
|
}) |
|
|
|
}) ; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var e_get = Ember.get, |
|
set = Ember.set, |
|
guidFor = Ember.guidFor, |
|
metaFor = Ember.meta, |
|
propertyWillChange = Ember.propertyWillChange, |
|
propertyDidChange = Ember.propertyDidChange, |
|
addBeforeObserver = Ember.addBeforeObserver, |
|
removeBeforeObserver = Ember.removeBeforeObserver, |
|
addObserver = Ember.addObserver, |
|
removeObserver = Ember.removeObserver, |
|
ComputedProperty = Ember.ComputedProperty, |
|
a_slice = [].slice, |
|
o_create = Ember.create, |
|
forEach = Ember.EnumerableUtils.forEach, |
|
cacheSet = Ember.cacheFor.set, |
|
cacheGet = Ember.cacheFor.get, |
|
cacheRemove = Ember.cacheFor.remove, |
|
// Here we explicitly don't allow `@each.foo`; it would require some special |
|
// testing, but there's no particular reason why it should be disallowed. |
|
eachPropertyPattern = /^(.*)\.@each\.(.*)/, |
|
doubleEachPropertyPattern = /(.*\.@each){2,}/, |
|
arrayBracketPattern = /\.\[\]$/; |
|
|
|
var expandProperties = Ember.expandProperties; |
|
|
|
function get(obj, key) { |
|
if (key === '@this') { |
|
return obj; |
|
} |
|
|
|
return e_get(obj, key); |
|
} |
|
|
|
/* |
|
Tracks changes to dependent arrays, as well as to properties of items in |
|
dependent arrays. |
|
|
|
@class DependentArraysObserver |
|
*/ |
|
function DependentArraysObserver(callbacks, cp, instanceMeta, context, propertyName, sugarMeta) { |
|
// user specified callbacks for `addedItem` and `removedItem` |
|
this.callbacks = callbacks; |
|
|
|
// the computed property: remember these are shared across instances |
|
this.cp = cp; |
|
|
|
// the ReduceComputedPropertyInstanceMeta this DependentArraysObserver is |
|
// associated with |
|
this.instanceMeta = instanceMeta; |
|
|
|
// A map of array guids to dependentKeys, for the given context. We track |
|
// this because we want to set up the computed property potentially before the |
|
// dependent array even exists, but when the array observer fires, we lack |
|
// enough context to know what to update: we can recover that context by |
|
// getting the dependentKey. |
|
this.dependentKeysByGuid = {}; |
|
|
|
// a map of dependent array guids -> Ember.TrackedArray instances. We use |
|
// this to lazily recompute indexes for item property observers. |
|
this.trackedArraysByGuid = {}; |
|
|
|
// We suspend observers to ignore replacements from `reset` when totally |
|
// recomputing. Unfortunately we cannot properly suspend the observers |
|
// because we only have the key; instead we make the observers no-ops |
|
this.suspended = false; |
|
|
|
// This is used to coalesce item changes from property observers. |
|
this.changedItems = {}; |
|
} |
|
|
|
function ItemPropertyObserverContext (dependentArray, index, trackedArray) { |
|
Ember.assert("Internal error: trackedArray is null or undefined", trackedArray); |
|
|
|
this.dependentArray = dependentArray; |
|
this.index = index; |
|
this.item = dependentArray.objectAt(index); |
|
this.trackedArray = trackedArray; |
|
this.beforeObserver = null; |
|
this.observer = null; |
|
|
|
this.destroyed = false; |
|
} |
|
|
|
DependentArraysObserver.prototype = { |
|
setValue: function (newValue) { |
|
this.instanceMeta.setValue(newValue, true); |
|
}, |
|
getValue: function () { |
|
return this.instanceMeta.getValue(); |
|
}, |
|
|
|
setupObservers: function (dependentArray, dependentKey) { |
|
this.dependentKeysByGuid[guidFor(dependentArray)] = dependentKey; |
|
|
|
dependentArray.addArrayObserver(this, { |
|
willChange: 'dependentArrayWillChange', |
|
didChange: 'dependentArrayDidChange' |
|
}); |
|
|
|
if (this.cp._itemPropertyKeys[dependentKey]) { |
|
this.setupPropertyObservers(dependentKey, this.cp._itemPropertyKeys[dependentKey]); |
|
} |
|
}, |
|
|
|
teardownObservers: function (dependentArray, dependentKey) { |
|
var itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || []; |
|
|
|
delete this.dependentKeysByGuid[guidFor(dependentArray)]; |
|
|
|
this.teardownPropertyObservers(dependentKey, itemPropertyKeys); |
|
|
|
dependentArray.removeArrayObserver(this, { |
|
willChange: 'dependentArrayWillChange', |
|
didChange: 'dependentArrayDidChange' |
|
}); |
|
}, |
|
|
|
suspendArrayObservers: function (callback, binding) { |
|
var oldSuspended = this.suspended; |
|
this.suspended = true; |
|
callback.call(binding); |
|
this.suspended = oldSuspended; |
|
}, |
|
|
|
setupPropertyObservers: function (dependentKey, itemPropertyKeys) { |
|
var dependentArray = get(this.instanceMeta.context, dependentKey), |
|
length = get(dependentArray, 'length'), |
|
observerContexts = new Array(length); |
|
|
|
this.resetTransformations(dependentKey, observerContexts); |
|
|
|
forEach(dependentArray, function (item, index) { |
|
var observerContext = this.createPropertyObserverContext(dependentArray, index, this.trackedArraysByGuid[dependentKey]); |
|
observerContexts[index] = observerContext; |
|
|
|
forEach(itemPropertyKeys, function (propertyKey) { |
|
addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); |
|
addObserver(item, propertyKey, this, observerContext.observer); |
|
}, this); |
|
}, this); |
|
}, |
|
|
|
teardownPropertyObservers: function (dependentKey, itemPropertyKeys) { |
|
var dependentArrayObserver = this, |
|
trackedArray = this.trackedArraysByGuid[dependentKey], |
|
beforeObserver, |
|
observer, |
|
item; |
|
|
|
if (!trackedArray) { return; } |
|
|
|
trackedArray.apply(function (observerContexts, offset, operation) { |
|
if (operation === Ember.TrackedArray.DELETE) { return; } |
|
|
|
forEach(observerContexts, function (observerContext) { |
|
observerContext.destroyed = true; |
|
beforeObserver = observerContext.beforeObserver; |
|
observer = observerContext.observer; |
|
item = observerContext.item; |
|
|
|
forEach(itemPropertyKeys, function (propertyKey) { |
|
removeBeforeObserver(item, propertyKey, dependentArrayObserver, beforeObserver); |
|
removeObserver(item, propertyKey, dependentArrayObserver, observer); |
|
}); |
|
}); |
|
}); |
|
}, |
|
|
|
createPropertyObserverContext: function (dependentArray, index, trackedArray) { |
|
var observerContext = new ItemPropertyObserverContext(dependentArray, index, trackedArray); |
|
|
|
this.createPropertyObserver(observerContext); |
|
|
|
return observerContext; |
|
}, |
|
|
|
createPropertyObserver: function (observerContext) { |
|
var dependentArrayObserver = this; |
|
|
|
observerContext.beforeObserver = function (obj, keyName) { |
|
return dependentArrayObserver.itemPropertyWillChange(obj, keyName, observerContext.dependentArray, observerContext); |
|
}; |
|
observerContext.observer = function (obj, keyName) { |
|
return dependentArrayObserver.itemPropertyDidChange(obj, keyName, observerContext.dependentArray, observerContext); |
|
}; |
|
}, |
|
|
|
resetTransformations: function (dependentKey, observerContexts) { |
|
this.trackedArraysByGuid[dependentKey] = new Ember.TrackedArray(observerContexts); |
|
}, |
|
|
|
trackAdd: function (dependentKey, index, newItems) { |
|
var trackedArray = this.trackedArraysByGuid[dependentKey]; |
|
if (trackedArray) { |
|
trackedArray.addItems(index, newItems); |
|
} |
|
}, |
|
|
|
trackRemove: function (dependentKey, index, removedCount) { |
|
var trackedArray = this.trackedArraysByGuid[dependentKey]; |
|
|
|
if (trackedArray) { |
|
return trackedArray.removeItems(index, removedCount); |
|
} |
|
|
|
return []; |
|
}, |
|
|
|
updateIndexes: function (trackedArray, array) { |
|
var length = get(array, 'length'); |
|
// OPTIMIZE: we could stop updating once we hit the object whose observer |
|
// fired; ie partially apply the transformations |
|
trackedArray.apply(function (observerContexts, offset, operation) { |
|
// we don't even have observer contexts for removed items, even if we did, |
|
// they no longer have any index in the array |
|
if (operation === Ember.TrackedArray.DELETE) { return; } |
|
if (operation === Ember.TrackedArray.RETAIN && observerContexts.length === length && offset === 0) { |
|
// If we update many items we don't want to walk the array each time: we |
|
// only need to update the indexes at most once per run loop. |
|
return; |
|
} |
|
|
|
forEach(observerContexts, function (context, index) { |
|
context.index = index + offset; |
|
}); |
|
}); |
|
}, |
|
|
|
dependentArrayWillChange: function (dependentArray, index, removedCount, addedCount) { |
|
if (this.suspended) { return; } |
|
|
|
var removedItem = this.callbacks.removedItem, |
|
changeMeta, |
|
guid = guidFor(dependentArray), |
|
dependentKey = this.dependentKeysByGuid[guid], |
|
itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey] || [], |
|
length = get(dependentArray, 'length'), |
|
normalizedIndex = normalizeIndex(index, length, 0), |
|
normalizedRemoveCount = normalizeRemoveCount(normalizedIndex, length, removedCount), |
|
item, |
|
itemIndex, |
|
sliceIndex, |
|
observerContexts; |
|
|
|
observerContexts = this.trackRemove(dependentKey, normalizedIndex, normalizedRemoveCount); |
|
|
|
function removeObservers(propertyKey) { |
|
observerContexts[sliceIndex].destroyed = true; |
|
removeBeforeObserver(item, propertyKey, this, observerContexts[sliceIndex].beforeObserver); |
|
removeObserver(item, propertyKey, this, observerContexts[sliceIndex].observer); |
|
} |
|
|
|
for (sliceIndex = normalizedRemoveCount - 1; sliceIndex >= 0; --sliceIndex) { |
|
itemIndex = normalizedIndex + sliceIndex; |
|
if (itemIndex >= length) { break; } |
|
|
|
item = dependentArray.objectAt(itemIndex); |
|
|
|
forEach(itemPropertyKeys, removeObservers, this); |
|
|
|
changeMeta = createChangeMeta(dependentArray, item, itemIndex, this.instanceMeta.propertyName, this.cp); |
|
this.setValue( removedItem.call( |
|
this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); |
|
} |
|
}, |
|
|
|
dependentArrayDidChange: function (dependentArray, index, removedCount, addedCount) { |
|
if (this.suspended) { return; } |
|
|
|
var addedItem = this.callbacks.addedItem, |
|
guid = guidFor(dependentArray), |
|
dependentKey = this.dependentKeysByGuid[guid], |
|
observerContexts = new Array(addedCount), |
|
itemPropertyKeys = this.cp._itemPropertyKeys[dependentKey], |
|
length = get(dependentArray, 'length'), |
|
normalizedIndex = normalizeIndex(index, length, addedCount), |
|
changeMeta, |
|
observerContext; |
|
|
|
forEach(dependentArray.slice(normalizedIndex, normalizedIndex + addedCount), function (item, sliceIndex) { |
|
if (itemPropertyKeys) { |
|
observerContext = |
|
observerContexts[sliceIndex] = |
|
this.createPropertyObserverContext(dependentArray, normalizedIndex + sliceIndex, this.trackedArraysByGuid[dependentKey]); |
|
forEach(itemPropertyKeys, function (propertyKey) { |
|
addBeforeObserver(item, propertyKey, this, observerContext.beforeObserver); |
|
addObserver(item, propertyKey, this, observerContext.observer); |
|
}, this); |
|
} |
|
|
|
changeMeta = createChangeMeta(dependentArray, item, normalizedIndex + sliceIndex, this.instanceMeta.propertyName, this.cp); |
|
this.setValue( addedItem.call( |
|
this.instanceMeta.context, this.getValue(), item, changeMeta, this.instanceMeta.sugarMeta)); |
|
}, this); |
|
|
|
this.trackAdd(dependentKey, normalizedIndex, observerContexts); |
|
}, |
|
|
|
itemPropertyWillChange: function (obj, keyName, array, observerContext) { |
|
var guid = guidFor(obj); |
|
|
|
if (!this.changedItems[guid]) { |
|
this.changedItems[guid] = { |
|
array: array, |
|
observerContext: observerContext, |
|
obj: obj, |
|
previousValues: {} |
|
}; |
|
} |
|
|
|
this.changedItems[guid].previousValues[keyName] = get(obj, keyName); |
|
}, |
|
|
|
itemPropertyDidChange: function(obj, keyName, array, observerContext) { |
|
this.flushChanges(); |
|
}, |
|
|
|
flushChanges: function() { |
|
var changedItems = this.changedItems, key, c, changeMeta; |
|
|
|
for (key in changedItems) { |
|
c = changedItems[key]; |
|
if (c.observerContext.destroyed) { continue; } |
|
|
|
this.updateIndexes(c.observerContext.trackedArray, c.observerContext.dependentArray); |
|
|
|
changeMeta = createChangeMeta(c.array, c.obj, c.observerContext.index, this.instanceMeta.propertyName, this.cp, c.previousValues); |
|
this.setValue( |
|
this.callbacks.removedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); |
|
this.setValue( |
|
this.callbacks.addedItem.call(this.instanceMeta.context, this.getValue(), c.obj, changeMeta, this.instanceMeta.sugarMeta)); |
|
} |
|
this.changedItems = {}; |
|
} |
|
}; |
|
|
|
function normalizeIndex(index, length, newItemsOffset) { |
|
if (index < 0) { |
|
return Math.max(0, length + index); |
|
} else if (index < length) { |
|
return index; |
|
} else /* index > length */ { |
|
return Math.min(length - newItemsOffset, index); |
|
} |
|
} |
|
|
|
function normalizeRemoveCount(index, length, removedCount) { |
|
return Math.min(removedCount, length - index); |
|
} |
|
|
|
function createChangeMeta(dependentArray, item, index, propertyName, property, previousValues) { |
|
var meta = { |
|
arrayChanged: dependentArray, |
|
index: index, |
|
item: item, |
|
propertyName: propertyName, |
|
property: property |
|
}; |
|
|
|
if (previousValues) { |
|
// previous values only available for item property changes |
|
meta.previousValues = previousValues; |
|
} |
|
|
|
return meta; |
|
} |
|
|
|
function addItems (dependentArray, callbacks, cp, propertyName, meta) { |
|
forEach(dependentArray, function (item, index) { |
|
meta.setValue( callbacks.addedItem.call( |
|
this, meta.getValue(), item, createChangeMeta(dependentArray, item, index, propertyName, cp), meta.sugarMeta)); |
|
}, this); |
|
} |
|
|
|
function reset(cp, propertyName) { |
|
var callbacks = cp._callbacks(), |
|
meta; |
|
|
|
if (cp._hasInstanceMeta(this, propertyName)) { |
|
meta = cp._instanceMeta(this, propertyName); |
|
meta.setValue(cp.resetValue(meta.getValue())); |
|
} else { |
|
meta = cp._instanceMeta(this, propertyName); |
|
} |
|
|
|
if (cp.options.initialize) { |
|
cp.options.initialize.call(this, meta.getValue(), { property: cp, propertyName: propertyName }, meta.sugarMeta); |
|
} |
|
} |
|
|
|
function partiallyRecomputeFor(obj, dependentKey) { |
|
if (arrayBracketPattern.test(dependentKey)) { |
|
return false; |
|
} |
|
|
|
var value = get(obj, dependentKey); |
|
return Ember.Array.detect(value); |
|
} |
|
|
|
function ReduceComputedPropertyInstanceMeta(context, propertyName, initialValue) { |
|
this.context = context; |
|
this.propertyName = propertyName; |
|
this.cache = metaFor(context).cache; |
|
|
|
this.dependentArrays = {}; |
|
this.sugarMeta = {}; |
|
|
|
this.initialValue = initialValue; |
|
} |
|
|
|
ReduceComputedPropertyInstanceMeta.prototype = { |
|
getValue: function () { |
|
if (this.propertyName in this.cache) { |
|
return this.cache[this.propertyName]; |
|
} else { |
|
return this.initialValue; |
|
} |
|
}, |
|
|
|
setValue: function(newValue, triggerObservers) { |
|
// This lets sugars force a recomputation, handy for very simple |
|
// implementations of eg max. |
|
if (newValue === this.cache[this.propertyName]) { |
|
return; |
|
} |
|
|
|
if (triggerObservers) { |
|
propertyWillChange(this.context, this.propertyName); |
|
} |
|
|
|
if (newValue === undefined) { |
|
delete this.cache[this.propertyName]; |
|
} else { |
|
this.cache[this.propertyName] = newValue; |
|
} |
|
|
|
if (triggerObservers) { |
|
propertyDidChange(this.context, this.propertyName); |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
A computed property whose dependent keys are arrays and which is updated with |
|
"one at a time" semantics. |
|
|
|
@class ReduceComputedProperty |
|
@namespace Ember |
|
@extends Ember.ComputedProperty |
|
@constructor |
|
*/ |
|
function ReduceComputedProperty(options) { |
|
var cp = this; |
|
|
|
this.options = options; |
|
|
|
this._dependentKeys = null; |
|
// A map of dependentKey -> [itemProperty, ...] that tracks what properties of |
|
// items in the array we must track to update this property. |
|
this._itemPropertyKeys = {}; |
|
this._previousItemPropertyKeys = {}; |
|
|
|
this.readOnly(); |
|
this.cacheable(); |
|
|
|
this.recomputeOnce = function(propertyName) { |
|
// TODO: Coalesce recomputation by <this, propertyName, cp>. |
|
recompute.call(this, propertyName); |
|
}; |
|
|
|
var recompute = function(propertyName) { |
|
var dependentKeys = cp._dependentKeys, |
|
meta = cp._instanceMeta(this, propertyName), |
|
callbacks = cp._callbacks(); |
|
|
|
reset.call(this, cp, propertyName); |
|
|
|
meta.dependentArraysObserver.suspendArrayObservers(function () { |
|
forEach(cp._dependentKeys, function (dependentKey) { |
|
Ember.assert( |
|
"dependent array " + dependentKey + " must be an `Ember.Array`. " + |
|
"If you are not extending arrays, you will need to wrap native arrays with `Ember.A`", |
|
!(Ember.isArray(get(this, dependentKey)) && !Ember.Array.detect(get(this, dependentKey)))); |
|
|
|
if (!partiallyRecomputeFor(this, dependentKey)) { return; } |
|
|
|
var dependentArray = get(this, dependentKey), |
|
previousDependentArray = meta.dependentArrays[dependentKey]; |
|
|
|
if (dependentArray === previousDependentArray) { |
|
// The array may be the same, but our item property keys may have |
|
// changed, so we set them up again. We can't easily tell if they've |
|
// changed: the array may be the same object, but with different |
|
// contents. |
|
if (cp._previousItemPropertyKeys[dependentKey]) { |
|
delete cp._previousItemPropertyKeys[dependentKey]; |
|
meta.dependentArraysObserver.setupPropertyObservers(dependentKey, cp._itemPropertyKeys[dependentKey]); |
|
} |
|
} else { |
|
meta.dependentArrays[dependentKey] = dependentArray; |
|
|
|
if (previousDependentArray) { |
|
meta.dependentArraysObserver.teardownObservers(previousDependentArray, dependentKey); |
|
} |
|
|
|
if (dependentArray) { |
|
meta.dependentArraysObserver.setupObservers(dependentArray, dependentKey); |
|
} |
|
} |
|
}, this); |
|
}, this); |
|
|
|
forEach(cp._dependentKeys, function(dependentKey) { |
|
if (!partiallyRecomputeFor(this, dependentKey)) { return; } |
|
|
|
var dependentArray = get(this, dependentKey); |
|
if (dependentArray) { |
|
addItems.call(this, dependentArray, callbacks, cp, propertyName, meta); |
|
} |
|
}, this); |
|
}; |
|
|
|
|
|
this.func = function (propertyName) { |
|
Ember.assert("Computed reduce values require at least one dependent key", cp._dependentKeys); |
|
|
|
recompute.call(this, propertyName); |
|
|
|
return cp._instanceMeta(this, propertyName).getValue(); |
|
}; |
|
} |
|
|
|
Ember.ReduceComputedProperty = ReduceComputedProperty; |
|
ReduceComputedProperty.prototype = o_create(ComputedProperty.prototype); |
|
|
|
function defaultCallback(computedValue) { |
|
return computedValue; |
|
} |
|
|
|
ReduceComputedProperty.prototype._callbacks = function () { |
|
if (!this.callbacks) { |
|
var options = this.options; |
|
this.callbacks = { |
|
removedItem: options.removedItem || defaultCallback, |
|
addedItem: options.addedItem || defaultCallback |
|
}; |
|
} |
|
return this.callbacks; |
|
}; |
|
|
|
ReduceComputedProperty.prototype._hasInstanceMeta = function (context, propertyName) { |
|
return !!metaFor(context).cacheMeta[propertyName]; |
|
}; |
|
|
|
ReduceComputedProperty.prototype._instanceMeta = function (context, propertyName) { |
|
var cacheMeta = metaFor(context).cacheMeta, |
|
meta = cacheMeta[propertyName]; |
|
|
|
if (!meta) { |
|
meta = cacheMeta[propertyName] = new ReduceComputedPropertyInstanceMeta(context, propertyName, this.initialValue()); |
|
meta.dependentArraysObserver = new DependentArraysObserver(this._callbacks(), this, meta, context, propertyName, meta.sugarMeta); |
|
} |
|
|
|
return meta; |
|
}; |
|
|
|
ReduceComputedProperty.prototype.initialValue = function () { |
|
if (typeof this.options.initialValue === 'function') { |
|
return this.options.initialValue(); |
|
} |
|
else { |
|
return this.options.initialValue; |
|
} |
|
}; |
|
|
|
ReduceComputedProperty.prototype.resetValue = function (value) { |
|
return this.initialValue(); |
|
}; |
|
|
|
ReduceComputedProperty.prototype.itemPropertyKey = function (dependentArrayKey, itemPropertyKey) { |
|
this._itemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey] || []; |
|
this._itemPropertyKeys[dependentArrayKey].push(itemPropertyKey); |
|
}; |
|
|
|
ReduceComputedProperty.prototype.clearItemPropertyKeys = function (dependentArrayKey) { |
|
if (this._itemPropertyKeys[dependentArrayKey]) { |
|
this._previousItemPropertyKeys[dependentArrayKey] = this._itemPropertyKeys[dependentArrayKey]; |
|
this._itemPropertyKeys[dependentArrayKey] = []; |
|
} |
|
}; |
|
|
|
ReduceComputedProperty.prototype.property = function () { |
|
var cp = this, |
|
args = a_slice.call(arguments), |
|
propertyArgs = new Ember.Set(), |
|
match, |
|
dependentArrayKey, |
|
itemPropertyKey; |
|
|
|
forEach(args, function (dependentKey) { |
|
if (doubleEachPropertyPattern.test(dependentKey)) { |
|
throw new Ember.Error("Nested @each properties not supported: " + dependentKey); |
|
} else if (match = eachPropertyPattern.exec(dependentKey)) { |
|
dependentArrayKey = match[1]; |
|
|
|
var itemPropertyKeyPattern = match[2], |
|
addItemPropertyKey = function (itemPropertyKey) { |
|
cp.itemPropertyKey(dependentArrayKey, itemPropertyKey); |
|
}; |
|
|
|
expandProperties(itemPropertyKeyPattern, addItemPropertyKey); |
|
propertyArgs.add(dependentArrayKey); |
|
} else { |
|
propertyArgs.add(dependentKey); |
|
} |
|
}); |
|
|
|
return ComputedProperty.prototype.property.apply(this, propertyArgs.toArray()); |
|
|
|
}; |
|
|
|
/** |
|
Creates a computed property which operates on dependent arrays and |
|
is updated with "one at a time" semantics. When items are added or |
|
removed from the dependent array(s) a reduce computed only operates |
|
on the change instead of re-evaluating the entire array. |
|
|
|
If there are more than one arguments the first arguments are |
|
considered to be dependent property keys. The last argument is |
|
required to be an options object. The options object can have the |
|
following four properties: |
|
|
|
`initialValue` - A value or function that will be used as the initial |
|
value for the computed. If this property is a function the result of calling |
|
the function will be used as the initial value. This property is required. |
|
|
|
`initialize` - An optional initialize function. Typically this will be used |
|
to set up state on the instanceMeta object. |
|
|
|
`removedItem` - A function that is called each time an element is removed |
|
from the array. |
|
|
|
`addedItem` - A function that is called each time an element is added to |
|
the array. |
|
|
|
|
|
The `initialize` function has the following signature: |
|
|
|
```javascript |
|
function (initialValue, changeMeta, instanceMeta) |
|
``` |
|
|
|
`initialValue` - The value of the `initialValue` property from the |
|
options object. |
|
|
|
`changeMeta` - An object which contains meta information about the |
|
computed. It contains the following properties: |
|
|
|
- `property` the computed property |
|
- `propertyName` the name of the property on the object |
|
|
|
`instanceMeta` - An object that can be used to store meta |
|
information needed for calculating your computed. For example a |
|
unique computed might use this to store the number of times a given |
|
element is found in the dependent array. |
|
|
|
|
|
The `removedItem` and `addedItem` functions both have the following signature: |
|
|
|
```javascript |
|
function (accumulatedValue, item, changeMeta, instanceMeta) |
|
``` |
|
|
|
`accumulatedValue` - The value returned from the last time |
|
`removedItem` or `addedItem` was called or `initialValue`. |
|
|
|
`item` - the element added or removed from the array |
|
|
|
`changeMeta` - An object which contains meta information about the |
|
change. It contains the following properties: |
|
|
|
- `property` the computed property |
|
- `propertyName` the name of the property on the object |
|
- `index` the index of the added or removed item |
|
- `item` the added or removed item: this is exactly the same as |
|
the second arg |
|
- `arrayChanged` the array that triggered the change. Can be |
|
useful when depending on multiple arrays. |
|
|
|
For property changes triggered on an item property change (when |
|
depKey is something like `someArray.@each.someProperty`), |
|
`changeMeta` will also contain the following property: |
|
|
|
- `previousValues` an object whose keys are the properties that changed on |
|
the item, and whose values are the item's previous values. |
|
|
|
`previousValues` is important Ember coalesces item property changes via |
|
Ember.run.once. This means that by the time removedItem gets called, item has |
|
the new values, but you may need the previous value (eg for sorting & |
|
filtering). |
|
|
|
`instanceMeta` - An object that can be used to store meta |
|
information needed for calculating your computed. For example a |
|
unique computed might use this to store the number of times a given |
|
element is found in the dependent array. |
|
|
|
The `removedItem` and `addedItem` functions should return the accumulated |
|
value. It is acceptable to not return anything (ie return undefined) |
|
to invalidate the computation. This is generally not a good idea for |
|
arrayComputed but it's used in eg max and min. |
|
|
|
Note that observers will be fired if either of these functions return a value |
|
that differs from the accumulated value. When returning an object that |
|
mutates in response to array changes, for example an array that maps |
|
everything from some other array (see `Ember.computed.map`), it is usually |
|
important that the *same* array be returned to avoid accidentally triggering observers. |
|
|
|
Example |
|
|
|
```javascript |
|
Ember.computed.max = function (dependentKey) { |
|
return Ember.reduceComputed(dependentKey, { |
|
initialValue: -Infinity, |
|
|
|
addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { |
|
return Math.max(accumulatedValue, item); |
|
}, |
|
|
|
removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { |
|
if (item < accumulatedValue) { |
|
return accumulatedValue; |
|
} |
|
} |
|
}); |
|
}; |
|
``` |
|
|
|
Dependent keys may refer to `@this` to observe changes to the object itself, |
|
which must be array-like, rather than a property of the object. This is |
|
mostly useful for array proxies, to ensure objects are retrieved via |
|
`objectAtContent`. This is how you could sort items by properties defined on an item controller. |
|
|
|
Example |
|
|
|
```javascript |
|
App.PeopleController = Ember.ArrayController.extend({ |
|
itemController: 'person', |
|
|
|
sortedPeople: Ember.computed.sort('@this.@each.reversedName', function(personA, personB) { |
|
// `reversedName` isn't defined on Person, but we have access to it via |
|
// the item controller App.PersonController. If we'd used |
|
// `content.@each.reversedName` above, we would be getting the objects |
|
// directly and not have access to `reversedName`. |
|
// |
|
var reversedNameA = get(personA, 'reversedName'), |
|
reversedNameB = get(personB, 'reversedName'); |
|
|
|
return Ember.compare(reversedNameA, reversedNameB); |
|
}) |
|
}); |
|
|
|
App.PersonController = Ember.ObjectController.extend({ |
|
reversedName: function () { |
|
return reverse(get(this, 'name')); |
|
}.property('name') |
|
}) |
|
``` |
|
|
|
Dependent keys whose values are not arrays are treated as regular |
|
dependencies: when they change, the computed property is completely |
|
recalculated. It is sometimes useful to have dependent arrays with similar |
|
semantics. Dependent keys which end in `.[]` do not use "one at a time" |
|
semantics. When an item is added or removed from such a dependency, the |
|
computed property is completely recomputed. |
|
|
|
Example |
|
|
|
```javascript |
|
Ember.Object.extend({ |
|
// When `string` is changed, `computed` is completely recomputed. |
|
string: 'a string', |
|
|
|
// When an item is added to `array`, `addedItem` is called. |
|
array: [], |
|
|
|
// When an item is added to `anotherArray`, `computed` is completely |
|
// recomputed. |
|
anotherArray: [], |
|
|
|
computed: Ember.reduceComputed('string', 'array', 'anotherArray.[]', { |
|
addedItem: addedItemCallback, |
|
removedItem: removedItemCallback |
|
}) |
|
}); |
|
``` |
|
|
|
@method reduceComputed |
|
@for Ember |
|
@param {String} [dependentKeys*] |
|
@param {Object} options |
|
@return {Ember.ComputedProperty} |
|
*/ |
|
Ember.reduceComputed = function (options) { |
|
var args; |
|
|
|
if (arguments.length > 1) { |
|
args = a_slice.call(arguments, 0, -1); |
|
options = a_slice.call(arguments, -1)[0]; |
|
} |
|
|
|
if (typeof options !== "object") { |
|
throw new Ember.Error("Reduce Computed Property declared without an options hash"); |
|
} |
|
|
|
if (!('initialValue' in options)) { |
|
throw new Ember.Error("Reduce Computed Property declared without an initial value"); |
|
} |
|
|
|
var cp = new ReduceComputedProperty(options); |
|
|
|
if (args) { |
|
cp.property.apply(cp, args); |
|
} |
|
|
|
return cp; |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var ReduceComputedProperty = Ember.ReduceComputedProperty, |
|
a_slice = [].slice, |
|
o_create = Ember.create, |
|
forEach = Ember.EnumerableUtils.forEach; |
|
|
|
function ArrayComputedProperty() { |
|
var cp = this; |
|
|
|
ReduceComputedProperty.apply(this, arguments); |
|
|
|
this.func = (function(reduceFunc) { |
|
return function (propertyName) { |
|
if (!cp._hasInstanceMeta(this, propertyName)) { |
|
// When we recompute an array computed property, we need already |
|
// retrieved arrays to be updated; we can't simply empty the cache and |
|
// hope the array is re-retrieved. |
|
forEach(cp._dependentKeys, function(dependentKey) { |
|
Ember.addObserver(this, dependentKey, function() { |
|
cp.recomputeOnce.call(this, propertyName); |
|
}); |
|
}, this); |
|
} |
|
|
|
return reduceFunc.apply(this, arguments); |
|
}; |
|
})(this.func); |
|
|
|
return this; |
|
} |
|
Ember.ArrayComputedProperty = ArrayComputedProperty; |
|
ArrayComputedProperty.prototype = o_create(ReduceComputedProperty.prototype); |
|
ArrayComputedProperty.prototype.initialValue = function () { |
|
return Ember.A(); |
|
}; |
|
ArrayComputedProperty.prototype.resetValue = function (array) { |
|
array.clear(); |
|
return array; |
|
}; |
|
|
|
// This is a stopgap to keep the reference counts correct with lazy CPs. |
|
ArrayComputedProperty.prototype.didChange = function (obj, keyName) { |
|
return; |
|
}; |
|
|
|
/** |
|
Creates a computed property which operates on dependent arrays and |
|
is updated with "one at a time" semantics. When items are added or |
|
removed from the dependent array(s) an array computed only operates |
|
on the change instead of re-evaluating the entire array. This should |
|
return an array, if you'd like to use "one at a time" semantics and |
|
compute some value other then an array look at |
|
`Ember.reduceComputed`. |
|
|
|
If there are more than one arguments the first arguments are |
|
considered to be dependent property keys. The last argument is |
|
required to be an options object. The options object can have the |
|
following three properties. |
|
|
|
`initialize` - An optional initialize function. Typically this will be used |
|
to set up state on the instanceMeta object. |
|
|
|
`removedItem` - A function that is called each time an element is |
|
removed from the array. |
|
|
|
`addedItem` - A function that is called each time an element is |
|
added to the array. |
|
|
|
|
|
The `initialize` function has the following signature: |
|
|
|
```javascript |
|
function (array, changeMeta, instanceMeta) |
|
``` |
|
|
|
`array` - The initial value of the arrayComputed, an empty array. |
|
|
|
`changeMeta` - An object which contains meta information about the |
|
computed. It contains the following properties: |
|
|
|
- `property` the computed property |
|
- `propertyName` the name of the property on the object |
|
|
|
`instanceMeta` - An object that can be used to store meta |
|
information needed for calculating your computed. For example a |
|
unique computed might use this to store the number of times a given |
|
element is found in the dependent array. |
|
|
|
|
|
The `removedItem` and `addedItem` functions both have the following signature: |
|
|
|
```javascript |
|
function (accumulatedValue, item, changeMeta, instanceMeta) |
|
``` |
|
|
|
`accumulatedValue` - The value returned from the last time |
|
`removedItem` or `addedItem` was called or an empty array. |
|
|
|
`item` - the element added or removed from the array |
|
|
|
`changeMeta` - An object which contains meta information about the |
|
change. It contains the following properties: |
|
|
|
- `property` the computed property |
|
- `propertyName` the name of the property on the object |
|
- `index` the index of the added or removed item |
|
- `item` the added or removed item: this is exactly the same as |
|
the second arg |
|
- `arrayChanged` the array that triggered the change. Can be |
|
useful when depending on multiple arrays. |
|
|
|
For property changes triggered on an item property change (when |
|
depKey is something like `someArray.@each.someProperty`), |
|
`changeMeta` will also contain the following property: |
|
|
|
- `previousValues` an object whose keys are the properties that changed on |
|
the item, and whose values are the item's previous values. |
|
|
|
`previousValues` is important Ember coalesces item property changes via |
|
Ember.run.once. This means that by the time removedItem gets called, item has |
|
the new values, but you may need the previous value (eg for sorting & |
|
filtering). |
|
|
|
`instanceMeta` - An object that can be used to store meta |
|
information needed for calculating your computed. For example a |
|
unique computed might use this to store the number of times a given |
|
element is found in the dependent array. |
|
|
|
The `removedItem` and `addedItem` functions should return the accumulated |
|
value. It is acceptable to not return anything (ie return undefined) |
|
to invalidate the computation. This is generally not a good idea for |
|
arrayComputed but it's used in eg max and min. |
|
|
|
Example |
|
|
|
```javascript |
|
Ember.computed.map = function(dependentKey, callback) { |
|
var options = { |
|
addedItem: function(array, item, changeMeta, instanceMeta) { |
|
var mapped = callback(item); |
|
array.insertAt(changeMeta.index, mapped); |
|
return array; |
|
}, |
|
removedItem: function(array, item, changeMeta, instanceMeta) { |
|
array.removeAt(changeMeta.index, 1); |
|
return array; |
|
} |
|
}; |
|
|
|
return Ember.arrayComputed(dependentKey, options); |
|
}; |
|
``` |
|
|
|
@method arrayComputed |
|
@for Ember |
|
@param {String} [dependentKeys*] |
|
@param {Object} options |
|
@return {Ember.ComputedProperty} |
|
*/ |
|
Ember.arrayComputed = function (options) { |
|
var args; |
|
|
|
if (arguments.length > 1) { |
|
args = a_slice.call(arguments, 0, -1); |
|
options = a_slice.call(arguments, -1)[0]; |
|
} |
|
|
|
if (typeof options !== "object") { |
|
throw new Ember.Error("Array Computed Property declared without an options hash"); |
|
} |
|
|
|
var cp = new ArrayComputedProperty(options); |
|
|
|
if (args) { |
|
cp.property.apply(cp, args); |
|
} |
|
|
|
return cp; |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var get = Ember.get, |
|
set = Ember.set, |
|
guidFor = Ember.guidFor, |
|
merge = Ember.merge, |
|
a_slice = [].slice, |
|
forEach = Ember.EnumerableUtils.forEach, |
|
map = Ember.EnumerableUtils.map, |
|
SearchProxy; |
|
|
|
/** |
|
A computed property that returns the sum of the value |
|
in the dependent array. |
|
|
|
@method computed.sum |
|
@for Ember |
|
@param {String} dependentKey |
|
@return {Ember.ComputedProperty} computes the sum of all values in the dependentKey's array |
|
*/ |
|
|
|
Ember.computed.sum = function(dependentKey){ |
|
return Ember.reduceComputed(dependentKey, { |
|
initialValue: 0, |
|
|
|
addedItem: function(accumulatedValue, item, changeMeta, instanceMeta){ |
|
return accumulatedValue + item; |
|
}, |
|
|
|
removedItem: function(accumulatedValue, item, changeMeta, instanceMeta){ |
|
return accumulatedValue - item; |
|
} |
|
}); |
|
}; |
|
|
|
/** |
|
A computed property that calculates the maximum value in the |
|
dependent array. This will return `-Infinity` when the dependent |
|
array is empty. |
|
|
|
```javascript |
|
App.Person = Ember.Object.extend({ |
|
childAges: Ember.computed.mapBy('children', 'age'), |
|
maxChildAge: Ember.computed.max('childAges') |
|
}); |
|
|
|
var lordByron = App.Person.create({children: []}); |
|
lordByron.get('maxChildAge'); // -Infinity |
|
lordByron.get('children').pushObject({ |
|
name: 'Augusta Ada Byron', age: 7 |
|
}); |
|
lordByron.get('maxChildAge'); // 7 |
|
lordByron.get('children').pushObjects([{ |
|
name: 'Allegra Byron', |
|
age: 5 |
|
}, { |
|
name: 'Elizabeth Medora Leigh', |
|
age: 8 |
|
}]); |
|
lordByron.get('maxChildAge'); // 8 |
|
``` |
|
|
|
@method computed.max |
|
@for Ember |
|
@param {String} dependentKey |
|
@return {Ember.ComputedProperty} computes the largest value in the dependentKey's array |
|
*/ |
|
Ember.computed.max = function (dependentKey) { |
|
return Ember.reduceComputed(dependentKey, { |
|
initialValue: -Infinity, |
|
|
|
addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { |
|
return Math.max(accumulatedValue, item); |
|
}, |
|
|
|
removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { |
|
if (item < accumulatedValue) { |
|
return accumulatedValue; |
|
} |
|
} |
|
}); |
|
}; |
|
|
|
/** |
|
A computed property that calculates the minimum value in the |
|
dependent array. This will return `Infinity` when the dependent |
|
array is empty. |
|
|
|
```javascript |
|
App.Person = Ember.Object.extend({ |
|
childAges: Ember.computed.mapBy('children', 'age'), |
|
minChildAge: Ember.computed.min('childAges') |
|
}); |
|
|
|
var lordByron = App.Person.create({children: []}); |
|
lordByron.get('minChildAge'); // Infinity |
|
lordByron.get('children').pushObject({ |
|
name: 'Augusta Ada Byron', age: 7 |
|
}); |
|
lordByron.get('minChildAge'); // 7 |
|
lordByron.get('children').pushObjects([{ |
|
name: 'Allegra Byron', |
|
age: 5 |
|
}, { |
|
name: 'Elizabeth Medora Leigh', |
|
age: 8 |
|
}]); |
|
lordByron.get('minChildAge'); // 5 |
|
``` |
|
|
|
@method computed.min |
|
@for Ember |
|
@param {String} dependentKey |
|
@return {Ember.ComputedProperty} computes the smallest value in the dependentKey's array |
|
*/ |
|
Ember.computed.min = function (dependentKey) { |
|
return Ember.reduceComputed(dependentKey, { |
|
initialValue: Infinity, |
|
|
|
addedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { |
|
return Math.min(accumulatedValue, item); |
|
}, |
|
|
|
removedItem: function (accumulatedValue, item, changeMeta, instanceMeta) { |
|
if (item > accumulatedValue) { |
|
return accumulatedValue; |
|
} |
|
} |
|
}); |
|
}; |
|
|
|
/** |
|
Returns an array mapped via the callback |
|
|
|
The callback method you provide should have the following signature. |
|
`item` is the current item in the iteration. |
|
|
|
```javascript |
|
function(item); |
|
``` |
|
|
|
Example |
|
|
|
```javascript |
|
App.Hamster = Ember.Object.extend({ |
|
excitingChores: Ember.computed.map('chores', function(chore) { |
|
return chore.toUpperCase() + '!'; |
|
}) |
|
}); |
|
|
|
var hamster = App.Hamster.create({ |
|
chores: ['clean', 'write more unit tests'] |
|
}); |
|
hamster.get('excitingChores'); // ['CLEAN!', 'WRITE MORE UNIT TESTS!'] |
|
``` |
|
|
|
@method computed.map |
|
@for Ember |
|
@param {String} dependentKey |
|
@param {Function} callback |
|
@return {Ember.ComputedProperty} an array mapped via the callback |
|
*/ |
|
Ember.computed.map = function(dependentKey, callback) { |
|
var options = { |
|
addedItem: function(array, item, changeMeta, instanceMeta) { |
|
var mapped = callback.call(this, item); |
|
array.insertAt(changeMeta.index, mapped); |
|
return array; |
|
}, |
|
removedItem: function(array, item, changeMeta, instanceMeta) { |
|
array.removeAt(changeMeta.index, 1); |
|
return array; |
|
} |
|
}; |
|
|
|
return Ember.arrayComputed(dependentKey, options); |
|
}; |
|
|
|
/** |
|
Returns an array mapped to the specified key. |
|
|
|
```javascript |
|
App.Person = Ember.Object.extend({ |
|
childAges: Ember.computed.mapBy('children', 'age') |
|
}); |
|
|
|
var lordByron = App.Person.create({children: []}); |
|
lordByron.get('childAges'); // [] |
|
lordByron.get('children').pushObject({name: 'Augusta Ada Byron', age: 7}); |
|
lordByron.get('childAges'); // [7] |
|
lordByron.get('children').pushObjects([{ |
|
name: 'Allegra Byron', |
|
age: 5 |
|
}, { |
|
name: 'Elizabeth Medora Leigh', |
|
age: 8 |
|
}]); |
|
lordByron.get('childAges'); // [7, 5, 8] |
|
``` |
|
|
|
@method computed.mapBy |
|
@for Ember |
|
@param {String} dependentKey |
|
@param {String} propertyKey |
|
@return {Ember.ComputedProperty} an array mapped to the specified key |
|
*/ |
|
Ember.computed.mapBy = function(dependentKey, propertyKey) { |
|
var callback = function(item) { return get(item, propertyKey); }; |
|
return Ember.computed.map(dependentKey + '.@each.' + propertyKey, callback); |
|
}; |
|
|
|
/** |
|
@method computed.mapProperty |
|
@for Ember |
|
@deprecated Use `Ember.computed.mapBy` instead |
|
@param dependentKey |
|
@param propertyKey |
|
*/ |
|
Ember.computed.mapProperty = Ember.computed.mapBy; |
|
|
|
/** |
|
Filters the array by the callback. |
|
|
|
The callback method you provide should have the following signature. |
|
`item` is the current item in the iteration. |
|
|
|
```javascript |
|
function(item); |
|
``` |
|
|
|
```javascript |
|
App.Hamster = Ember.Object.extend({ |
|
remainingChores: Ember.computed.filter('chores', function(chore) { |
|
return !chore.done; |
|
}) |
|
}); |
|
|
|
var hamster = App.Hamster.create({chores: [ |
|
{name: 'cook', done: true}, |
|
{name: 'clean', done: true}, |
|
{name: 'write more unit tests', done: false} |
|
]}); |
|
hamster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] |
|
``` |
|
|
|
@method computed.filter |
|
@for Ember |
|
@param {String} dependentKey |
|
@param {Function} callback |
|
@return {Ember.ComputedProperty} the filtered array |
|
*/ |
|
Ember.computed.filter = function(dependentKey, callback) { |
|
var options = { |
|
initialize: function (array, changeMeta, instanceMeta) { |
|
instanceMeta.filteredArrayIndexes = new Ember.SubArray(); |
|
}, |
|
|
|
addedItem: function(array, item, changeMeta, instanceMeta) { |
|
var match = !!callback.call(this, item), |
|
filterIndex = instanceMeta.filteredArrayIndexes.addItem(changeMeta.index, match); |
|
|
|
if (match) { |
|
array.insertAt(filterIndex, item); |
|
} |
|
|
|
return array; |
|
}, |
|
|
|
removedItem: function(array, item, changeMeta, instanceMeta) { |
|
var filterIndex = instanceMeta.filteredArrayIndexes.removeItem(changeMeta.index); |
|
|
|
if (filterIndex > -1) { |
|
array.removeAt(filterIndex); |
|
} |
|
|
|
return array; |
|
} |
|
}; |
|
|
|
return Ember.arrayComputed(dependentKey, options); |
|
}; |
|
|
|
/** |
|
Filters the array by the property and value |
|
|
|
```javascript |
|
App.Hamster = Ember.Object.extend({ |
|
remainingChores: Ember.computed.filterBy('chores', 'done', false) |
|
}); |
|
|
|
var hamster = App.Hamster.create({chores: [ |
|
{name: 'cook', done: true}, |
|
{name: 'clean', done: true}, |
|
{name: 'write more unit tests', done: false} |
|
]}); |
|
hamster.get('remainingChores'); // [{name: 'write more unit tests', done: false}] |
|
``` |
|
|
|
@method computed.filterBy |
|
@for Ember |
|
@param {String} dependentKey |
|
@param {String} propertyKey |
|
@param {*} value |
|
@return {Ember.ComputedProperty} the filtered array |
|
*/ |
|
Ember.computed.filterBy = function(dependentKey, propertyKey, value) { |
|
var callback; |
|
|
|
if (arguments.length === 2) { |
|
callback = function(item) { |
|
return get(item, propertyKey); |
|
}; |
|
} else { |
|
callback = function(item) { |
|
return get(item, propertyKey) === value; |
|
}; |
|
} |
|
|
|
return Ember.computed.filter(dependentKey + '.@each.' + propertyKey, callback); |
|
}; |
|
|
|
/** |
|
@method computed.filterProperty |
|
@for Ember |
|
@param dependentKey |
|
@param propertyKey |
|
@param value |
|
@deprecated Use `Ember.computed.filterBy` instead |
|
*/ |
|
Ember.computed.filterProperty = Ember.computed.filterBy; |
|
|
|
/** |
|
A computed property which returns a new array with all the unique |
|
elements from one or more dependent arrays. |
|
|
|
Example |
|
|
|
```javascript |
|
App.Hamster = Ember.Object.extend({ |
|
uniqueFruits: Ember.computed.uniq('fruits') |
|
}); |
|
|
|
var hamster = App.Hamster.create({fruits: [ |
|
'banana', |
|
'grape', |
|
'kale', |
|
'banana' |
|
]}); |
|
hamster.get('uniqueFruits'); // ['banana', 'grape', 'kale'] |
|
``` |
|
|
|
@method computed.uniq |
|
@for Ember |
|
@param {String} propertyKey* |
|
@return {Ember.ComputedProperty} computes a new array with all the |
|
unique elements from the dependent array |
|
*/ |
|
Ember.computed.uniq = function() { |
|
var args = a_slice.call(arguments); |
|
args.push({ |
|
initialize: function(array, changeMeta, instanceMeta) { |
|
instanceMeta.itemCounts = {}; |
|
}, |
|
|
|
addedItem: function(array, item, changeMeta, instanceMeta) { |
|
var guid = guidFor(item); |
|
|
|
if (!instanceMeta.itemCounts[guid]) { |
|
instanceMeta.itemCounts[guid] = 1; |
|
} else { |
|
++instanceMeta.itemCounts[guid]; |
|
} |
|
array.addObject(item); |
|
return array; |
|
}, |
|
removedItem: function(array, item, _, instanceMeta) { |
|
var guid = guidFor(item), |
|
itemCounts = instanceMeta.itemCounts; |
|
|
|
if (--itemCounts[guid] === 0) { |
|
array.removeObject(item); |
|
} |
|
return array; |
|
} |
|
}); |
|
return Ember.arrayComputed.apply(null, args); |
|
}; |
|
|
|
/** |
|
Alias for [Ember.computed.uniq](/api/#method_computed_uniq). |
|
|
|
@method computed.union |
|
@for Ember |
|
@param {String} propertyKey* |
|
@return {Ember.ComputedProperty} computes a new array with all the |
|
unique elements from the dependent array |
|
*/ |
|
Ember.computed.union = Ember.computed.uniq; |
|
|
|
/** |
|
A computed property which returns a new array with all the duplicated |
|
elements from two or more dependent arrays. |
|
|
|
Example |
|
|
|
```javascript |
|
var obj = Ember.Object.createWithMixins({ |
|
adaFriends: ['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'], |
|
charlesFriends: ['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock'], |
|
friendsInCommon: Ember.computed.intersect('adaFriends', 'charlesFriends') |
|
}); |
|
|
|
obj.get('friendsInCommon'); // ['William King', 'Mary Somerville'] |
|
``` |
|
|
|
@method computed.intersect |
|
@for Ember |
|
@param {String} propertyKey* |
|
@return {Ember.ComputedProperty} computes a new array with all the |
|
duplicated elements from the dependent arrays |
|
*/ |
|
Ember.computed.intersect = function () { |
|
var getDependentKeyGuids = function (changeMeta) { |
|
return map(changeMeta.property._dependentKeys, function (dependentKey) { |
|
return guidFor(dependentKey); |
|
}); |
|
}; |
|
|
|
var args = a_slice.call(arguments); |
|
args.push({ |
|
initialize: function (array, changeMeta, instanceMeta) { |
|
instanceMeta.itemCounts = {}; |
|
}, |
|
|
|
addedItem: function(array, item, changeMeta, instanceMeta) { |
|
var itemGuid = guidFor(item), |
|
dependentGuids = getDependentKeyGuids(changeMeta), |
|
dependentGuid = guidFor(changeMeta.arrayChanged), |
|
numberOfDependentArrays = changeMeta.property._dependentKeys.length, |
|
itemCounts = instanceMeta.itemCounts; |
|
|
|
if (!itemCounts[itemGuid]) { itemCounts[itemGuid] = {}; } |
|
if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } |
|
|
|
if (++itemCounts[itemGuid][dependentGuid] === 1 && |
|
numberOfDependentArrays === Ember.keys(itemCounts[itemGuid]).length) { |
|
|
|
array.addObject(item); |
|
} |
|
return array; |
|
}, |
|
removedItem: function(array, item, changeMeta, instanceMeta) { |
|
var itemGuid = guidFor(item), |
|
dependentGuids = getDependentKeyGuids(changeMeta), |
|
dependentGuid = guidFor(changeMeta.arrayChanged), |
|
numberOfDependentArrays = changeMeta.property._dependentKeys.length, |
|
numberOfArraysItemAppearsIn, |
|
itemCounts = instanceMeta.itemCounts; |
|
|
|
if (itemCounts[itemGuid][dependentGuid] === undefined) { itemCounts[itemGuid][dependentGuid] = 0; } |
|
if (--itemCounts[itemGuid][dependentGuid] === 0) { |
|
delete itemCounts[itemGuid][dependentGuid]; |
|
numberOfArraysItemAppearsIn = Ember.keys(itemCounts[itemGuid]).length; |
|
|
|
if (numberOfArraysItemAppearsIn === 0) { |
|
delete itemCounts[itemGuid]; |
|
} |
|
array.removeObject(item); |
|
} |
|
return array; |
|
} |
|
}); |
|
return Ember.arrayComputed.apply(null, args); |
|
}; |
|
|
|
/** |
|
A computed property which returns a new array with all the |
|
properties from the first dependent array that are not in the second |
|
dependent array. |
|
|
|
Example |
|
|
|
```javascript |
|
App.Hamster = Ember.Object.extend({ |
|
likes: ['banana', 'grape', 'kale'], |
|
wants: Ember.computed.setDiff('likes', 'fruits') |
|
}); |
|
|
|
var hamster = App.Hamster.create({fruits: [ |
|
'grape', |
|
'kale', |
|
]}); |
|
hamster.get('wants'); // ['banana'] |
|
``` |
|
|
|
@method computed.setDiff |
|
@for Ember |
|
@param {String} setAProperty |
|
@param {String} setBProperty |
|
@return {Ember.ComputedProperty} computes a new array with all the |
|
items from the first dependent array that are not in the second |
|
dependent array |
|
*/ |
|
Ember.computed.setDiff = function (setAProperty, setBProperty) { |
|
if (arguments.length !== 2) { |
|
throw new Ember.Error("setDiff requires exactly two dependent arrays."); |
|
} |
|
return Ember.arrayComputed(setAProperty, setBProperty, { |
|
addedItem: function (array, item, changeMeta, instanceMeta) { |
|
var setA = get(this, setAProperty), |
|
setB = get(this, setBProperty); |
|
|
|
if (changeMeta.arrayChanged === setA) { |
|
if (!setB.contains(item)) { |
|
array.addObject(item); |
|
} |
|
} else { |
|
array.removeObject(item); |
|
} |
|
return array; |
|
}, |
|
|
|
removedItem: function (array, item, changeMeta, instanceMeta) { |
|
var setA = get(this, setAProperty), |
|
setB = get(this, setBProperty); |
|
|
|
if (changeMeta.arrayChanged === setB) { |
|
if (setA.contains(item)) { |
|
array.addObject(item); |
|
} |
|
} else { |
|
array.removeObject(item); |
|
} |
|
return array; |
|
} |
|
}); |
|
}; |
|
|
|
function binarySearch(array, item, low, high) { |
|
var mid, midItem, res, guidMid, guidItem; |
|
|
|
if (arguments.length < 4) { high = get(array, 'length'); } |
|
if (arguments.length < 3) { low = 0; } |
|
|
|
if (low === high) { |
|
return low; |
|
} |
|
|
|
mid = low + Math.floor((high - low) / 2); |
|
midItem = array.objectAt(mid); |
|
|
|
guidMid = _guidFor(midItem); |
|
guidItem = _guidFor(item); |
|
|
|
if (guidMid === guidItem) { |
|
return mid; |
|
} |
|
|
|
res = this.order(midItem, item); |
|
if (res === 0) { |
|
res = guidMid < guidItem ? -1 : 1; |
|
} |
|
|
|
|
|
if (res < 0) { |
|
return this.binarySearch(array, item, mid+1, high); |
|
} else if (res > 0) { |
|
return this.binarySearch(array, item, low, mid); |
|
} |
|
|
|
return mid; |
|
|
|
function _guidFor(item) { |
|
if (SearchProxy.detectInstance(item)) { |
|
return guidFor(get(item, 'content')); |
|
} |
|
return guidFor(item); |
|
} |
|
} |
|
|
|
|
|
SearchProxy = Ember.ObjectProxy.extend(); |
|
|
|
/** |
|
A computed property which returns a new array with all the |
|
properties from the first dependent array sorted based on a property |
|
or sort function. |
|
|
|
The callback method you provide should have the following signature: |
|
|
|
```javascript |
|
function(itemA, itemB); |
|
``` |
|
|
|
- `itemA` the first item to compare. |
|
- `itemB` the second item to compare. |
|
|
|
This function should return negative number (e.g. `-1`) when `itemA` should come before |
|
`itemB`. It should return positive number (e.g. `1`) when `itemA` should come after |
|
`itemB`. If the `itemA` and `itemB` are equal this function should return `0`. |
|
|
|
Therefore, if this function is comparing some numeric values, simple `itemA - itemB` or |
|
`itemA.get( 'foo' ) - itemB.get( 'foo' )` can be used instead of series of `if`. |
|
|
|
Example |
|
|
|
```javascript |
|
var ToDoList = Ember.Object.extend({ |
|
todosSorting: ['name'], |
|
sortedTodos: Ember.computed.sort('todos', 'todosSorting'), |
|
priorityTodos: Ember.computed.sort('todos', function(a, b){ |
|
if (a.priority > b.priority) { |
|
return 1; |
|
} else if (a.priority < b.priority) { |
|
return -1; |
|
} |
|
return 0; |
|
}), |
|
}); |
|
var todoList = ToDoList.create({todos: [ |
|
{name: 'Unit Test', priority: 2}, |
|
{name: 'Documentation', priority: 3}, |
|
{name: 'Release', priority: 1} |
|
]}); |
|
|
|
todoList.get('sortedTodos'); // [{name:'Documentation', priority:3}, {name:'Release', priority:1}, {name:'Unit Test', priority:2}] |
|
todoList.get('priorityTodos'); // [{name:'Release', priority:1}, {name:'Unit Test', priority:2}, {name:'Documentation', priority:3}] |
|
``` |
|
|
|
@method computed.sort |
|
@for Ember |
|
@param {String} dependentKey |
|
@param {String or Function} sortDefinition a dependent key to an |
|
array of sort properties or a function to use when sorting |
|
@return {Ember.ComputedProperty} computes a new sorted array based |
|
on the sort property array or callback function |
|
*/ |
|
Ember.computed.sort = function (itemsKey, sortDefinition) { |
|
Ember.assert("Ember.computed.sort requires two arguments: an array key to sort and either a sort properties key or sort function", arguments.length === 2); |
|
|
|
var initFn, sortPropertiesKey; |
|
|
|
if (typeof sortDefinition === 'function') { |
|
initFn = function (array, changeMeta, instanceMeta) { |
|
instanceMeta.order = sortDefinition; |
|
instanceMeta.binarySearch = binarySearch; |
|
}; |
|
} else { |
|
sortPropertiesKey = sortDefinition; |
|
initFn = function (array, changeMeta, instanceMeta) { |
|
function setupSortProperties() { |
|
var sortPropertyDefinitions = get(this, sortPropertiesKey), |
|
sortProperty, |
|
sortProperties = instanceMeta.sortProperties = [], |
|
sortPropertyAscending = instanceMeta.sortPropertyAscending = {}, |
|
idx, |
|
asc; |
|
|
|
Ember.assert("Cannot sort: '" + sortPropertiesKey + "' is not an array.", Ember.isArray(sortPropertyDefinitions)); |
|
|
|
changeMeta.property.clearItemPropertyKeys(itemsKey); |
|
|
|
forEach(sortPropertyDefinitions, function (sortPropertyDefinition) { |
|
if ((idx = sortPropertyDefinition.indexOf(':')) !== -1) { |
|
sortProperty = sortPropertyDefinition.substring(0, idx); |
|
asc = sortPropertyDefinition.substring(idx+1).toLowerCase() !== 'desc'; |
|
} else { |
|
sortProperty = sortPropertyDefinition; |
|
asc = true; |
|
} |
|
|
|
sortProperties.push(sortProperty); |
|
sortPropertyAscending[sortProperty] = asc; |
|
changeMeta.property.itemPropertyKey(itemsKey, sortProperty); |
|
}); |
|
|
|
sortPropertyDefinitions.addObserver('@each', this, updateSortPropertiesOnce); |
|
} |
|
|
|
function updateSortPropertiesOnce() { |
|
Ember.run.once(this, updateSortProperties, changeMeta.propertyName); |
|
} |
|
|
|
function updateSortProperties(propertyName) { |
|
setupSortProperties.call(this); |
|
changeMeta.property.recomputeOnce.call(this, propertyName); |
|
} |
|
|
|
Ember.addObserver(this, sortPropertiesKey, updateSortPropertiesOnce); |
|
|
|
setupSortProperties.call(this); |
|
|
|
|
|
instanceMeta.order = function (itemA, itemB) { |
|
var isProxy = itemB instanceof SearchProxy, |
|
sortProperty, result, asc; |
|
|
|
for (var i = 0; i < this.sortProperties.length; ++i) { |
|
sortProperty = this.sortProperties[i]; |
|
result = Ember.compare(get(itemA, sortProperty), isProxy ? itemB[sortProperty] : get(itemB, sortProperty)); |
|
|
|
if (result !== 0) { |
|
asc = this.sortPropertyAscending[sortProperty]; |
|
return asc ? result : (-1 * result); |
|
} |
|
} |
|
|
|
return 0; |
|
}; |
|
|
|
instanceMeta.binarySearch = binarySearch; |
|
}; |
|
} |
|
|
|
return Ember.arrayComputed(itemsKey, { |
|
initialize: initFn, |
|
|
|
addedItem: function (array, item, changeMeta, instanceMeta) { |
|
var index = instanceMeta.binarySearch(array, item); |
|
array.insertAt(index, item); |
|
return array; |
|
}, |
|
|
|
removedItem: function (array, item, changeMeta, instanceMeta) { |
|
var proxyProperties, index, searchItem; |
|
|
|
if (changeMeta.previousValues) { |
|
proxyProperties = merge({ content: item }, changeMeta.previousValues); |
|
|
|
searchItem = SearchProxy.create(proxyProperties); |
|
} else { |
|
searchItem = item; |
|
} |
|
|
|
index = instanceMeta.binarySearch(array, searchItem); |
|
array.removeAt(index); |
|
return array; |
|
} |
|
}); |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
Ember.RSVP = requireModule('rsvp'); |
|
|
|
Ember.RSVP.onerrorDefault = function(error) { |
|
if (error instanceof Error) { |
|
if (Ember.testing) { |
|
if (Ember.Test && Ember.Test.adapter) { |
|
Ember.Test.adapter.exception(error); |
|
} else { |
|
throw error; |
|
} |
|
} else { |
|
Ember.Logger.error(error.stack); |
|
Ember.assert(error, false); |
|
} |
|
} |
|
}; |
|
|
|
Ember.RSVP.on('error', Ember.RSVP.onerrorDefault); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var a_slice = Array.prototype.slice; |
|
|
|
var expandProperties = Ember.expandProperties; |
|
|
|
if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Function) { |
|
|
|
/** |
|
The `property` extension of Javascript's Function prototype is available |
|
when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is |
|
`true`, which is the default. |
|
|
|
Computed properties allow you to treat a function like a property: |
|
|
|
```javascript |
|
MyApp.President = Ember.Object.extend({ |
|
firstName: '', |
|
lastName: '', |
|
|
|
fullName: function() { |
|
return this.get('firstName') + ' ' + this.get('lastName'); |
|
|
|
// Call this flag to mark the function as a property |
|
}.property() |
|
}); |
|
|
|
var president = MyApp.President.create({ |
|
firstName: "Barack", |
|
lastName: "Obama" |
|
}); |
|
|
|
president.get('fullName'); // "Barack Obama" |
|
``` |
|
|
|
Treating a function like a property is useful because they can work with |
|
bindings, just like any other property. |
|
|
|
Many computed properties have dependencies on other properties. For |
|
example, in the above example, the `fullName` property depends on |
|
`firstName` and `lastName` to determine its value. You can tell Ember |
|
about these dependencies like this: |
|
|
|
```javascript |
|
MyApp.President = Ember.Object.extend({ |
|
firstName: '', |
|
lastName: '', |
|
|
|
fullName: function() { |
|
return this.get('firstName') + ' ' + this.get('lastName'); |
|
|
|
// Tell Ember.js that this computed property depends on firstName |
|
// and lastName |
|
}.property('firstName', 'lastName') |
|
}); |
|
``` |
|
|
|
Make sure you list these dependencies so Ember knows when to update |
|
bindings that connect to a computed property. Changing a dependency |
|
will not immediately trigger an update of the computed property, but |
|
will instead clear the cache so that it is updated when the next `get` |
|
is called on the property. |
|
|
|
See [Ember.ComputedProperty](/api/classes/Ember.ComputedProperty.html), [Ember.computed](/api/#method_computed). |
|
|
|
@method property |
|
@for Function |
|
*/ |
|
Function.prototype.property = function() { |
|
var ret = Ember.computed(this); |
|
// ComputedProperty.prototype.property expands properties; no need for us to |
|
// do so here. |
|
return ret.property.apply(ret, arguments); |
|
}; |
|
|
|
/** |
|
The `observes` extension of Javascript's Function prototype is available |
|
when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is |
|
true, which is the default. |
|
|
|
You can observe property changes simply by adding the `observes` |
|
call to the end of your method declarations in classes that you write. |
|
For example: |
|
|
|
```javascript |
|
Ember.Object.extend({ |
|
valueObserver: function() { |
|
// Executes whenever the "value" property changes |
|
}.observes('value') |
|
}); |
|
``` |
|
|
|
In the future this method may become asynchronous. If you want to ensure |
|
synchronous behavior, use `observesImmediately`. |
|
|
|
See `Ember.observer`. |
|
|
|
@method observes |
|
@for Function |
|
*/ |
|
Function.prototype.observes = function() { |
|
var addWatchedProperty = function (obs) { watched.push(obs); }; |
|
var watched = []; |
|
|
|
for (var i=0; i<arguments.length; ++i) { |
|
expandProperties(arguments[i], addWatchedProperty); |
|
} |
|
|
|
this.__ember_observes__ = watched; |
|
|
|
return this; |
|
}; |
|
|
|
/** |
|
The `observesImmediately` extension of Javascript's Function prototype is |
|
available when `Ember.EXTEND_PROTOTYPES` or |
|
`Ember.EXTEND_PROTOTYPES.Function` is true, which is the default. |
|
|
|
You can observe property changes simply by adding the `observesImmediately` |
|
call to the end of your method declarations in classes that you write. |
|
For example: |
|
|
|
```javascript |
|
Ember.Object.extend({ |
|
valueObserver: function() { |
|
// Executes immediately after the "value" property changes |
|
}.observesImmediately('value') |
|
}); |
|
``` |
|
|
|
In the future, `observes` may become asynchronous. In this event, |
|
`observesImmediately` will maintain the synchronous behavior. |
|
|
|
See `Ember.immediateObserver`. |
|
|
|
@method observesImmediately |
|
@for Function |
|
*/ |
|
Function.prototype.observesImmediately = function() { |
|
for (var i=0, l=arguments.length; i<l; i++) { |
|
var arg = arguments[i]; |
|
Ember.assert("Immediate observers must observe internal properties only, not properties on other objects.", arg.indexOf('.') === -1); |
|
} |
|
|
|
// observes handles property expansion |
|
return this.observes.apply(this, arguments); |
|
}; |
|
|
|
/** |
|
The `observesBefore` extension of Javascript's Function prototype is |
|
available when `Ember.EXTEND_PROTOTYPES` or |
|
`Ember.EXTEND_PROTOTYPES.Function` is true, which is the default. |
|
|
|
You can get notified when a property change is about to happen by |
|
by adding the `observesBefore` call to the end of your method |
|
declarations in classes that you write. For example: |
|
|
|
```javascript |
|
Ember.Object.extend({ |
|
valueObserver: function() { |
|
// Executes whenever the "value" property is about to change |
|
}.observesBefore('value') |
|
}); |
|
``` |
|
|
|
See `Ember.beforeObserver`. |
|
|
|
@method observesBefore |
|
@for Function |
|
*/ |
|
Function.prototype.observesBefore = function() { |
|
var addWatchedProperty = function (obs) { watched.push(obs); }; |
|
var watched = []; |
|
|
|
for (var i=0; i<arguments.length; ++i) { |
|
expandProperties(arguments[i], addWatchedProperty); |
|
} |
|
|
|
this.__ember_observesBefore__ = watched; |
|
|
|
return this; |
|
}; |
|
|
|
/** |
|
The `on` extension of Javascript's Function prototype is available |
|
when `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Function` is |
|
true, which is the default. |
|
|
|
You can listen for events simply by adding the `on` call to the end of |
|
your method declarations in classes or mixins that you write. For example: |
|
|
|
```javascript |
|
Ember.Mixin.create({ |
|
doSomethingWithElement: function() { |
|
// Executes whenever the "didInsertElement" event fires |
|
}.on('didInsertElement') |
|
}); |
|
``` |
|
|
|
See `Ember.on`. |
|
|
|
@method on |
|
@for Function |
|
*/ |
|
Function.prototype.on = function() { |
|
var events = a_slice.call(arguments); |
|
this.__ember_listens__ = events; |
|
return this; |
|
}; |
|
} |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
|
|
/** |
|
Implements some standard methods for comparing objects. Add this mixin to |
|
any class you create that can compare its instances. |
|
|
|
You should implement the `compare()` method. |
|
|
|
@class Comparable |
|
@namespace Ember |
|
@since Ember 0.9 |
|
*/ |
|
Ember.Comparable = Ember.Mixin.create({ |
|
|
|
/** |
|
Override to return the result of the comparison of the two parameters. The |
|
compare method should return: |
|
|
|
- `-1` if `a < b` |
|
- `0` if `a == b` |
|
- `1` if `a > b` |
|
|
|
Default implementation raises an exception. |
|
|
|
@method compare |
|
@param a {Object} the first object to compare |
|
@param b {Object} the second object to compare |
|
@return {Integer} the result of the comparison |
|
*/ |
|
compare: Ember.required(Function) |
|
|
|
}); |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
|
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
/** |
|
Implements some standard methods for copying an object. Add this mixin to |
|
any object you create that can create a copy of itself. This mixin is |
|
added automatically to the built-in array. |
|
|
|
You should generally implement the `copy()` method to return a copy of the |
|
receiver. |
|
|
|
Note that `frozenCopy()` will only work if you also implement |
|
`Ember.Freezable`. |
|
|
|
@class Copyable |
|
@namespace Ember |
|
@since Ember 0.9 |
|
*/ |
|
Ember.Copyable = Ember.Mixin.create({ |
|
|
|
/** |
|
Override to return a copy of the receiver. Default implementation raises |
|
an exception. |
|
|
|
@method copy |
|
@param {Boolean} deep if `true`, a deep copy of the object should be made |
|
@return {Object} copy of receiver |
|
*/ |
|
copy: Ember.required(Function), |
|
|
|
/** |
|
If the object implements `Ember.Freezable`, then this will return a new |
|
copy if the object is not frozen and the receiver if the object is frozen. |
|
|
|
Raises an exception if you try to call this method on a object that does |
|
not support freezing. |
|
|
|
You should use this method whenever you want a copy of a freezable object |
|
since a freezable object can simply return itself without actually |
|
consuming more memory. |
|
|
|
@method frozenCopy |
|
@return {Object} copy of receiver or receiver |
|
*/ |
|
frozenCopy: function() { |
|
if (Ember.Freezable && Ember.Freezable.detect(this)) { |
|
return get(this, 'isFrozen') ? this : this.copy().freeze(); |
|
} else { |
|
throw new Ember.Error(Ember.String.fmt("%@ does not support freezing", [this])); |
|
} |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
/** |
|
The `Ember.Freezable` mixin implements some basic methods for marking an |
|
object as frozen. Once an object is frozen it should be read only. No changes |
|
may be made the internal state of the object. |
|
|
|
## Enforcement |
|
|
|
To fully support freezing in your subclass, you must include this mixin and |
|
override any method that might alter any property on the object to instead |
|
raise an exception. You can check the state of an object by checking the |
|
`isFrozen` property. |
|
|
|
Although future versions of JavaScript may support language-level freezing |
|
object objects, that is not the case today. Even if an object is freezable, |
|
it is still technically possible to modify the object, even though it could |
|
break other parts of your application that do not expect a frozen object to |
|
change. It is, therefore, very important that you always respect the |
|
`isFrozen` property on all freezable objects. |
|
|
|
## Example Usage |
|
|
|
The example below shows a simple object that implement the `Ember.Freezable` |
|
protocol. |
|
|
|
```javascript |
|
Contact = Ember.Object.extend(Ember.Freezable, { |
|
firstName: null, |
|
lastName: null, |
|
|
|
// swaps the names |
|
swapNames: function() { |
|
if (this.get('isFrozen')) throw Ember.FROZEN_ERROR; |
|
var tmp = this.get('firstName'); |
|
this.set('firstName', this.get('lastName')); |
|
this.set('lastName', tmp); |
|
return this; |
|
} |
|
|
|
}); |
|
|
|
c = Contact.create({ firstName: "John", lastName: "Doe" }); |
|
c.swapNames(); // returns c |
|
c.freeze(); |
|
c.swapNames(); // EXCEPTION |
|
``` |
|
|
|
## Copying |
|
|
|
Usually the `Ember.Freezable` protocol is implemented in cooperation with the |
|
`Ember.Copyable` protocol, which defines a `frozenCopy()` method that will |
|
return a frozen object, if the object implements this method as well. |
|
|
|
@class Freezable |
|
@namespace Ember |
|
@since Ember 0.9 |
|
*/ |
|
Ember.Freezable = Ember.Mixin.create({ |
|
|
|
/** |
|
Set to `true` when the object is frozen. Use this property to detect |
|
whether your object is frozen or not. |
|
|
|
@property isFrozen |
|
@type Boolean |
|
*/ |
|
isFrozen: false, |
|
|
|
/** |
|
Freezes the object. Once this method has been called the object should |
|
no longer allow any properties to be edited. |
|
|
|
@method freeze |
|
@return {Object} receiver |
|
*/ |
|
freeze: function() { |
|
if (get(this, 'isFrozen')) return this; |
|
set(this, 'isFrozen', true); |
|
return this; |
|
} |
|
|
|
}); |
|
|
|
Ember.FROZEN_ERROR = "Frozen object cannot be modified."; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var forEach = Ember.EnumerableUtils.forEach; |
|
|
|
/** |
|
This mixin defines the API for modifying generic enumerables. These methods |
|
can be applied to an object regardless of whether it is ordered or |
|
unordered. |
|
|
|
Note that an Enumerable can change even if it does not implement this mixin. |
|
For example, a MappedEnumerable cannot be directly modified but if its |
|
underlying enumerable changes, it will change also. |
|
|
|
## Adding Objects |
|
|
|
To add an object to an enumerable, use the `addObject()` method. This |
|
method will only add the object to the enumerable if the object is not |
|
already present and is of a type supported by the enumerable. |
|
|
|
```javascript |
|
set.addObject(contact); |
|
``` |
|
|
|
## Removing Objects |
|
|
|
To remove an object from an enumerable, use the `removeObject()` method. This |
|
will only remove the object if it is present in the enumerable, otherwise |
|
this method has no effect. |
|
|
|
```javascript |
|
set.removeObject(contact); |
|
``` |
|
|
|
## Implementing In Your Own Code |
|
|
|
If you are implementing an object and want to support this API, just include |
|
this mixin in your class and implement the required methods. In your unit |
|
tests, be sure to apply the Ember.MutableEnumerableTests to your object. |
|
|
|
@class MutableEnumerable |
|
@namespace Ember |
|
@uses Ember.Enumerable |
|
*/ |
|
Ember.MutableEnumerable = Ember.Mixin.create(Ember.Enumerable, { |
|
|
|
/** |
|
__Required.__ You must implement this method to apply this mixin. |
|
|
|
Attempts to add the passed object to the receiver if the object is not |
|
already present in the collection. If the object is present, this method |
|
has no effect. |
|
|
|
If the passed object is of a type not supported by the receiver, |
|
then this method should raise an exception. |
|
|
|
@method addObject |
|
@param {Object} object The object to add to the enumerable. |
|
@return {Object} the passed object |
|
*/ |
|
addObject: Ember.required(Function), |
|
|
|
/** |
|
Adds each object in the passed enumerable to the receiver. |
|
|
|
@method addObjects |
|
@param {Ember.Enumerable} objects the objects to add. |
|
@return {Object} receiver |
|
*/ |
|
addObjects: function(objects) { |
|
Ember.beginPropertyChanges(this); |
|
forEach(objects, function(obj) { this.addObject(obj); }, this); |
|
Ember.endPropertyChanges(this); |
|
return this; |
|
}, |
|
|
|
/** |
|
__Required.__ You must implement this method to apply this mixin. |
|
|
|
Attempts to remove the passed object from the receiver collection if the |
|
object is present in the collection. If the object is not present, |
|
this method has no effect. |
|
|
|
If the passed object is of a type not supported by the receiver, |
|
then this method should raise an exception. |
|
|
|
@method removeObject |
|
@param {Object} object The object to remove from the enumerable. |
|
@return {Object} the passed object |
|
*/ |
|
removeObject: Ember.required(Function), |
|
|
|
|
|
/** |
|
Removes each object in the passed enumerable from the receiver. |
|
|
|
@method removeObjects |
|
@param {Ember.Enumerable} objects the objects to remove |
|
@return {Object} receiver |
|
*/ |
|
removeObjects: function(objects) { |
|
Ember.beginPropertyChanges(this); |
|
forEach(objects, function(obj) { this.removeObject(obj); }, this); |
|
Ember.endPropertyChanges(this); |
|
return this; |
|
} |
|
|
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
// .......................................................... |
|
// CONSTANTS |
|
// |
|
|
|
var OUT_OF_RANGE_EXCEPTION = "Index out of range" ; |
|
var EMPTY = []; |
|
|
|
// .......................................................... |
|
// HELPERS |
|
// |
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
/** |
|
This mixin defines the API for modifying array-like objects. These methods |
|
can be applied only to a collection that keeps its items in an ordered set. |
|
It builds upon the Array mixin and adds methods to modify the array. |
|
Concrete implementations of this class include ArrayProxy and ArrayController. |
|
|
|
It is important to use the methods in this class to modify arrays so that |
|
changes are observable. This allows the binding system in Ember to function |
|
correctly. |
|
|
|
Note that an Array can change even if it does not implement this mixin. |
|
For example, one might implement a SparseArray that cannot be directly |
|
modified, but if its underlying enumerable changes, it will change also. |
|
|
|
@class MutableArray |
|
@namespace Ember |
|
@uses Ember.Array |
|
@uses Ember.MutableEnumerable |
|
*/ |
|
Ember.MutableArray = Ember.Mixin.create(Ember.Array, Ember.MutableEnumerable, { |
|
|
|
/** |
|
__Required.__ You must implement this method to apply this mixin. |
|
|
|
This is one of the primitives you must implement to support `Ember.Array`. |
|
You should replace amt objects started at idx with the objects in the |
|
passed array. You should also call `this.enumerableContentDidChange()` |
|
|
|
@method replace |
|
@param {Number} idx Starting index in the array to replace. If |
|
idx >= length, then append to the end of the array. |
|
@param {Number} amt Number of elements that should be removed from |
|
the array, starting at *idx*. |
|
@param {Array} objects An array of zero or more objects that should be |
|
inserted into the array at *idx* |
|
*/ |
|
replace: Ember.required(), |
|
|
|
/** |
|
Remove all elements from self. This is useful if you |
|
want to reuse an existing array without having to recreate it. |
|
|
|
```javascript |
|
var colors = ["red", "green", "blue"]; |
|
color.length(); // 3 |
|
colors.clear(); // [] |
|
colors.length(); // 0 |
|
``` |
|
|
|
@method clear |
|
@return {Ember.Array} An empty Array. |
|
*/ |
|
clear: function () { |
|
var len = get(this, 'length'); |
|
if (len === 0) return this; |
|
this.replace(0, len, EMPTY); |
|
return this; |
|
}, |
|
|
|
/** |
|
This will use the primitive `replace()` method to insert an object at the |
|
specified index. |
|
|
|
```javascript |
|
var colors = ["red", "green", "blue"]; |
|
colors.insertAt(2, "yellow"); // ["red", "green", "yellow", "blue"] |
|
colors.insertAt(5, "orange"); // Error: Index out of range |
|
``` |
|
|
|
@method insertAt |
|
@param {Number} idx index of insert the object at. |
|
@param {Object} object object to insert |
|
@return this |
|
*/ |
|
insertAt: function(idx, object) { |
|
if (idx > get(this, 'length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION) ; |
|
this.replace(idx, 0, [object]) ; |
|
return this ; |
|
}, |
|
|
|
/** |
|
Remove an object at the specified index using the `replace()` primitive |
|
method. You can pass either a single index, or a start and a length. |
|
|
|
If you pass a start and length that is beyond the |
|
length this method will throw an `OUT_OF_RANGE_EXCEPTION`. |
|
|
|
```javascript |
|
var colors = ["red", "green", "blue", "yellow", "orange"]; |
|
colors.removeAt(0); // ["green", "blue", "yellow", "orange"] |
|
colors.removeAt(2, 2); // ["green", "blue"] |
|
colors.removeAt(4, 2); // Error: Index out of range |
|
``` |
|
|
|
@method removeAt |
|
@param {Number} start index, start of range |
|
@param {Number} len length of passing range |
|
@return {Object} receiver |
|
*/ |
|
removeAt: function(start, len) { |
|
if ('number' === typeof start) { |
|
|
|
if ((start < 0) || (start >= get(this, 'length'))) { |
|
throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); |
|
} |
|
|
|
// fast case |
|
if (len === undefined) len = 1; |
|
this.replace(start, len, EMPTY); |
|
} |
|
|
|
return this ; |
|
}, |
|
|
|
/** |
|
Push the object onto the end of the array. Works just like `push()` but it |
|
is KVO-compliant. |
|
|
|
```javascript |
|
var colors = ["red", "green"]; |
|
colors.pushObject("black"); // ["red", "green", "black"] |
|
colors.pushObject(["yellow"]); // ["red", "green", ["yellow"]] |
|
``` |
|
|
|
@method pushObject |
|
@param {*} obj object to push |
|
@return The same obj passed as param |
|
*/ |
|
pushObject: function(obj) { |
|
this.insertAt(get(this, 'length'), obj) ; |
|
return obj; |
|
}, |
|
|
|
/** |
|
Add the objects in the passed numerable to the end of the array. Defers |
|
notifying observers of the change until all objects are added. |
|
|
|
```javascript |
|
var colors = ["red"]; |
|
colors.pushObjects(["yellow", "orange"]); // ["red", "yellow", "orange"] |
|
``` |
|
|
|
@method pushObjects |
|
@param {Ember.Enumerable} objects the objects to add |
|
@return {Ember.Array} receiver |
|
*/ |
|
pushObjects: function(objects) { |
|
if (!(Ember.Enumerable.detect(objects) || Ember.isArray(objects))) { |
|
throw new TypeError("Must pass Ember.Enumerable to Ember.MutableArray#pushObjects"); |
|
} |
|
this.replace(get(this, 'length'), 0, objects); |
|
return this; |
|
}, |
|
|
|
/** |
|
Pop object from array or nil if none are left. Works just like `pop()` but |
|
it is KVO-compliant. |
|
|
|
```javascript |
|
var colors = ["red", "green", "blue"]; |
|
colors.popObject(); // "blue" |
|
console.log(colors); // ["red", "green"] |
|
``` |
|
|
|
@method popObject |
|
@return object |
|
*/ |
|
popObject: function() { |
|
var len = get(this, 'length') ; |
|
if (len === 0) return null ; |
|
|
|
var ret = this.objectAt(len-1) ; |
|
this.removeAt(len-1, 1) ; |
|
return ret ; |
|
}, |
|
|
|
/** |
|
Shift an object from start of array or nil if none are left. Works just |
|
like `shift()` but it is KVO-compliant. |
|
|
|
```javascript |
|
var colors = ["red", "green", "blue"]; |
|
colors.shiftObject(); // "red" |
|
console.log(colors); // ["green", "blue"] |
|
``` |
|
|
|
@method shiftObject |
|
@return object |
|
*/ |
|
shiftObject: function() { |
|
if (get(this, 'length') === 0) return null ; |
|
var ret = this.objectAt(0) ; |
|
this.removeAt(0) ; |
|
return ret ; |
|
}, |
|
|
|
/** |
|
Unshift an object to start of array. Works just like `unshift()` but it is |
|
KVO-compliant. |
|
|
|
```javascript |
|
var colors = ["red"]; |
|
colors.unshiftObject("yellow"); // ["yellow", "red"] |
|
colors.unshiftObject(["black"]); // [["black"], "yellow", "red"] |
|
``` |
|
|
|
@method unshiftObject |
|
@param {*} obj object to unshift |
|
@return The same obj passed as param |
|
*/ |
|
unshiftObject: function(obj) { |
|
this.insertAt(0, obj) ; |
|
return obj ; |
|
}, |
|
|
|
/** |
|
Adds the named objects to the beginning of the array. Defers notifying |
|
observers until all objects have been added. |
|
|
|
```javascript |
|
var colors = ["red"]; |
|
colors.unshiftObjects(["black", "white"]); // ["black", "white", "red"] |
|
colors.unshiftObjects("yellow"); // Type Error: 'undefined' is not a function |
|
``` |
|
|
|
@method unshiftObjects |
|
@param {Ember.Enumerable} objects the objects to add |
|
@return {Ember.Array} receiver |
|
*/ |
|
unshiftObjects: function(objects) { |
|
this.replace(0, 0, objects); |
|
return this; |
|
}, |
|
|
|
/** |
|
Reverse objects in the array. Works just like `reverse()` but it is |
|
KVO-compliant. |
|
|
|
@method reverseObjects |
|
@return {Ember.Array} receiver |
|
*/ |
|
reverseObjects: function() { |
|
var len = get(this, 'length'); |
|
if (len === 0) return this; |
|
var objects = this.toArray().reverse(); |
|
this.replace(0, len, objects); |
|
return this; |
|
}, |
|
|
|
/** |
|
Replace all the the receiver's content with content of the argument. |
|
If argument is an empty array receiver will be cleared. |
|
|
|
```javascript |
|
var colors = ["red", "green", "blue"]; |
|
colors.setObjects(["black", "white"]); // ["black", "white"] |
|
colors.setObjects([]); // [] |
|
``` |
|
|
|
@method setObjects |
|
@param {Ember.Array} objects array whose content will be used for replacing |
|
the content of the receiver |
|
@return {Ember.Array} receiver with the new content |
|
*/ |
|
setObjects: function(objects) { |
|
if (objects.length === 0) return this.clear(); |
|
|
|
var len = get(this, 'length'); |
|
this.replace(0, len, objects); |
|
return this; |
|
}, |
|
|
|
// .......................................................... |
|
// IMPLEMENT Ember.MutableEnumerable |
|
// |
|
|
|
removeObject: function(obj) { |
|
var loc = get(this, 'length') || 0; |
|
while(--loc >= 0) { |
|
var curObject = this.objectAt(loc) ; |
|
if (curObject === obj) this.removeAt(loc) ; |
|
} |
|
return this ; |
|
}, |
|
|
|
addObject: function(obj) { |
|
if (!this.contains(obj)) this.pushObject(obj); |
|
return this ; |
|
} |
|
|
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
/** |
|
`Ember.TargetActionSupport` is a mixin that can be included in a class |
|
to add a `triggerAction` method with semantics similar to the Handlebars |
|
`{{action}}` helper. In normal Ember usage, the `{{action}}` helper is |
|
usually the best choice. This mixin is most often useful when you are |
|
doing more complex event handling in View objects. |
|
|
|
See also `Ember.ViewTargetActionSupport`, which has |
|
view-aware defaults for target and actionContext. |
|
|
|
@class TargetActionSupport |
|
@namespace Ember |
|
@extends Ember.Mixin |
|
*/ |
|
Ember.TargetActionSupport = Ember.Mixin.create({ |
|
target: null, |
|
action: null, |
|
actionContext: null, |
|
|
|
targetObject: Ember.computed(function() { |
|
var target = get(this, 'target'); |
|
|
|
if (Ember.typeOf(target) === "string") { |
|
var value = get(this, target); |
|
if (value === undefined) { value = get(Ember.lookup, target); } |
|
return value; |
|
} else { |
|
return target; |
|
} |
|
}).property('target'), |
|
|
|
actionContextObject: Ember.computed(function() { |
|
var actionContext = get(this, 'actionContext'); |
|
|
|
if (Ember.typeOf(actionContext) === "string") { |
|
var value = get(this, actionContext); |
|
if (value === undefined) { value = get(Ember.lookup, actionContext); } |
|
return value; |
|
} else { |
|
return actionContext; |
|
} |
|
}).property('actionContext'), |
|
|
|
/** |
|
Send an `action` with an `actionContext` to a `target`. The action, actionContext |
|
and target will be retrieved from properties of the object. For example: |
|
|
|
```javascript |
|
App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { |
|
target: Ember.computed.alias('controller'), |
|
action: 'save', |
|
actionContext: Ember.computed.alias('context'), |
|
click: function() { |
|
this.triggerAction(); // Sends the `save` action, along with the current context |
|
// to the current controller |
|
} |
|
}); |
|
``` |
|
|
|
The `target`, `action`, and `actionContext` can be provided as properties of |
|
an optional object argument to `triggerAction` as well. |
|
|
|
```javascript |
|
App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { |
|
click: function() { |
|
this.triggerAction({ |
|
action: 'save', |
|
target: this.get('controller'), |
|
actionContext: this.get('context'), |
|
}); // Sends the `save` action, along with the current context |
|
// to the current controller |
|
} |
|
}); |
|
``` |
|
|
|
The `actionContext` defaults to the object you mixing `TargetActionSupport` into. |
|
But `target` and `action` must be specified either as properties or with the argument |
|
to `triggerAction`, or a combination: |
|
|
|
```javascript |
|
App.SaveButtonView = Ember.View.extend(Ember.TargetActionSupport, { |
|
target: Ember.computed.alias('controller'), |
|
click: function() { |
|
this.triggerAction({ |
|
action: 'save' |
|
}); // Sends the `save` action, along with a reference to `this`, |
|
// to the current controller |
|
} |
|
}); |
|
``` |
|
|
|
@method triggerAction |
|
@param opts {Hash} (optional, with the optional keys action, target and/or actionContext) |
|
@return {Boolean} true if the action was sent successfully and did not return false |
|
*/ |
|
triggerAction: function(opts) { |
|
opts = opts || {}; |
|
var action = opts.action || get(this, 'action'), |
|
target = opts.target || get(this, 'targetObject'), |
|
actionContext = opts.actionContext; |
|
|
|
function args(options, actionName) { |
|
var ret = []; |
|
if (actionName) { ret.push(actionName); } |
|
|
|
return ret.concat(options); |
|
} |
|
|
|
if (typeof actionContext === 'undefined') { |
|
actionContext = get(this, 'actionContextObject') || this; |
|
} |
|
|
|
if (target && action) { |
|
var ret; |
|
|
|
if (target.send) { |
|
ret = target.send.apply(target, args(actionContext, action)); |
|
} else { |
|
Ember.assert("The action '" + action + "' did not exist on " + target, typeof target[action] === 'function'); |
|
ret = target[action].apply(target, args(actionContext)); |
|
} |
|
|
|
if (ret !== false) ret = true; |
|
|
|
return ret; |
|
} else { |
|
return false; |
|
} |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
/** |
|
This mixin allows for Ember objects to subscribe to and emit events. |
|
|
|
```javascript |
|
App.Person = Ember.Object.extend(Ember.Evented, { |
|
greet: function() { |
|
// ... |
|
this.trigger('greet'); |
|
} |
|
}); |
|
|
|
var person = App.Person.create(); |
|
|
|
person.on('greet', function() { |
|
console.log('Our person has greeted'); |
|
}); |
|
|
|
person.greet(); |
|
|
|
// outputs: 'Our person has greeted' |
|
``` |
|
|
|
You can also chain multiple event subscriptions: |
|
|
|
```javascript |
|
person.on('greet', function() { |
|
console.log('Our person has greeted'); |
|
}).one('greet', function() { |
|
console.log('Offer one-time special'); |
|
}).off('event', this, forgetThis); |
|
``` |
|
|
|
@class Evented |
|
@namespace Ember |
|
*/ |
|
Ember.Evented = Ember.Mixin.create({ |
|
|
|
/** |
|
Subscribes to a named event with given function. |
|
|
|
```javascript |
|
person.on('didLoad', function() { |
|
// fired once the person has loaded |
|
}); |
|
``` |
|
|
|
An optional target can be passed in as the 2nd argument that will |
|
be set as the "this" for the callback. This is a good way to give your |
|
function access to the object triggering the event. When the target |
|
parameter is used the callback becomes the third argument. |
|
|
|
@method on |
|
@param {String} name The name of the event |
|
@param {Object} [target] The "this" binding for the callback |
|
@param {Function} method The callback to execute |
|
@return this |
|
*/ |
|
on: function(name, target, method) { |
|
Ember.addListener(this, name, target, method); |
|
return this; |
|
}, |
|
|
|
/** |
|
Subscribes a function to a named event and then cancels the subscription |
|
after the first time the event is triggered. It is good to use ``one`` when |
|
you only care about the first time an event has taken place. |
|
|
|
This function takes an optional 2nd argument that will become the "this" |
|
value for the callback. If this argument is passed then the 3rd argument |
|
becomes the function. |
|
|
|
@method one |
|
@param {String} name The name of the event |
|
@param {Object} [target] The "this" binding for the callback |
|
@param {Function} method The callback to execute |
|
@return this |
|
*/ |
|
one: function(name, target, method) { |
|
if (!method) { |
|
method = target; |
|
target = null; |
|
} |
|
|
|
Ember.addListener(this, name, target, method, true); |
|
return this; |
|
}, |
|
|
|
/** |
|
Triggers a named event for the object. Any additional arguments |
|
will be passed as parameters to the functions that are subscribed to the |
|
event. |
|
|
|
```javascript |
|
person.on('didEat', function(food) { |
|
console.log('person ate some ' + food); |
|
}); |
|
|
|
person.trigger('didEat', 'broccoli'); |
|
|
|
// outputs: person ate some broccoli |
|
``` |
|
@method trigger |
|
@param {String} name The name of the event |
|
@param {Object...} args Optional arguments to pass on |
|
*/ |
|
trigger: function(name) { |
|
var args = [], i, l; |
|
for (i = 1, l = arguments.length; i < l; i++) { |
|
args.push(arguments[i]); |
|
} |
|
Ember.sendEvent(this, name, args); |
|
}, |
|
|
|
/** |
|
Cancels subscription for given name, target, and method. |
|
|
|
@method off |
|
@param {String} name The name of the event |
|
@param {Object} target The target of the subscription |
|
@param {Function} method The function of the subscription |
|
@return this |
|
*/ |
|
off: function(name, target, method) { |
|
Ember.removeListener(this, name, target, method); |
|
return this; |
|
}, |
|
|
|
/** |
|
Checks to see if object has any subscriptions for named event. |
|
|
|
@method has |
|
@param {String} name The name of the event |
|
@return {Boolean} does the object have a subscription for event |
|
*/ |
|
has: function(name) { |
|
return Ember.hasListeners(this, name); |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var RSVP = requireModule("rsvp"); |
|
|
|
if (Ember.FEATURES['ember-runtime-test-friendly-promises']) { |
|
|
|
var asyncStart = function() { |
|
if (Ember.Test && Ember.Test.adapter) { |
|
Ember.Test.adapter.asyncStart(); |
|
} |
|
}; |
|
|
|
var asyncEnd = function() { |
|
if (Ember.Test && Ember.Test.adapter) { |
|
Ember.Test.adapter.asyncEnd(); |
|
} |
|
}; |
|
|
|
RSVP.configure('async', function(callback, promise) { |
|
var async = !Ember.run.currentRunLoop; |
|
|
|
if (Ember.testing && async) { asyncStart(); } |
|
|
|
Ember.run.backburner.schedule('actions', function(){ |
|
if (Ember.testing && async) { asyncEnd(); } |
|
callback(promise); |
|
}); |
|
}); |
|
} else { |
|
RSVP.configure('async', function(callback, promise) { |
|
Ember.run.backburner.schedule('actions', function(){ |
|
callback(promise); |
|
}); |
|
}); |
|
} |
|
|
|
RSVP.Promise.prototype.fail = function(callback, label){ |
|
Ember.deprecate('RSVP.Promise.fail has been renamed as RSVP.Promise.catch'); |
|
return this['catch'](callback, label); |
|
}; |
|
|
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var get = Ember.get; |
|
|
|
/** |
|
@class Deferred |
|
@namespace Ember |
|
*/ |
|
Ember.DeferredMixin = Ember.Mixin.create({ |
|
/** |
|
Add handlers to be called when the Deferred object is resolved or rejected. |
|
|
|
@method then |
|
@param {Function} resolve a callback function to be called when done |
|
@param {Function} reject a callback function to be called when failed |
|
*/ |
|
then: function(resolve, reject, label) { |
|
var deferred, promise, entity; |
|
|
|
entity = this; |
|
deferred = get(this, '_deferred'); |
|
promise = deferred.promise; |
|
|
|
function fulfillmentHandler(fulfillment) { |
|
if (fulfillment === promise) { |
|
return resolve(entity); |
|
} else { |
|
return resolve(fulfillment); |
|
} |
|
} |
|
|
|
return promise.then(resolve && fulfillmentHandler, reject, label); |
|
}, |
|
|
|
/** |
|
Resolve a Deferred object and call any `doneCallbacks` with the given args. |
|
|
|
@method resolve |
|
*/ |
|
resolve: function(value) { |
|
var deferred, promise; |
|
|
|
deferred = get(this, '_deferred'); |
|
promise = deferred.promise; |
|
|
|
if (value === this) { |
|
deferred.resolve(promise); |
|
} else { |
|
deferred.resolve(value); |
|
} |
|
}, |
|
|
|
/** |
|
Reject a Deferred object and call any `failCallbacks` with the given args. |
|
|
|
@method reject |
|
*/ |
|
reject: function(value) { |
|
get(this, '_deferred').reject(value); |
|
}, |
|
|
|
_deferred: Ember.computed(function() { |
|
return RSVP.defer('Ember: DeferredMixin - ' + this); |
|
}) |
|
}); |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var get = Ember.get, typeOf = Ember.typeOf; |
|
|
|
/** |
|
The `Ember.ActionHandler` mixin implements support for moving an `actions` |
|
property to an `_actions` property at extend time, and adding `_actions` |
|
to the object's mergedProperties list. |
|
|
|
`Ember.ActionHandler` is available on some familiar classes including |
|
`Ember.Route`, `Ember.View`, `Ember.Component`, and controllers such as |
|
`Ember.Controller` and `Ember.ObjectController`. |
|
(Internally the mixin is used by `Ember.CoreView`, `Ember.ControllerMixin`, |
|
and `Ember.Route` and available to the above classes through |
|
inheritance.) |
|
|
|
@class ActionHandler |
|
@namespace Ember |
|
*/ |
|
Ember.ActionHandler = Ember.Mixin.create({ |
|
mergedProperties: ['_actions'], |
|
|
|
/** |
|
The collection of functions, keyed by name, available on this |
|
`ActionHandler` as action targets. |
|
|
|
These functions will be invoked when a matching `{{action}}` is triggered |
|
from within a template and the application's current route is this route. |
|
|
|
Actions can also be invoked from other parts of your application |
|
via `ActionHandler#send`. |
|
|
|
The `actions` hash will inherit action handlers from |
|
the `actions` hash defined on extended parent classes |
|
or mixins rather than just replace the entire hash, e.g.: |
|
|
|
```js |
|
App.CanDisplayBanner = Ember.Mixin.create({ |
|
actions: { |
|
displayBanner: function(msg) { |
|
// ... |
|
} |
|
} |
|
}); |
|
|
|
App.WelcomeRoute = Ember.Route.extend(App.CanDisplayBanner, { |
|
actions: { |
|
playMusic: function() { |
|
// ... |
|
} |
|
} |
|
}); |
|
|
|
// `WelcomeRoute`, when active, will be able to respond |
|
// to both actions, since the actions hash is merged rather |
|
// then replaced when extending mixins / parent classes. |
|
this.send('displayBanner'); |
|
this.send('playMusic'); |
|
``` |
|
|
|
Within a Controller, Route, View or Component's action handler, |
|
the value of the `this` context is the Controller, Route, View or |
|
Component object: |
|
|
|
```js |
|
App.SongRoute = Ember.Route.extend({ |
|
actions: { |
|
myAction: function() { |
|
this.controllerFor("song"); |
|
this.transitionTo("other.route"); |
|
... |
|
} |
|
} |
|
}); |
|
``` |
|
|
|
It is also possible to call `this._super()` from within an |
|
action handler if it overrides a handler defined on a parent |
|
class or mixin: |
|
|
|
Take for example the following routes: |
|
|
|
```js |
|
App.DebugRoute = Ember.Mixin.create({ |
|
actions: { |
|
debugRouteInformation: function() { |
|
console.debug("trololo"); |
|
} |
|
} |
|
}); |
|
|
|
App.AnnoyingDebugRoute = Ember.Route.extend(App.DebugRoute, { |
|
actions: { |
|
debugRouteInformation: function() { |
|
// also call the debugRouteInformation of mixed in App.DebugRoute |
|
this._super(); |
|
|
|
// show additional annoyance |
|
window.alert(...); |
|
} |
|
} |
|
}); |
|
``` |
|
|
|
## Bubbling |
|
|
|
By default, an action will stop bubbling once a handler defined |
|
on the `actions` hash handles it. To continue bubbling the action, |
|
you must return `true` from the handler: |
|
|
|
```js |
|
App.Router.map(function() { |
|
this.resource("album", function() { |
|
this.route("song"); |
|
}); |
|
}); |
|
|
|
App.AlbumRoute = Ember.Route.extend({ |
|
actions: { |
|
startPlaying: function() { |
|
} |
|
} |
|
}); |
|
|
|
App.AlbumSongRoute = Ember.Route.extend({ |
|
actions: { |
|
startPlaying: function() { |
|
// ... |
|
|
|
if (actionShouldAlsoBeTriggeredOnParentRoute) { |
|
return true; |
|
} |
|
} |
|
} |
|
}); |
|
``` |
|
|
|
@property actions |
|
@type Hash |
|
@default null |
|
*/ |
|
|
|
/** |
|
Moves `actions` to `_actions` at extend time. Note that this currently |
|
modifies the mixin themselves, which is technically dubious but |
|
is practically of little consequence. This may change in the future. |
|
|
|
@private |
|
@method willMergeMixin |
|
*/ |
|
willMergeMixin: function(props) { |
|
var hashName; |
|
|
|
if (!props._actions) { |
|
Ember.assert("'actions' should not be a function", typeof(props.actions) !== 'function'); |
|
|
|
if (typeOf(props.actions) === 'object') { |
|
hashName = 'actions'; |
|
} else if (typeOf(props.events) === 'object') { |
|
Ember.deprecate('Action handlers contained in an `events` object are deprecated in favor of putting them in an `actions` object', false); |
|
hashName = 'events'; |
|
} |
|
|
|
if (hashName) { |
|
props._actions = Ember.merge(props._actions || {}, props[hashName]); |
|
} |
|
|
|
delete props[hashName]; |
|
} |
|
}, |
|
|
|
/** |
|
Triggers a named action on the `ActionHandler`. Any parameters |
|
supplied after the `actionName` string will be passed as arguments |
|
to the action target function. |
|
|
|
If the `ActionHandler` has its `target` property set, actions may |
|
bubble to the `target`. Bubbling happens when an `actionName` can |
|
not be found in the `ActionHandler`'s `actions` hash or if the |
|
action target function returns `true`. |
|
|
|
Example |
|
|
|
```js |
|
App.WelcomeRoute = Ember.Route.extend({ |
|
actions: { |
|
playTheme: function() { |
|
this.send('playMusic', 'theme.mp3'); |
|
}, |
|
playMusic: function(track) { |
|
// ... |
|
} |
|
} |
|
}); |
|
``` |
|
|
|
@method send |
|
@param {String} actionName The action to trigger |
|
@param {*} context a context to send with the action |
|
*/ |
|
send: function(actionName) { |
|
var args = [].slice.call(arguments, 1), target; |
|
|
|
if (this._actions && this._actions[actionName]) { |
|
if (this._actions[actionName].apply(this, args) === true) { |
|
// handler returned true, so this action will bubble |
|
} else { |
|
return; |
|
} |
|
} else if (!Ember.FEATURES.isEnabled('ember-routing-drop-deprecated-action-style') && this.deprecatedSend && this.deprecatedSendHandles && this.deprecatedSendHandles(actionName)) { |
|
Ember.warn("The current default is deprecated but will prefer to handle actions directly on the controller instead of a similarly named action in the actions hash. To turn off this deprecated feature set: Ember.FEATURES['ember-routing-drop-deprecated-action-style'] = true"); |
|
if (this.deprecatedSend.apply(this, [].slice.call(arguments)) === true) { |
|
// handler return true, so this action will bubble |
|
} else { |
|
return; |
|
} |
|
} |
|
|
|
if (target = get(this, 'target')) { |
|
Ember.assert("The `target` for " + this + " (" + target + ") does not have a `send` method", typeof target.send === 'function'); |
|
target.send.apply(target, arguments); |
|
} |
|
} |
|
|
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var set = Ember.set, get = Ember.get, |
|
not = Ember.computed.not, |
|
or = Ember.computed.or; |
|
|
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
function tap(proxy, promise) { |
|
set(proxy, 'isFulfilled', false); |
|
set(proxy, 'isRejected', false); |
|
|
|
return promise.then(function(value) { |
|
set(proxy, 'isFulfilled', true); |
|
set(proxy, 'content', value); |
|
return value; |
|
}, function(reason) { |
|
set(proxy, 'isRejected', true); |
|
set(proxy, 'reason', reason); |
|
throw reason; |
|
}, "Ember: PromiseProxy"); |
|
} |
|
|
|
/** |
|
A low level mixin making ObjectProxy, ObjectController or ArrayController's promise aware. |
|
|
|
```javascript |
|
var ObjectPromiseController = Ember.ObjectController.extend(Ember.PromiseProxyMixin); |
|
|
|
var controller = ObjectPromiseController.create({ |
|
promise: $.getJSON('/some/remote/data.json') |
|
}); |
|
|
|
controller.then(function(json){ |
|
// the json |
|
}, function(reason) { |
|
// the reason why you have no json |
|
}); |
|
``` |
|
|
|
the controller has bindable attributes which |
|
track the promises life cycle |
|
|
|
```javascript |
|
controller.get('isPending') //=> true |
|
controller.get('isSettled') //=> false |
|
controller.get('isRejected') //=> false |
|
controller.get('isFulfilled') //=> false |
|
``` |
|
|
|
When the the $.getJSON completes, and the promise is fulfilled |
|
with json, the life cycle attributes will update accordingly. |
|
|
|
```javascript |
|
controller.get('isPending') //=> false |
|
controller.get('isSettled') //=> true |
|
controller.get('isRejected') //=> false |
|
controller.get('isFulfilled') //=> true |
|
``` |
|
|
|
As the controller is an ObjectController, and the json now its content, |
|
all the json properties will be available directly from the controller. |
|
|
|
```javascript |
|
// Assuming the following json: |
|
{ |
|
firstName: 'Stefan', |
|
lastName: 'Penner' |
|
} |
|
|
|
// both properties will accessible on the controller |
|
controller.get('firstName') //=> 'Stefan' |
|
controller.get('lastName') //=> 'Penner' |
|
``` |
|
|
|
If the controller is backing a template, the attributes are |
|
bindable from within that template |
|
|
|
```handlebars |
|
{{#if isPending}} |
|
loading... |
|
{{else}} |
|
firstName: {{firstName}} |
|
lastName: {{lastName}} |
|
{{/if}} |
|
``` |
|
@class Ember.PromiseProxyMixin |
|
*/ |
|
Ember.PromiseProxyMixin = Ember.Mixin.create({ |
|
/** |
|
If the proxied promise is rejected this will contain the reason |
|
provided. |
|
|
|
@property reason |
|
@default null |
|
*/ |
|
reason: null, |
|
|
|
/** |
|
Once the proxied promise has settled this will become `false`. |
|
|
|
@property isPending |
|
@default true |
|
*/ |
|
isPending: not('isSettled').readOnly(), |
|
|
|
/** |
|
Once the proxied promise has settled this will become `true`. |
|
|
|
@property isSettled |
|
@default false |
|
*/ |
|
isSettled: or('isRejected', 'isFulfilled').readOnly(), |
|
|
|
/** |
|
Will become `true` if the proxied promise is rejected. |
|
|
|
@property isRejected |
|
@default false |
|
*/ |
|
isRejected: false, |
|
|
|
/** |
|
Will become `true` if the proxied promise is fulfilled. |
|
|
|
@property isFullfilled |
|
@default false |
|
*/ |
|
isFulfilled: false, |
|
|
|
/** |
|
The promise whose fulfillment value is being proxied by this object. |
|
|
|
This property must be specified upon creation, and should not be |
|
changed once created. |
|
|
|
Example: |
|
|
|
```javascript |
|
Ember.ObjectController.extend(Ember.PromiseProxyMixin).create({ |
|
promise: <thenable> |
|
}); |
|
``` |
|
|
|
@property promise |
|
*/ |
|
promise: Ember.computed(function(key, promise) { |
|
if (arguments.length === 2) { |
|
return tap(this, promise); |
|
} else { |
|
throw new Ember.Error("PromiseProxy's promise must be set"); |
|
} |
|
}), |
|
|
|
/** |
|
An alias to the proxied promise's `then`. |
|
|
|
See RSVP.Promise.then. |
|
|
|
@method then |
|
@param {Function} callback |
|
@return {RSVP.Promise} |
|
*/ |
|
then: promiseAlias('then'), |
|
|
|
/** |
|
An alias to the proxied promise's `catch`. |
|
|
|
See RSVP.Promise.catch. |
|
|
|
@method catch |
|
@param {Function} callback |
|
@return {RSVP.Promise} |
|
*/ |
|
'catch': promiseAlias('catch'), |
|
|
|
/** |
|
An alias to the proxied promise's `finally`. |
|
|
|
See RSVP.Promise.finally. |
|
|
|
@method finally |
|
@param {Function} callback |
|
@return {RSVP.Promise} |
|
*/ |
|
'finally': promiseAlias('finally') |
|
|
|
}); |
|
|
|
function promiseAlias(name) { |
|
return function () { |
|
var promise = get(this, 'promise'); |
|
return promise[name].apply(promise, arguments); |
|
}; |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var get = Ember.get, |
|
forEach = Ember.EnumerableUtils.forEach, |
|
RETAIN = 'r', |
|
INSERT = 'i', |
|
DELETE = 'd'; |
|
|
|
/** |
|
An `Ember.TrackedArray` tracks array operations. It's useful when you want to |
|
lazily compute the indexes of items in an array after they've been shifted by |
|
subsequent operations. |
|
|
|
@class TrackedArray |
|
@namespace Ember |
|
@param {array} [items=[]] The array to be tracked. This is used just to get |
|
the initial items for the starting state of retain:n. |
|
*/ |
|
Ember.TrackedArray = function (items) { |
|
if (arguments.length < 1) { items = []; } |
|
|
|
var length = get(items, 'length'); |
|
|
|
if (length) { |
|
this._operations = [new ArrayOperation(RETAIN, length, items)]; |
|
} else { |
|
this._operations = []; |
|
} |
|
}; |
|
|
|
Ember.TrackedArray.RETAIN = RETAIN; |
|
Ember.TrackedArray.INSERT = INSERT; |
|
Ember.TrackedArray.DELETE = DELETE; |
|
|
|
Ember.TrackedArray.prototype = { |
|
|
|
/** |
|
Track that `newItems` were added to the tracked array at `index`. |
|
|
|
@method addItems |
|
@param index |
|
@param newItems |
|
*/ |
|
addItems: function (index, newItems) { |
|
var count = get(newItems, 'length'); |
|
if (count < 1) { return; } |
|
|
|
var match = this._findArrayOperation(index), |
|
arrayOperation = match.operation, |
|
arrayOperationIndex = match.index, |
|
arrayOperationRangeStart = match.rangeStart, |
|
composeIndex, |
|
splitIndex, |
|
splitItems, |
|
splitArrayOperation, |
|
newArrayOperation; |
|
|
|
newArrayOperation = new ArrayOperation(INSERT, count, newItems); |
|
|
|
if (arrayOperation) { |
|
if (!match.split) { |
|
// insert left of arrayOperation |
|
this._operations.splice(arrayOperationIndex, 0, newArrayOperation); |
|
composeIndex = arrayOperationIndex; |
|
} else { |
|
this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); |
|
composeIndex = arrayOperationIndex + 1; |
|
} |
|
} else { |
|
// insert at end |
|
this._operations.push(newArrayOperation); |
|
composeIndex = arrayOperationIndex; |
|
} |
|
|
|
this._composeInsert(composeIndex); |
|
}, |
|
|
|
/** |
|
Track that `count` items were removed at `index`. |
|
|
|
@method removeItems |
|
@param index |
|
@param count |
|
*/ |
|
removeItems: function (index, count) { |
|
if (count < 1) { return; } |
|
|
|
var match = this._findArrayOperation(index), |
|
arrayOperation = match.operation, |
|
arrayOperationIndex = match.index, |
|
arrayOperationRangeStart = match.rangeStart, |
|
newArrayOperation, |
|
composeIndex; |
|
|
|
newArrayOperation = new ArrayOperation(DELETE, count); |
|
if (!match.split) { |
|
// insert left of arrayOperation |
|
this._operations.splice(arrayOperationIndex, 0, newArrayOperation); |
|
composeIndex = arrayOperationIndex; |
|
} else { |
|
this._split(arrayOperationIndex, index - arrayOperationRangeStart, newArrayOperation); |
|
composeIndex = arrayOperationIndex + 1; |
|
} |
|
|
|
return this._composeDelete(composeIndex); |
|
}, |
|
|
|
/** |
|
Apply all operations, reducing them to retain:n, for `n`, the number of |
|
items in the array. |
|
|
|
`callback` will be called for each operation and will be passed the following arguments: |
|
|
|
* {array} items The items for the given operation |
|
* {number} offset The computed offset of the items, ie the index in the |
|
array of the first item for this operation. |
|
* {string} operation The type of the operation. One of |
|
`Ember.TrackedArray.{RETAIN, DELETE, INSERT}` |
|
|
|
@method apply |
|
@param {function} callback |
|
*/ |
|
apply: function (callback) { |
|
var items = [], |
|
offset = 0; |
|
|
|
forEach(this._operations, function (arrayOperation) { |
|
callback(arrayOperation.items, offset, arrayOperation.type); |
|
|
|
if (arrayOperation.type !== DELETE) { |
|
offset += arrayOperation.count; |
|
items = items.concat(arrayOperation.items); |
|
} |
|
}); |
|
|
|
this._operations = [new ArrayOperation(RETAIN, items.length, items)]; |
|
}, |
|
|
|
/** |
|
Return an `ArrayOperationMatch` for the operation that contains the item at `index`. |
|
|
|
@method _findArrayOperation |
|
|
|
@param {number} index the index of the item whose operation information |
|
should be returned. |
|
@private |
|
*/ |
|
_findArrayOperation: function (index) { |
|
var arrayOperationIndex, |
|
len, |
|
split = false, |
|
arrayOperation, |
|
arrayOperationRangeStart, |
|
arrayOperationRangeEnd; |
|
|
|
// OPTIMIZE: we could search these faster if we kept a balanced tree. |
|
// find leftmost arrayOperation to the right of `index` |
|
for (arrayOperationIndex = arrayOperationRangeStart = 0, len = this._operations.length; arrayOperationIndex < len; ++arrayOperationIndex) { |
|
arrayOperation = this._operations[arrayOperationIndex]; |
|
|
|
if (arrayOperation.type === DELETE) { continue; } |
|
|
|
arrayOperationRangeEnd = arrayOperationRangeStart + arrayOperation.count - 1; |
|
|
|
if (index === arrayOperationRangeStart) { |
|
break; |
|
} else if (index > arrayOperationRangeStart && index <= arrayOperationRangeEnd) { |
|
split = true; |
|
break; |
|
} else { |
|
arrayOperationRangeStart = arrayOperationRangeEnd + 1; |
|
} |
|
} |
|
|
|
return new ArrayOperationMatch(arrayOperation, arrayOperationIndex, split, arrayOperationRangeStart); |
|
}, |
|
|
|
_split: function (arrayOperationIndex, splitIndex, newArrayOperation) { |
|
var arrayOperation = this._operations[arrayOperationIndex], |
|
splitItems = arrayOperation.items.slice(splitIndex), |
|
splitArrayOperation = new ArrayOperation(arrayOperation.type, splitItems.length, splitItems); |
|
|
|
// truncate LHS |
|
arrayOperation.count = splitIndex; |
|
arrayOperation.items = arrayOperation.items.slice(0, splitIndex); |
|
|
|
this._operations.splice(arrayOperationIndex + 1, 0, newArrayOperation, splitArrayOperation); |
|
}, |
|
|
|
// see SubArray for a better implementation. |
|
_composeInsert: function (index) { |
|
var newArrayOperation = this._operations[index], |
|
leftArrayOperation = this._operations[index-1], // may be undefined |
|
rightArrayOperation = this._operations[index+1], // may be undefined |
|
leftOp = leftArrayOperation && leftArrayOperation.type, |
|
rightOp = rightArrayOperation && rightArrayOperation.type; |
|
|
|
if (leftOp === INSERT) { |
|
// merge left |
|
leftArrayOperation.count += newArrayOperation.count; |
|
leftArrayOperation.items = leftArrayOperation.items.concat(newArrayOperation.items); |
|
|
|
if (rightOp === INSERT) { |
|
// also merge right (we have split an insert with an insert) |
|
leftArrayOperation.count += rightArrayOperation.count; |
|
leftArrayOperation.items = leftArrayOperation.items.concat(rightArrayOperation.items); |
|
this._operations.splice(index, 2); |
|
} else { |
|
// only merge left |
|
this._operations.splice(index, 1); |
|
} |
|
} else if (rightOp === INSERT) { |
|
// merge right |
|
newArrayOperation.count += rightArrayOperation.count; |
|
newArrayOperation.items = newArrayOperation.items.concat(rightArrayOperation.items); |
|
this._operations.splice(index + 1, 1); |
|
} |
|
}, |
|
|
|
_composeDelete: function (index) { |
|
var arrayOperation = this._operations[index], |
|
deletesToGo = arrayOperation.count, |
|
leftArrayOperation = this._operations[index-1], // may be undefined |
|
leftOp = leftArrayOperation && leftArrayOperation.type, |
|
nextArrayOperation, |
|
nextOp, |
|
nextCount, |
|
removeNewAndNextOp = false, |
|
removedItems = []; |
|
|
|
if (leftOp === DELETE) { |
|
arrayOperation = leftArrayOperation; |
|
index -= 1; |
|
} |
|
|
|
for (var i = index + 1; deletesToGo > 0; ++i) { |
|
nextArrayOperation = this._operations[i]; |
|
nextOp = nextArrayOperation.type; |
|
nextCount = nextArrayOperation.count; |
|
|
|
if (nextOp === DELETE) { |
|
arrayOperation.count += nextCount; |
|
continue; |
|
} |
|
|
|
if (nextCount > deletesToGo) { |
|
// d:2 {r,i}:5 we reduce the retain or insert, but it stays |
|
removedItems = removedItems.concat(nextArrayOperation.items.splice(0, deletesToGo)); |
|
nextArrayOperation.count -= deletesToGo; |
|
|
|
// In the case where we truncate the last arrayOperation, we don't need to |
|
// remove it; also the deletesToGo reduction is not the entirety of |
|
// nextCount |
|
i -= 1; |
|
nextCount = deletesToGo; |
|
|
|
deletesToGo = 0; |
|
} else { |
|
if (nextCount === deletesToGo) { |
|
// Handle edge case of d:2 i:2 in which case both operations go away |
|
// during composition. |
|
removeNewAndNextOp = true; |
|
} |
|
removedItems = removedItems.concat(nextArrayOperation.items); |
|
deletesToGo -= nextCount; |
|
} |
|
|
|
if (nextOp === INSERT) { |
|
// d:2 i:3 will result in delete going away |
|
arrayOperation.count -= nextCount; |
|
} |
|
} |
|
|
|
if (arrayOperation.count > 0) { |
|
// compose our new delete with possibly several operations to the right of |
|
// disparate types |
|
this._operations.splice(index+1, i-1-index); |
|
} else { |
|
// The delete operation can go away; it has merely reduced some other |
|
// operation, as in d:3 i:4; it may also have eliminated that operation, |
|
// as in d:3 i:3. |
|
this._operations.splice(index, removeNewAndNextOp ? 2 : 1); |
|
} |
|
|
|
return removedItems; |
|
}, |
|
|
|
toString: function () { |
|
var str = ""; |
|
forEach(this._operations, function (operation) { |
|
str += " " + operation.type + ":" + operation.count; |
|
}); |
|
return str.substring(1); |
|
} |
|
}; |
|
|
|
/** |
|
Internal data structure to represent an array operation. |
|
|
|
@method ArrayOperation |
|
@private |
|
@param {string} type The type of the operation. One of |
|
`Ember.TrackedArray.{RETAIN, INSERT, DELETE}` |
|
@param {number} count The number of items in this operation. |
|
@param {array} items The items of the operation, if included. RETAIN and |
|
INSERT include their items, DELETE does not. |
|
*/ |
|
function ArrayOperation (operation, count, items) { |
|
this.type = operation; // RETAIN | INSERT | DELETE |
|
this.count = count; |
|
this.items = items; |
|
} |
|
|
|
/** |
|
Internal data structure used to include information when looking up operations |
|
by item index. |
|
|
|
@method ArrayOperationMatch |
|
@private |
|
@param {ArrayOperation} operation |
|
@param {number} index The index of `operation` in the array of operations. |
|
@param {boolean} split Whether or not the item index searched for would |
|
require a split for a new operation type. |
|
@param {number} rangeStart The index of the first item in the operation, |
|
with respect to the tracked array. The index of the last item can be computed |
|
from `rangeStart` and `operation.count`. |
|
*/ |
|
function ArrayOperationMatch(operation, index, split, rangeStart) { |
|
this.operation = operation; |
|
this.index = index; |
|
this.split = split; |
|
this.rangeStart = rangeStart; |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var get = Ember.get, |
|
forEach = Ember.EnumerableUtils.forEach, |
|
RETAIN = 'r', |
|
FILTER = 'f'; |
|
|
|
function Operation (type, count) { |
|
this.type = type; |
|
this.count = count; |
|
} |
|
|
|
/** |
|
An `Ember.SubArray` tracks an array in a way similar to, but more specialized |
|
than, `Ember.TrackedArray`. It is useful for keeping track of the indexes of |
|
items within a filtered array. |
|
|
|
@class SubArray |
|
@namespace Ember |
|
*/ |
|
Ember.SubArray = function (length) { |
|
if (arguments.length < 1) { length = 0; } |
|
|
|
if (length > 0) { |
|
this._operations = [new Operation(RETAIN, length)]; |
|
} else { |
|
this._operations = []; |
|
} |
|
}; |
|
|
|
Ember.SubArray.prototype = { |
|
/** |
|
Track that an item was added to the tracked array. |
|
|
|
@method addItem |
|
|
|
@param {number} index The index of the item in the tracked array. |
|
@param {boolean} match `true` iff the item is included in the subarray. |
|
|
|
@return {number} The index of the item in the subarray. |
|
*/ |
|
addItem: function(index, match) { |
|
var returnValue = -1, |
|
itemType = match ? RETAIN : FILTER, |
|
self = this; |
|
|
|
this._findOperation(index, function(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { |
|
var newOperation, splitOperation; |
|
|
|
if (itemType === operation.type) { |
|
++operation.count; |
|
} else if (index === rangeStart) { |
|
// insert to the left of `operation` |
|
self._operations.splice(operationIndex, 0, new Operation(itemType, 1)); |
|
} else { |
|
newOperation = new Operation(itemType, 1); |
|
splitOperation = new Operation(operation.type, rangeEnd - index + 1); |
|
operation.count = index - rangeStart; |
|
|
|
self._operations.splice(operationIndex + 1, 0, newOperation, splitOperation); |
|
} |
|
|
|
if (match) { |
|
if (operation.type === RETAIN) { |
|
returnValue = seenInSubArray + (index - rangeStart); |
|
} else { |
|
returnValue = seenInSubArray; |
|
} |
|
} |
|
|
|
self._composeAt(operationIndex); |
|
}, function(seenInSubArray) { |
|
self._operations.push(new Operation(itemType, 1)); |
|
|
|
if (match) { |
|
returnValue = seenInSubArray; |
|
} |
|
|
|
self._composeAt(self._operations.length-1); |
|
}); |
|
|
|
return returnValue; |
|
}, |
|
|
|
/** |
|
Track that an item was removed from the tracked array. |
|
|
|
@method removeItem |
|
|
|
@param {number} index The index of the item in the tracked array. |
|
|
|
@return {number} The index of the item in the subarray, or `-1` if the item |
|
was not in the subarray. |
|
*/ |
|
removeItem: function(index) { |
|
var returnValue = -1, |
|
self = this; |
|
|
|
this._findOperation(index, function (operation, operationIndex, rangeStart, rangeEnd, seenInSubArray) { |
|
if (operation.type === RETAIN) { |
|
returnValue = seenInSubArray + (index - rangeStart); |
|
} |
|
|
|
if (operation.count > 1) { |
|
--operation.count; |
|
} else { |
|
self._operations.splice(operationIndex, 1); |
|
self._composeAt(operationIndex); |
|
} |
|
}, function() { |
|
throw new Ember.Error("Can't remove an item that has never been added."); |
|
}); |
|
|
|
return returnValue; |
|
}, |
|
|
|
|
|
_findOperation: function (index, foundCallback, notFoundCallback) { |
|
var operationIndex, |
|
len, |
|
operation, |
|
rangeStart, |
|
rangeEnd, |
|
seenInSubArray = 0; |
|
|
|
// OPTIMIZE: change to balanced tree |
|
// find leftmost operation to the right of `index` |
|
for (operationIndex = rangeStart = 0, len = this._operations.length; operationIndex < len; rangeStart = rangeEnd + 1, ++operationIndex) { |
|
operation = this._operations[operationIndex]; |
|
rangeEnd = rangeStart + operation.count - 1; |
|
|
|
if (index >= rangeStart && index <= rangeEnd) { |
|
foundCallback(operation, operationIndex, rangeStart, rangeEnd, seenInSubArray); |
|
return; |
|
} else if (operation.type === RETAIN) { |
|
seenInSubArray += operation.count; |
|
} |
|
} |
|
|
|
notFoundCallback(seenInSubArray); |
|
}, |
|
|
|
_composeAt: function(index) { |
|
var op = this._operations[index], |
|
otherOp; |
|
|
|
if (!op) { |
|
// Composing out of bounds is a no-op, as when removing the last operation |
|
// in the list. |
|
return; |
|
} |
|
|
|
if (index > 0) { |
|
otherOp = this._operations[index-1]; |
|
if (otherOp.type === op.type) { |
|
op.count += otherOp.count; |
|
this._operations.splice(index-1, 1); |
|
--index; |
|
} |
|
} |
|
|
|
if (index < this._operations.length-1) { |
|
otherOp = this._operations[index+1]; |
|
if (otherOp.type === op.type) { |
|
op.count += otherOp.count; |
|
this._operations.splice(index+1, 1); |
|
} |
|
} |
|
}, |
|
|
|
toString: function () { |
|
var str = ""; |
|
forEach(this._operations, function (operation) { |
|
str += " " + operation.type + ":" + operation.count; |
|
}); |
|
return str.substring(1); |
|
} |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
Ember.Container = requireModule('container')['default']; |
|
Ember.Container.set = Ember.set; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
Ember.Application = Ember.Namespace.extend(); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var OUT_OF_RANGE_EXCEPTION = "Index out of range"; |
|
var EMPTY = []; |
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
/** |
|
An ArrayProxy wraps any other object that implements `Ember.Array` and/or |
|
`Ember.MutableArray,` forwarding all requests. This makes it very useful for |
|
a number of binding use cases or other cases where being able to swap |
|
out the underlying array is useful. |
|
|
|
A simple example of usage: |
|
|
|
```javascript |
|
var pets = ['dog', 'cat', 'fish']; |
|
var ap = Ember.ArrayProxy.create({ content: Ember.A(pets) }); |
|
|
|
ap.get('firstObject'); // 'dog' |
|
ap.set('content', ['amoeba', 'paramecium']); |
|
ap.get('firstObject'); // 'amoeba' |
|
``` |
|
|
|
This class can also be useful as a layer to transform the contents of |
|
an array, as they are accessed. This can be done by overriding |
|
`objectAtContent`: |
|
|
|
```javascript |
|
var pets = ['dog', 'cat', 'fish']; |
|
var ap = Ember.ArrayProxy.create({ |
|
content: Ember.A(pets), |
|
objectAtContent: function(idx) { |
|
return this.get('content').objectAt(idx).toUpperCase(); |
|
} |
|
}); |
|
|
|
ap.get('firstObject'); // . 'DOG' |
|
``` |
|
|
|
@class ArrayProxy |
|
@namespace Ember |
|
@extends Ember.Object |
|
@uses Ember.MutableArray |
|
*/ |
|
Ember.ArrayProxy = Ember.Object.extend(Ember.MutableArray, { |
|
|
|
/** |
|
The content array. Must be an object that implements `Ember.Array` and/or |
|
`Ember.MutableArray.` |
|
|
|
@property content |
|
@type Ember.Array |
|
*/ |
|
content: null, |
|
|
|
/** |
|
The array that the proxy pretends to be. In the default `ArrayProxy` |
|
implementation, this and `content` are the same. Subclasses of `ArrayProxy` |
|
can override this property to provide things like sorting and filtering. |
|
|
|
@property arrangedContent |
|
*/ |
|
arrangedContent: Ember.computed.alias('content'), |
|
|
|
/** |
|
Should actually retrieve the object at the specified index from the |
|
content. You can override this method in subclasses to transform the |
|
content item to something new. |
|
|
|
This method will only be called if content is non-`null`. |
|
|
|
@method objectAtContent |
|
@param {Number} idx The index to retrieve. |
|
@return {Object} the value or undefined if none found |
|
*/ |
|
objectAtContent: function(idx) { |
|
return get(this, 'arrangedContent').objectAt(idx); |
|
}, |
|
|
|
/** |
|
Should actually replace the specified objects on the content array. |
|
You can override this method in subclasses to transform the content item |
|
into something new. |
|
|
|
This method will only be called if content is non-`null`. |
|
|
|
@method replaceContent |
|
@param {Number} idx The starting index |
|
@param {Number} amt The number of items to remove from the content. |
|
@param {Array} objects Optional array of objects to insert or null if no |
|
objects. |
|
@return {void} |
|
*/ |
|
replaceContent: function(idx, amt, objects) { |
|
get(this, 'content').replace(idx, amt, objects); |
|
}, |
|
|
|
/** |
|
Invoked when the content property is about to change. Notifies observers that the |
|
entire array content will change. |
|
|
|
@private |
|
@method _contentWillChange |
|
*/ |
|
_contentWillChange: Ember.beforeObserver('content', function() { |
|
this._teardownContent(); |
|
}), |
|
|
|
_teardownContent: function() { |
|
var content = get(this, 'content'); |
|
|
|
if (content) { |
|
content.removeArrayObserver(this, { |
|
willChange: 'contentArrayWillChange', |
|
didChange: 'contentArrayDidChange' |
|
}); |
|
} |
|
}, |
|
|
|
contentArrayWillChange: Ember.K, |
|
contentArrayDidChange: Ember.K, |
|
|
|
/** |
|
Invoked when the content property changes. Notifies observers that the |
|
entire array content has changed. |
|
|
|
@private |
|
@method _contentDidChange |
|
*/ |
|
_contentDidChange: Ember.observer('content', function() { |
|
var content = get(this, 'content'); |
|
|
|
Ember.assert("Can't set ArrayProxy's content to itself", content !== this); |
|
|
|
this._setupContent(); |
|
}), |
|
|
|
_setupContent: function() { |
|
var content = get(this, 'content'); |
|
|
|
if (content) { |
|
Ember.assert(Ember.String.fmt('ArrayProxy expects an Array or ' + |
|
'Ember.ArrayProxy, but you passed %@', [typeof content]), |
|
Ember.isArray(content) || content.isDestroyed); |
|
|
|
content.addArrayObserver(this, { |
|
willChange: 'contentArrayWillChange', |
|
didChange: 'contentArrayDidChange' |
|
}); |
|
} |
|
}, |
|
|
|
_arrangedContentWillChange: Ember.beforeObserver('arrangedContent', function() { |
|
var arrangedContent = get(this, 'arrangedContent'), |
|
len = arrangedContent ? get(arrangedContent, 'length') : 0; |
|
|
|
this.arrangedContentArrayWillChange(this, 0, len, undefined); |
|
this.arrangedContentWillChange(this); |
|
|
|
this._teardownArrangedContent(arrangedContent); |
|
}), |
|
|
|
_arrangedContentDidChange: Ember.observer('arrangedContent', function() { |
|
var arrangedContent = get(this, 'arrangedContent'), |
|
len = arrangedContent ? get(arrangedContent, 'length') : 0; |
|
|
|
Ember.assert("Can't set ArrayProxy's content to itself", arrangedContent !== this); |
|
|
|
this._setupArrangedContent(); |
|
|
|
this.arrangedContentDidChange(this); |
|
this.arrangedContentArrayDidChange(this, 0, undefined, len); |
|
}), |
|
|
|
_setupArrangedContent: function() { |
|
var arrangedContent = get(this, 'arrangedContent'); |
|
|
|
if (arrangedContent) { |
|
Ember.assert(Ember.String.fmt('ArrayProxy expects an Array or ' + |
|
'Ember.ArrayProxy, but you passed %@', [typeof arrangedContent]), |
|
Ember.isArray(arrangedContent) || arrangedContent.isDestroyed); |
|
|
|
arrangedContent.addArrayObserver(this, { |
|
willChange: 'arrangedContentArrayWillChange', |
|
didChange: 'arrangedContentArrayDidChange' |
|
}); |
|
} |
|
}, |
|
|
|
_teardownArrangedContent: function() { |
|
var arrangedContent = get(this, 'arrangedContent'); |
|
|
|
if (arrangedContent) { |
|
arrangedContent.removeArrayObserver(this, { |
|
willChange: 'arrangedContentArrayWillChange', |
|
didChange: 'arrangedContentArrayDidChange' |
|
}); |
|
} |
|
}, |
|
|
|
arrangedContentWillChange: Ember.K, |
|
arrangedContentDidChange: Ember.K, |
|
|
|
objectAt: function(idx) { |
|
return get(this, 'content') && this.objectAtContent(idx); |
|
}, |
|
|
|
length: Ember.computed(function() { |
|
var arrangedContent = get(this, 'arrangedContent'); |
|
return arrangedContent ? get(arrangedContent, 'length') : 0; |
|
// No dependencies since Enumerable notifies length of change |
|
}), |
|
|
|
_replace: function(idx, amt, objects) { |
|
var content = get(this, 'content'); |
|
Ember.assert('The content property of '+ this.constructor + ' should be set before modifying it', content); |
|
if (content) this.replaceContent(idx, amt, objects); |
|
return this; |
|
}, |
|
|
|
replace: function() { |
|
if (get(this, 'arrangedContent') === get(this, 'content')) { |
|
this._replace.apply(this, arguments); |
|
} else { |
|
throw new Ember.Error("Using replace on an arranged ArrayProxy is not allowed."); |
|
} |
|
}, |
|
|
|
_insertAt: function(idx, object) { |
|
if (idx > get(this, 'content.length')) throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); |
|
this._replace(idx, 0, [object]); |
|
return this; |
|
}, |
|
|
|
insertAt: function(idx, object) { |
|
if (get(this, 'arrangedContent') === get(this, 'content')) { |
|
return this._insertAt(idx, object); |
|
} else { |
|
throw new Ember.Error("Using insertAt on an arranged ArrayProxy is not allowed."); |
|
} |
|
}, |
|
|
|
removeAt: function(start, len) { |
|
if ('number' === typeof start) { |
|
var content = get(this, 'content'), |
|
arrangedContent = get(this, 'arrangedContent'), |
|
indices = [], i; |
|
|
|
if ((start < 0) || (start >= get(this, 'length'))) { |
|
throw new Ember.Error(OUT_OF_RANGE_EXCEPTION); |
|
} |
|
|
|
if (len === undefined) len = 1; |
|
|
|
// Get a list of indices in original content to remove |
|
for (i=start; i<start+len; i++) { |
|
// Use arrangedContent here so we avoid confusion with objects transformed by objectAtContent |
|
indices.push(content.indexOf(arrangedContent.objectAt(i))); |
|
} |
|
|
|
// Replace in reverse order since indices will change |
|
indices.sort(function(a,b) { return b - a; }); |
|
|
|
Ember.beginPropertyChanges(); |
|
for (i=0; i<indices.length; i++) { |
|
this._replace(indices[i], 1, EMPTY); |
|
} |
|
Ember.endPropertyChanges(); |
|
} |
|
|
|
return this ; |
|
}, |
|
|
|
pushObject: function(obj) { |
|
this._insertAt(get(this, 'content.length'), obj) ; |
|
return obj ; |
|
}, |
|
|
|
pushObjects: function(objects) { |
|
if (!(Ember.Enumerable.detect(objects) || Ember.isArray(objects))) { |
|
throw new TypeError("Must pass Ember.Enumerable to Ember.MutableArray#pushObjects"); |
|
} |
|
this._replace(get(this, 'length'), 0, objects); |
|
return this; |
|
}, |
|
|
|
setObjects: function(objects) { |
|
if (objects.length === 0) return this.clear(); |
|
|
|
var len = get(this, 'length'); |
|
this._replace(0, len, objects); |
|
return this; |
|
}, |
|
|
|
unshiftObject: function(obj) { |
|
this._insertAt(0, obj) ; |
|
return obj ; |
|
}, |
|
|
|
unshiftObjects: function(objects) { |
|
this._replace(0, 0, objects); |
|
return this; |
|
}, |
|
|
|
slice: function() { |
|
var arr = this.toArray(); |
|
return arr.slice.apply(arr, arguments); |
|
}, |
|
|
|
arrangedContentArrayWillChange: function(item, idx, removedCnt, addedCnt) { |
|
this.arrayContentWillChange(idx, removedCnt, addedCnt); |
|
}, |
|
|
|
arrangedContentArrayDidChange: function(item, idx, removedCnt, addedCnt) { |
|
this.arrayContentDidChange(idx, removedCnt, addedCnt); |
|
}, |
|
|
|
init: function() { |
|
this._super(); |
|
this._setupContent(); |
|
this._setupArrangedContent(); |
|
}, |
|
|
|
willDestroy: function() { |
|
this._teardownArrangedContent(); |
|
this._teardownContent(); |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
|
|
var set = Ember.set, get = Ember.get, guidFor = Ember.guidFor; |
|
var forEach = Ember.EnumerableUtils.forEach, |
|
indexOf = Ember.ArrayPolyfills.indexOf; |
|
|
|
var EachArray = Ember.Object.extend(Ember.Array, { |
|
|
|
init: function(content, keyName, owner) { |
|
this._super(); |
|
this._keyName = keyName; |
|
this._owner = owner; |
|
this._content = content; |
|
}, |
|
|
|
objectAt: function(idx) { |
|
var item = this._content.objectAt(idx); |
|
return item && get(item, this._keyName); |
|
}, |
|
|
|
length: Ember.computed(function() { |
|
var content = this._content; |
|
return content ? get(content, 'length') : 0; |
|
}) |
|
|
|
}); |
|
|
|
var IS_OBSERVER = /^.+:(before|change)$/; |
|
|
|
function addObserverForContentKey(content, keyName, proxy, idx, loc) { |
|
var objects = proxy._objects, guid; |
|
if (!objects) objects = proxy._objects = {}; |
|
|
|
while(--loc>=idx) { |
|
var item = content.objectAt(loc); |
|
if (item) { |
|
Ember.assert('When using @each to observe the array ' + content + ', the array must return an object', Ember.typeOf(item) === 'instance' || Ember.typeOf(item) === 'object'); |
|
Ember.addBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); |
|
Ember.addObserver(item, keyName, proxy, 'contentKeyDidChange'); |
|
|
|
// keep track of the index each item was found at so we can map |
|
// it back when the obj changes. |
|
guid = guidFor(item); |
|
if (!objects[guid]) objects[guid] = []; |
|
objects[guid].push(loc); |
|
} |
|
} |
|
} |
|
|
|
function removeObserverForContentKey(content, keyName, proxy, idx, loc) { |
|
var objects = proxy._objects; |
|
if (!objects) objects = proxy._objects = {}; |
|
var indicies, guid; |
|
|
|
while(--loc>=idx) { |
|
var item = content.objectAt(loc); |
|
if (item) { |
|
Ember.removeBeforeObserver(item, keyName, proxy, 'contentKeyWillChange'); |
|
Ember.removeObserver(item, keyName, proxy, 'contentKeyDidChange'); |
|
|
|
guid = guidFor(item); |
|
indicies = objects[guid]; |
|
indicies[indexOf.call(indicies, loc)] = null; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
This is the object instance returned when you get the `@each` property on an |
|
array. It uses the unknownProperty handler to automatically create |
|
EachArray instances for property names. |
|
|
|
@private |
|
@class EachProxy |
|
@namespace Ember |
|
@extends Ember.Object |
|
*/ |
|
Ember.EachProxy = Ember.Object.extend({ |
|
|
|
init: function(content) { |
|
this._super(); |
|
this._content = content; |
|
content.addArrayObserver(this); |
|
|
|
// in case someone is already observing some keys make sure they are |
|
// added |
|
forEach(Ember.watchedEvents(this), function(eventName) { |
|
this.didAddListener(eventName); |
|
}, this); |
|
}, |
|
|
|
/** |
|
You can directly access mapped properties by simply requesting them. |
|
The `unknownProperty` handler will generate an EachArray of each item. |
|
|
|
@method unknownProperty |
|
@param keyName {String} |
|
@param value {*} |
|
*/ |
|
unknownProperty: function(keyName, value) { |
|
var ret; |
|
ret = new EachArray(this._content, keyName, this); |
|
Ember.defineProperty(this, keyName, null, ret); |
|
this.beginObservingContentKey(keyName); |
|
return ret; |
|
}, |
|
|
|
// .......................................................... |
|
// ARRAY CHANGES |
|
// Invokes whenever the content array itself changes. |
|
|
|
arrayWillChange: function(content, idx, removedCnt, addedCnt) { |
|
var keys = this._keys, key, lim; |
|
|
|
lim = removedCnt>0 ? idx+removedCnt : -1; |
|
Ember.beginPropertyChanges(this); |
|
|
|
for(key in keys) { |
|
if (!keys.hasOwnProperty(key)) { continue; } |
|
|
|
if (lim>0) { removeObserverForContentKey(content, key, this, idx, lim); } |
|
|
|
Ember.propertyWillChange(this, key); |
|
} |
|
|
|
Ember.propertyWillChange(this._content, '@each'); |
|
Ember.endPropertyChanges(this); |
|
}, |
|
|
|
arrayDidChange: function(content, idx, removedCnt, addedCnt) { |
|
var keys = this._keys, lim; |
|
|
|
lim = addedCnt>0 ? idx+addedCnt : -1; |
|
Ember.changeProperties(function() { |
|
for(var key in keys) { |
|
if (!keys.hasOwnProperty(key)) { continue; } |
|
|
|
if (lim>0) { addObserverForContentKey(content, key, this, idx, lim); } |
|
|
|
Ember.propertyDidChange(this, key); |
|
} |
|
|
|
Ember.propertyDidChange(this._content, '@each'); |
|
}, this); |
|
}, |
|
|
|
// .......................................................... |
|
// LISTEN FOR NEW OBSERVERS AND OTHER EVENT LISTENERS |
|
// Start monitoring keys based on who is listening... |
|
|
|
didAddListener: function(eventName) { |
|
if (IS_OBSERVER.test(eventName)) { |
|
this.beginObservingContentKey(eventName.slice(0, -7)); |
|
} |
|
}, |
|
|
|
didRemoveListener: function(eventName) { |
|
if (IS_OBSERVER.test(eventName)) { |
|
this.stopObservingContentKey(eventName.slice(0, -7)); |
|
} |
|
}, |
|
|
|
// .......................................................... |
|
// CONTENT KEY OBSERVING |
|
// Actual watch keys on the source content. |
|
|
|
beginObservingContentKey: function(keyName) { |
|
var keys = this._keys; |
|
if (!keys) keys = this._keys = {}; |
|
if (!keys[keyName]) { |
|
keys[keyName] = 1; |
|
var content = this._content, |
|
len = get(content, 'length'); |
|
addObserverForContentKey(content, keyName, this, 0, len); |
|
} else { |
|
keys[keyName]++; |
|
} |
|
}, |
|
|
|
stopObservingContentKey: function(keyName) { |
|
var keys = this._keys; |
|
if (keys && (keys[keyName]>0) && (--keys[keyName]<=0)) { |
|
var content = this._content, |
|
len = get(content, 'length'); |
|
removeObserverForContentKey(content, keyName, this, 0, len); |
|
} |
|
}, |
|
|
|
contentKeyWillChange: function(obj, keyName) { |
|
Ember.propertyWillChange(this, keyName); |
|
}, |
|
|
|
contentKeyDidChange: function(obj, keyName) { |
|
Ember.propertyDidChange(this, keyName); |
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
|
|
var get = Ember.get, set = Ember.set, replace = Ember.EnumerableUtils._replace; |
|
|
|
// Add Ember.Array to Array.prototype. Remove methods with native |
|
// implementations and supply some more optimized versions of generic methods |
|
// because they are so common. |
|
var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember.Copyable, { |
|
|
|
// because length is a built-in property we need to know to just get the |
|
// original property. |
|
get: function(key) { |
|
if (key==='length') return this.length; |
|
else if ('number' === typeof key) return this[key]; |
|
else return this._super(key); |
|
}, |
|
|
|
objectAt: function(idx) { |
|
return this[idx]; |
|
}, |
|
|
|
// primitive for array support. |
|
replace: function(idx, amt, objects) { |
|
|
|
if (this.isFrozen) throw Ember.FROZEN_ERROR; |
|
|
|
// if we replaced exactly the same number of items, then pass only the |
|
// replaced range. Otherwise, pass the full remaining array length |
|
// since everything has shifted |
|
var len = objects ? get(objects, 'length') : 0; |
|
this.arrayContentWillChange(idx, amt, len); |
|
|
|
if (len === 0) { |
|
this.splice(idx, amt); |
|
} else { |
|
replace(this, idx, amt, objects); |
|
} |
|
|
|
this.arrayContentDidChange(idx, amt, len); |
|
return this; |
|
}, |
|
|
|
// If you ask for an unknown property, then try to collect the value |
|
// from member items. |
|
unknownProperty: function(key, value) { |
|
var ret;// = this.reducedProperty(key, value) ; |
|
if ((value !== undefined) && ret === undefined) { |
|
ret = this[key] = value; |
|
} |
|
return ret ; |
|
}, |
|
|
|
// If browser did not implement indexOf natively, then override with |
|
// specialized version |
|
indexOf: function(object, startAt) { |
|
var idx, len = this.length; |
|
|
|
if (startAt === undefined) startAt = 0; |
|
else startAt = (startAt < 0) ? Math.ceil(startAt) : Math.floor(startAt); |
|
if (startAt < 0) startAt += len; |
|
|
|
for(idx=startAt;idx<len;idx++) { |
|
if (this[idx] === object) return idx ; |
|
} |
|
return -1; |
|
}, |
|
|
|
lastIndexOf: function(object, startAt) { |
|
var idx, len = this.length; |
|
|
|
if (startAt === undefined) startAt = len-1; |
|
else startAt = (startAt < 0) ? Math.ceil(startAt) : Math.floor(startAt); |
|
if (startAt < 0) startAt += len; |
|
|
|
for(idx=startAt;idx>=0;idx--) { |
|
if (this[idx] === object) return idx ; |
|
} |
|
return -1; |
|
}, |
|
|
|
copy: function(deep) { |
|
if (deep) { |
|
return this.map(function(item) { return Ember.copy(item, true); }); |
|
} |
|
|
|
return this.slice(); |
|
} |
|
}); |
|
|
|
// Remove any methods implemented natively so we don't override them |
|
var ignore = ['length']; |
|
Ember.EnumerableUtils.forEach(NativeArray.keys(), function(methodName) { |
|
if (Array.prototype[methodName]) ignore.push(methodName); |
|
}); |
|
|
|
if (ignore.length>0) { |
|
NativeArray = NativeArray.without.apply(NativeArray, ignore); |
|
} |
|
|
|
/** |
|
The NativeArray mixin contains the properties needed to to make the native |
|
Array support Ember.MutableArray and all of its dependent APIs. Unless you |
|
have `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Array` set to |
|
false, this will be applied automatically. Otherwise you can apply the mixin |
|
at anytime by calling `Ember.NativeArray.activate`. |
|
|
|
@class NativeArray |
|
@namespace Ember |
|
@uses Ember.MutableArray |
|
@uses Ember.Observable |
|
@uses Ember.Copyable |
|
*/ |
|
Ember.NativeArray = NativeArray; |
|
|
|
/** |
|
Creates an `Ember.NativeArray` from an Array like object. |
|
Does not modify the original object. Ember.A is not needed if |
|
`Ember.EXTEND_PROTOTYPES` is `true` (the default value). However, |
|
it is recommended that you use Ember.A when creating addons for |
|
ember or when you can not guarantee that `Ember.EXTEND_PROTOTYPES` |
|
will be `true`. |
|
|
|
Example |
|
|
|
```js |
|
var Pagination = Ember.CollectionView.extend({ |
|
tagName: 'ul', |
|
classNames: ['pagination'], |
|
init: function() { |
|
this._super(); |
|
if (!this.get('content')) { |
|
this.set('content', Ember.A([])); |
|
} |
|
} |
|
}); |
|
``` |
|
|
|
@method A |
|
@for Ember |
|
@return {Ember.NativeArray} |
|
*/ |
|
Ember.A = function(arr) { |
|
if (arr === undefined) { arr = []; } |
|
return Ember.Array.detect(arr) ? arr : Ember.NativeArray.apply(arr); |
|
}; |
|
|
|
/** |
|
Activates the mixin on the Array.prototype if not already applied. Calling |
|
this method more than once is safe. This will be called when ember is loaded |
|
unless you have `Ember.EXTEND_PROTOTYPES` or `Ember.EXTEND_PROTOTYPES.Array` |
|
set to `false`. |
|
|
|
Example |
|
|
|
```js |
|
if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { |
|
Ember.NativeArray.activate(); |
|
} |
|
``` |
|
|
|
@method activate |
|
@for Ember.NativeArray |
|
@static |
|
@return {void} |
|
*/ |
|
Ember.NativeArray.activate = function() { |
|
NativeArray.apply(Array.prototype); |
|
|
|
Ember.A = function(arr) { return arr || []; }; |
|
}; |
|
|
|
if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Array) { |
|
Ember.NativeArray.activate(); |
|
} |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor, isNone = Ember.isNone, fmt = Ember.String.fmt; |
|
|
|
/** |
|
An unordered collection of objects. |
|
|
|
A Set works a bit like an array except that its items are not ordered. You |
|
can create a set to efficiently test for membership for an object. You can |
|
also iterate through a set just like an array, even accessing objects by |
|
index, however there is no guarantee as to their order. |
|
|
|
All Sets are observable via the Enumerable Observer API - which works |
|
on any enumerable object including both Sets and Arrays. |
|
|
|
## Creating a Set |
|
|
|
You can create a set like you would most objects using |
|
`new Ember.Set()`. Most new sets you create will be empty, but you can |
|
also initialize the set with some content by passing an array or other |
|
enumerable of objects to the constructor. |
|
|
|
Finally, you can pass in an existing set and the set will be copied. You |
|
can also create a copy of a set by calling `Ember.Set#copy()`. |
|
|
|
```javascript |
|
// creates a new empty set |
|
var foundNames = new Ember.Set(); |
|
|
|
// creates a set with four names in it. |
|
var names = new Ember.Set(["Charles", "Tom", "Juan", "Alex"]); // :P |
|
|
|
// creates a copy of the names set. |
|
var namesCopy = new Ember.Set(names); |
|
|
|
// same as above. |
|
var anotherNamesCopy = names.copy(); |
|
``` |
|
|
|
## Adding/Removing Objects |
|
|
|
You generally add or remove objects from a set using `add()` or |
|
`remove()`. You can add any type of object including primitives such as |
|
numbers, strings, and booleans. |
|
|
|
Unlike arrays, objects can only exist one time in a set. If you call `add()` |
|
on a set with the same object multiple times, the object will only be added |
|
once. Likewise, calling `remove()` with the same object multiple times will |
|
remove the object the first time and have no effect on future calls until |
|
you add the object to the set again. |
|
|
|
NOTE: You cannot add/remove `null` or `undefined` to a set. Any attempt to do |
|
so will be ignored. |
|
|
|
In addition to add/remove you can also call `push()`/`pop()`. Push behaves |
|
just like `add()` but `pop()`, unlike `remove()` will pick an arbitrary |
|
object, remove it and return it. This is a good way to use a set as a job |
|
queue when you don't care which order the jobs are executed in. |
|
|
|
## Testing for an Object |
|
|
|
To test for an object's presence in a set you simply call |
|
`Ember.Set#contains()`. |
|
|
|
## Observing changes |
|
|
|
When using `Ember.Set`, you can observe the `"[]"` property to be |
|
alerted whenever the content changes. You can also add an enumerable |
|
observer to the set to be notified of specific objects that are added and |
|
removed from the set. See [Ember.Enumerable](/api/classes/Ember.Enumerable.html) |
|
for more information on enumerables. |
|
|
|
This is often unhelpful. If you are filtering sets of objects, for instance, |
|
it is very inefficient to re-filter all of the items each time the set |
|
changes. It would be better if you could just adjust the filtered set based |
|
on what was changed on the original set. The same issue applies to merging |
|
sets, as well. |
|
|
|
## Other Methods |
|
|
|
`Ember.Set` primary implements other mixin APIs. For a complete reference |
|
on the methods you will use with `Ember.Set`, please consult these mixins. |
|
The most useful ones will be `Ember.Enumerable` and |
|
`Ember.MutableEnumerable` which implement most of the common iterator |
|
methods you are used to on Array. |
|
|
|
Note that you can also use the `Ember.Copyable` and `Ember.Freezable` |
|
APIs on `Ember.Set` as well. Once a set is frozen it can no longer be |
|
modified. The benefit of this is that when you call `frozenCopy()` on it, |
|
Ember will avoid making copies of the set. This allows you to write |
|
code that can know with certainty when the underlying set data will or |
|
will not be modified. |
|
|
|
@class Set |
|
@namespace Ember |
|
@extends Ember.CoreObject |
|
@uses Ember.MutableEnumerable |
|
@uses Ember.Copyable |
|
@uses Ember.Freezable |
|
@since Ember 0.9 |
|
*/ |
|
Ember.Set = Ember.CoreObject.extend(Ember.MutableEnumerable, Ember.Copyable, Ember.Freezable, |
|
{ |
|
|
|
// .......................................................... |
|
// IMPLEMENT ENUMERABLE APIS |
|
// |
|
|
|
/** |
|
This property will change as the number of objects in the set changes. |
|
|
|
@property length |
|
@type number |
|
@default 0 |
|
*/ |
|
length: 0, |
|
|
|
/** |
|
Clears the set. This is useful if you want to reuse an existing set |
|
without having to recreate it. |
|
|
|
```javascript |
|
var colors = new Ember.Set(["red", "green", "blue"]); |
|
colors.length; // 3 |
|
colors.clear(); |
|
colors.length; // 0 |
|
``` |
|
|
|
@method clear |
|
@return {Ember.Set} An empty Set |
|
*/ |
|
clear: function() { |
|
if (this.isFrozen) { throw new Ember.Error(Ember.FROZEN_ERROR); } |
|
|
|
var len = get(this, 'length'); |
|
if (len === 0) { return this; } |
|
|
|
var guid; |
|
|
|
this.enumerableContentWillChange(len, 0); |
|
Ember.propertyWillChange(this, 'firstObject'); |
|
Ember.propertyWillChange(this, 'lastObject'); |
|
|
|
for (var i=0; i < len; i++) { |
|
guid = guidFor(this[i]); |
|
delete this[guid]; |
|
delete this[i]; |
|
} |
|
|
|
set(this, 'length', 0); |
|
|
|
Ember.propertyDidChange(this, 'firstObject'); |
|
Ember.propertyDidChange(this, 'lastObject'); |
|
this.enumerableContentDidChange(len, 0); |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Returns true if the passed object is also an enumerable that contains the |
|
same objects as the receiver. |
|
|
|
```javascript |
|
var colors = ["red", "green", "blue"], |
|
same_colors = new Ember.Set(colors); |
|
|
|
same_colors.isEqual(colors); // true |
|
same_colors.isEqual(["purple", "brown"]); // false |
|
``` |
|
|
|
@method isEqual |
|
@param {Ember.Set} obj the other object. |
|
@return {Boolean} |
|
*/ |
|
isEqual: function(obj) { |
|
// fail fast |
|
if (!Ember.Enumerable.detect(obj)) return false; |
|
|
|
var loc = get(this, 'length'); |
|
if (get(obj, 'length') !== loc) return false; |
|
|
|
while(--loc >= 0) { |
|
if (!obj.contains(this[loc])) return false; |
|
} |
|
|
|
return true; |
|
}, |
|
|
|
/** |
|
Adds an object to the set. Only non-`null` objects can be added to a set |
|
and those can only be added once. If the object is already in the set or |
|
the passed value is null this method will have no effect. |
|
|
|
This is an alias for `Ember.MutableEnumerable.addObject()`. |
|
|
|
```javascript |
|
var colors = new Ember.Set(); |
|
colors.add("blue"); // ["blue"] |
|
colors.add("blue"); // ["blue"] |
|
colors.add("red"); // ["blue", "red"] |
|
colors.add(null); // ["blue", "red"] |
|
colors.add(undefined); // ["blue", "red"] |
|
``` |
|
|
|
@method add |
|
@param {Object} obj The object to add. |
|
@return {Ember.Set} The set itself. |
|
*/ |
|
add: Ember.aliasMethod('addObject'), |
|
|
|
/** |
|
Removes the object from the set if it is found. If you pass a `null` value |
|
or an object that is already not in the set, this method will have no |
|
effect. This is an alias for `Ember.MutableEnumerable.removeObject()`. |
|
|
|
```javascript |
|
var colors = new Ember.Set(["red", "green", "blue"]); |
|
colors.remove("red"); // ["blue", "green"] |
|
colors.remove("purple"); // ["blue", "green"] |
|
colors.remove(null); // ["blue", "green"] |
|
``` |
|
|
|
@method remove |
|
@param {Object} obj The object to remove |
|
@return {Ember.Set} The set itself. |
|
*/ |
|
remove: Ember.aliasMethod('removeObject'), |
|
|
|
/** |
|
Removes the last element from the set and returns it, or `null` if it's empty. |
|
|
|
```javascript |
|
var colors = new Ember.Set(["green", "blue"]); |
|
colors.pop(); // "blue" |
|
colors.pop(); // "green" |
|
colors.pop(); // null |
|
``` |
|
|
|
@method pop |
|
@return {Object} The removed object from the set or null. |
|
*/ |
|
pop: function() { |
|
if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); |
|
var obj = this.length > 0 ? this[this.length-1] : null; |
|
this.remove(obj); |
|
return obj; |
|
}, |
|
|
|
/** |
|
Inserts the given object on to the end of the set. It returns |
|
the set itself. |
|
|
|
This is an alias for `Ember.MutableEnumerable.addObject()`. |
|
|
|
```javascript |
|
var colors = new Ember.Set(); |
|
colors.push("red"); // ["red"] |
|
colors.push("green"); // ["red", "green"] |
|
colors.push("blue"); // ["red", "green", "blue"] |
|
``` |
|
|
|
@method push |
|
@return {Ember.Set} The set itself. |
|
*/ |
|
push: Ember.aliasMethod('addObject'), |
|
|
|
/** |
|
Removes the last element from the set and returns it, or `null` if it's empty. |
|
|
|
This is an alias for `Ember.Set.pop()`. |
|
|
|
```javascript |
|
var colors = new Ember.Set(["green", "blue"]); |
|
colors.shift(); // "blue" |
|
colors.shift(); // "green" |
|
colors.shift(); // null |
|
``` |
|
|
|
@method shift |
|
@return {Object} The removed object from the set or null. |
|
*/ |
|
shift: Ember.aliasMethod('pop'), |
|
|
|
/** |
|
Inserts the given object on to the end of the set. It returns |
|
the set itself. |
|
|
|
This is an alias of `Ember.Set.push()` |
|
|
|
```javascript |
|
var colors = new Ember.Set(); |
|
colors.unshift("red"); // ["red"] |
|
colors.unshift("green"); // ["red", "green"] |
|
colors.unshift("blue"); // ["red", "green", "blue"] |
|
``` |
|
|
|
@method unshift |
|
@return {Ember.Set} The set itself. |
|
*/ |
|
unshift: Ember.aliasMethod('push'), |
|
|
|
/** |
|
Adds each object in the passed enumerable to the set. |
|
|
|
This is an alias of `Ember.MutableEnumerable.addObjects()` |
|
|
|
```javascript |
|
var colors = new Ember.Set(); |
|
colors.addEach(["red", "green", "blue"]); // ["red", "green", "blue"] |
|
``` |
|
|
|
@method addEach |
|
@param {Ember.Enumerable} objects the objects to add. |
|
@return {Ember.Set} The set itself. |
|
*/ |
|
addEach: Ember.aliasMethod('addObjects'), |
|
|
|
/** |
|
Removes each object in the passed enumerable to the set. |
|
|
|
This is an alias of `Ember.MutableEnumerable.removeObjects()` |
|
|
|
```javascript |
|
var colors = new Ember.Set(["red", "green", "blue"]); |
|
colors.removeEach(["red", "blue"]); // ["green"] |
|
``` |
|
|
|
@method removeEach |
|
@param {Ember.Enumerable} objects the objects to remove. |
|
@return {Ember.Set} The set itself. |
|
*/ |
|
removeEach: Ember.aliasMethod('removeObjects'), |
|
|
|
// .......................................................... |
|
// PRIVATE ENUMERABLE SUPPORT |
|
// |
|
|
|
init: function(items) { |
|
this._super(); |
|
if (items) this.addObjects(items); |
|
}, |
|
|
|
// implement Ember.Enumerable |
|
nextObject: function(idx) { |
|
return this[idx]; |
|
}, |
|
|
|
// more optimized version |
|
firstObject: Ember.computed(function() { |
|
return this.length > 0 ? this[0] : undefined; |
|
}), |
|
|
|
// more optimized version |
|
lastObject: Ember.computed(function() { |
|
return this.length > 0 ? this[this.length-1] : undefined; |
|
}), |
|
|
|
// implements Ember.MutableEnumerable |
|
addObject: function(obj) { |
|
if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); |
|
if (isNone(obj)) return this; // nothing to do |
|
|
|
var guid = guidFor(obj), |
|
idx = this[guid], |
|
len = get(this, 'length'), |
|
added ; |
|
|
|
if (idx>=0 && idx<len && (this[idx] === obj)) return this; // added |
|
|
|
added = [obj]; |
|
|
|
this.enumerableContentWillChange(null, added); |
|
Ember.propertyWillChange(this, 'lastObject'); |
|
|
|
len = get(this, 'length'); |
|
this[guid] = len; |
|
this[len] = obj; |
|
set(this, 'length', len+1); |
|
|
|
Ember.propertyDidChange(this, 'lastObject'); |
|
this.enumerableContentDidChange(null, added); |
|
|
|
return this; |
|
}, |
|
|
|
// implements Ember.MutableEnumerable |
|
removeObject: function(obj) { |
|
if (get(this, 'isFrozen')) throw new Ember.Error(Ember.FROZEN_ERROR); |
|
if (isNone(obj)) return this; // nothing to do |
|
|
|
var guid = guidFor(obj), |
|
idx = this[guid], |
|
len = get(this, 'length'), |
|
isFirst = idx === 0, |
|
isLast = idx === len-1, |
|
last, removed; |
|
|
|
|
|
if (idx>=0 && idx<len && (this[idx] === obj)) { |
|
removed = [obj]; |
|
|
|
this.enumerableContentWillChange(removed, null); |
|
if (isFirst) { Ember.propertyWillChange(this, 'firstObject'); } |
|
if (isLast) { Ember.propertyWillChange(this, 'lastObject'); } |
|
|
|
// swap items - basically move the item to the end so it can be removed |
|
if (idx < len-1) { |
|
last = this[len-1]; |
|
this[idx] = last; |
|
this[guidFor(last)] = idx; |
|
} |
|
|
|
delete this[guid]; |
|
delete this[len-1]; |
|
set(this, 'length', len-1); |
|
|
|
if (isFirst) { Ember.propertyDidChange(this, 'firstObject'); } |
|
if (isLast) { Ember.propertyDidChange(this, 'lastObject'); } |
|
this.enumerableContentDidChange(removed, null); |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
// optimized version |
|
contains: function(obj) { |
|
return this[guidFor(obj)]>=0; |
|
}, |
|
|
|
copy: function() { |
|
var C = this.constructor, ret = new C(), loc = get(this, 'length'); |
|
set(ret, 'length', loc); |
|
while(--loc>=0) { |
|
ret[loc] = this[loc]; |
|
ret[guidFor(this[loc])] = loc; |
|
} |
|
return ret; |
|
}, |
|
|
|
toString: function() { |
|
var len = this.length, idx, array = []; |
|
for(idx = 0; idx < len; idx++) { |
|
array[idx] = this[idx]; |
|
} |
|
return fmt("Ember.Set<%@>", [array.join(',')]); |
|
} |
|
|
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var DeferredMixin = Ember.DeferredMixin, // mixins/deferred |
|
get = Ember.get; |
|
|
|
var Deferred = Ember.Object.extend(DeferredMixin); |
|
|
|
Deferred.reopenClass({ |
|
promise: function(callback, binding) { |
|
var deferred = Deferred.create(); |
|
callback.call(binding, deferred); |
|
return deferred; |
|
} |
|
}); |
|
|
|
Ember.Deferred = Deferred; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/*globals CustomEvent */ |
|
|
|
var forEach = Ember.ArrayPolyfills.forEach; |
|
|
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var loadHooks = Ember.ENV.EMBER_LOAD_HOOKS || {}; |
|
var loaded = {}; |
|
|
|
/** |
|
Detects when a specific package of Ember (e.g. 'Ember.Handlebars') |
|
has fully loaded and is available for extension. |
|
|
|
The provided `callback` will be called with the `name` passed |
|
resolved from a string into the object: |
|
|
|
``` javascript |
|
Ember.onLoad('Ember.Handlebars' function(hbars){ |
|
hbars.registerHelper(...); |
|
}); |
|
``` |
|
|
|
@method onLoad |
|
@for Ember |
|
@param name {String} name of hook |
|
@param callback {Function} callback to be called |
|
*/ |
|
Ember.onLoad = function(name, callback) { |
|
var object; |
|
|
|
loadHooks[name] = loadHooks[name] || Ember.A(); |
|
loadHooks[name].pushObject(callback); |
|
|
|
if (object = loaded[name]) { |
|
callback(object); |
|
} |
|
}; |
|
|
|
/** |
|
Called when an Ember.js package (e.g Ember.Handlebars) has finished |
|
loading. Triggers any callbacks registered for this event. |
|
|
|
@method runLoadHooks |
|
@for Ember |
|
@param name {String} name of hook |
|
@param object {Object} object to pass to callbacks |
|
*/ |
|
Ember.runLoadHooks = function(name, object) { |
|
loaded[name] = object; |
|
|
|
if (typeof window === 'object' && typeof window.dispatchEvent === 'function' && typeof CustomEvent === "function") { |
|
var event = new CustomEvent(name, {detail: object, name: name}); |
|
window.dispatchEvent(event); |
|
} |
|
|
|
if (loadHooks[name]) { |
|
forEach.call(loadHooks[name], function(callback) { |
|
callback(object); |
|
}); |
|
} |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var get = Ember.get; |
|
|
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
/** |
|
`Ember.ControllerMixin` provides a standard interface for all classes that |
|
compose Ember's controller layer: `Ember.Controller`, |
|
`Ember.ArrayController`, and `Ember.ObjectController`. |
|
|
|
@class ControllerMixin |
|
@namespace Ember |
|
@uses Ember.ActionHandler |
|
*/ |
|
Ember.ControllerMixin = Ember.Mixin.create(Ember.ActionHandler, { |
|
/* ducktype as a controller */ |
|
isController: true, |
|
|
|
/** |
|
The object to which actions from the view should be sent. |
|
|
|
For example, when a Handlebars template uses the `{{action}}` helper, |
|
it will attempt to send the action to the view's controller's `target`. |
|
|
|
By default, a controller's `target` is set to the router after it is |
|
instantiated by `Ember.Application#initialize`. |
|
|
|
@property target |
|
@default null |
|
*/ |
|
target: null, |
|
|
|
container: null, |
|
|
|
parentController: null, |
|
|
|
store: null, |
|
|
|
model: Ember.computed.alias('content'), |
|
|
|
deprecatedSendHandles: function(actionName) { |
|
return !!this[actionName]; |
|
}, |
|
|
|
deprecatedSend: function(actionName) { |
|
var args = [].slice.call(arguments, 1); |
|
Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); |
|
Ember.deprecate('Action handlers implemented directly on controllers are deprecated in favor of action handlers on an `actions` object ( action: `' + actionName + '` on ' + this + ')', false); |
|
this[actionName].apply(this, args); |
|
return; |
|
} |
|
}); |
|
|
|
/** |
|
@class Controller |
|
@namespace Ember |
|
@extends Ember.Object |
|
@uses Ember.ControllerMixin |
|
*/ |
|
Ember.Controller = Ember.Object.extend(Ember.ControllerMixin); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach; |
|
|
|
/** |
|
`Ember.SortableMixin` provides a standard interface for array proxies |
|
to specify a sort order and maintain this sorting when objects are added, |
|
removed, or updated without changing the implicit order of their underlying |
|
content array: |
|
|
|
```javascript |
|
songs = [ |
|
{trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'}, |
|
{trackNumber: 2, title: 'Back in the U.S.S.R.'}, |
|
{trackNumber: 3, title: 'Glass Onion'}, |
|
]; |
|
|
|
songsController = Ember.ArrayController.create({ |
|
content: songs, |
|
sortProperties: ['trackNumber'], |
|
sortAscending: true |
|
}); |
|
|
|
songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} |
|
|
|
songsController.addObject({trackNumber: 1, title: 'Dear Prudence'}); |
|
songsController.get('firstObject'); // {trackNumber: 1, title: 'Dear Prudence'} |
|
``` |
|
|
|
If you add or remove the properties to sort by or change the sort direction the content |
|
sort order will be automatically updated. |
|
|
|
```javascript |
|
songsController.set('sortProperties', ['title']); |
|
songsController.get('firstObject'); // {trackNumber: 2, title: 'Back in the U.S.S.R.'} |
|
|
|
songsController.toggleProperty('sortAscending'); |
|
songsController.get('firstObject'); // {trackNumber: 4, title: 'Ob-La-Di, Ob-La-Da'} |
|
``` |
|
|
|
SortableMixin works by sorting the arrangedContent array, which is the array that |
|
arrayProxy displays. Due to the fact that the underlying 'content' array is not changed, that |
|
array will not display the sorted list: |
|
|
|
```javascript |
|
songsController.get('content').get('firstObject'); // Returns the unsorted original content |
|
songsController.get('firstObject'); // Returns the sorted content. |
|
``` |
|
|
|
Although the sorted content can also be accessed through the arrangedContent property, |
|
it is preferable to use the proxied class and not the arrangedContent array directly. |
|
|
|
@class SortableMixin |
|
@namespace Ember |
|
@uses Ember.MutableEnumerable |
|
*/ |
|
Ember.SortableMixin = Ember.Mixin.create(Ember.MutableEnumerable, { |
|
|
|
/** |
|
Specifies which properties dictate the arrangedContent's sort order. |
|
|
|
When specifying multiple properties the sorting will use properties |
|
from the `sortProperties` array prioritized from first to last. |
|
|
|
@property {Array} sortProperties |
|
*/ |
|
sortProperties: null, |
|
|
|
/** |
|
Specifies the arrangedContent's sort direction |
|
|
|
@property {Boolean} sortAscending |
|
*/ |
|
sortAscending: true, |
|
|
|
/** |
|
The function used to compare two values. You can override this if you |
|
want to do custom comparisons. Functions must be of the type expected by |
|
Array#sort, i.e. |
|
return 0 if the two parameters are equal, |
|
return a negative value if the first parameter is smaller than the second or |
|
return a positive value otherwise: |
|
|
|
```javascript |
|
function(x,y) { // These are assumed to be integers |
|
if (x === y) |
|
return 0; |
|
return x < y ? -1 : 1; |
|
} |
|
``` |
|
|
|
@property sortFunction |
|
@type {Function} |
|
@default Ember.compare |
|
*/ |
|
sortFunction: Ember.compare, |
|
|
|
orderBy: function(item1, item2) { |
|
var result = 0, |
|
sortProperties = get(this, 'sortProperties'), |
|
sortAscending = get(this, 'sortAscending'), |
|
sortFunction = get(this, 'sortFunction'); |
|
|
|
Ember.assert("you need to define `sortProperties`", !!sortProperties); |
|
|
|
forEach(sortProperties, function(propertyName) { |
|
if (result === 0) { |
|
result = sortFunction(get(item1, propertyName), get(item2, propertyName)); |
|
if ((result !== 0) && !sortAscending) { |
|
result = (-1) * result; |
|
} |
|
} |
|
}); |
|
|
|
return result; |
|
}, |
|
|
|
destroy: function() { |
|
var content = get(this, 'content'), |
|
sortProperties = get(this, 'sortProperties'); |
|
|
|
if (content && sortProperties) { |
|
forEach(content, function(item) { |
|
forEach(sortProperties, function(sortProperty) { |
|
Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); |
|
}, this); |
|
}, this); |
|
} |
|
|
|
return this._super(); |
|
}, |
|
|
|
isSorted: Ember.computed.bool('sortProperties'), |
|
|
|
/** |
|
Overrides the default arrangedContent from arrayProxy in order to sort by sortFunction. |
|
Also sets up observers for each sortProperty on each item in the content Array. |
|
|
|
@property arrangedContent |
|
*/ |
|
|
|
arrangedContent: Ember.computed('content', 'sortProperties.@each', function(key, value) { |
|
var content = get(this, 'content'), |
|
isSorted = get(this, 'isSorted'), |
|
sortProperties = get(this, 'sortProperties'), |
|
self = this; |
|
|
|
if (content && isSorted) { |
|
content = content.slice(); |
|
content.sort(function(item1, item2) { |
|
return self.orderBy(item1, item2); |
|
}); |
|
forEach(content, function(item) { |
|
forEach(sortProperties, function(sortProperty) { |
|
Ember.addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); |
|
}, this); |
|
}, this); |
|
return Ember.A(content); |
|
} |
|
|
|
return content; |
|
}), |
|
|
|
_contentWillChange: Ember.beforeObserver('content', function() { |
|
var content = get(this, 'content'), |
|
sortProperties = get(this, 'sortProperties'); |
|
|
|
if (content && sortProperties) { |
|
forEach(content, function(item) { |
|
forEach(sortProperties, function(sortProperty) { |
|
Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); |
|
}, this); |
|
}, this); |
|
} |
|
|
|
this._super(); |
|
}), |
|
|
|
sortAscendingWillChange: Ember.beforeObserver('sortAscending', function() { |
|
this._lastSortAscending = get(this, 'sortAscending'); |
|
}), |
|
|
|
sortAscendingDidChange: Ember.observer('sortAscending', function() { |
|
if (get(this, 'sortAscending') !== this._lastSortAscending) { |
|
var arrangedContent = get(this, 'arrangedContent'); |
|
arrangedContent.reverseObjects(); |
|
} |
|
}), |
|
|
|
contentArrayWillChange: function(array, idx, removedCount, addedCount) { |
|
var isSorted = get(this, 'isSorted'); |
|
|
|
if (isSorted) { |
|
var arrangedContent = get(this, 'arrangedContent'); |
|
var removedObjects = array.slice(idx, idx+removedCount); |
|
var sortProperties = get(this, 'sortProperties'); |
|
|
|
forEach(removedObjects, function(item) { |
|
arrangedContent.removeObject(item); |
|
|
|
forEach(sortProperties, function(sortProperty) { |
|
Ember.removeObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); |
|
}, this); |
|
}, this); |
|
} |
|
|
|
return this._super(array, idx, removedCount, addedCount); |
|
}, |
|
|
|
contentArrayDidChange: function(array, idx, removedCount, addedCount) { |
|
var isSorted = get(this, 'isSorted'), |
|
sortProperties = get(this, 'sortProperties'); |
|
|
|
if (isSorted) { |
|
var addedObjects = array.slice(idx, idx+addedCount); |
|
|
|
forEach(addedObjects, function(item) { |
|
this.insertItemSorted(item); |
|
|
|
forEach(sortProperties, function(sortProperty) { |
|
Ember.addObserver(item, sortProperty, this, 'contentItemSortPropertyDidChange'); |
|
}, this); |
|
}, this); |
|
} |
|
|
|
return this._super(array, idx, removedCount, addedCount); |
|
}, |
|
|
|
insertItemSorted: function(item) { |
|
var arrangedContent = get(this, 'arrangedContent'); |
|
var length = get(arrangedContent, 'length'); |
|
|
|
var idx = this._binarySearch(item, 0, length); |
|
arrangedContent.insertAt(idx, item); |
|
}, |
|
|
|
contentItemSortPropertyDidChange: function(item) { |
|
var arrangedContent = get(this, 'arrangedContent'), |
|
oldIndex = arrangedContent.indexOf(item), |
|
leftItem = arrangedContent.objectAt(oldIndex - 1), |
|
rightItem = arrangedContent.objectAt(oldIndex + 1), |
|
leftResult = leftItem && this.orderBy(item, leftItem), |
|
rightResult = rightItem && this.orderBy(item, rightItem); |
|
|
|
if (leftResult < 0 || rightResult > 0) { |
|
arrangedContent.removeObject(item); |
|
this.insertItemSorted(item); |
|
} |
|
}, |
|
|
|
_binarySearch: function(item, low, high) { |
|
var mid, midItem, res, arrangedContent; |
|
|
|
if (low === high) { |
|
return low; |
|
} |
|
|
|
arrangedContent = get(this, 'arrangedContent'); |
|
|
|
mid = low + Math.floor((high - low) / 2); |
|
midItem = arrangedContent.objectAt(mid); |
|
|
|
res = this.orderBy(midItem, item); |
|
|
|
if (res < 0) { |
|
return this._binarySearch(item, mid+1, high); |
|
} else if (res > 0) { |
|
return this._binarySearch(item, low, mid); |
|
} |
|
|
|
return mid; |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set, forEach = Ember.EnumerableUtils.forEach, |
|
replace = Ember.EnumerableUtils.replace; |
|
|
|
/** |
|
`Ember.ArrayController` provides a way for you to publish a collection of |
|
objects so that you can easily bind to the collection from a Handlebars |
|
`#each` helper, an `Ember.CollectionView`, or other controllers. |
|
|
|
The advantage of using an `ArrayController` is that you only have to set up |
|
your view bindings once; to change what's displayed, simply swap out the |
|
`content` property on the controller. |
|
|
|
For example, imagine you wanted to display a list of items fetched via an XHR |
|
request. Create an `Ember.ArrayController` and set its `content` property: |
|
|
|
```javascript |
|
MyApp.listController = Ember.ArrayController.create(); |
|
|
|
$.get('people.json', function(data) { |
|
MyApp.listController.set('content', data); |
|
}); |
|
``` |
|
|
|
Then, create a view that binds to your new controller: |
|
|
|
```handlebars |
|
{{#each MyApp.listController}} |
|
{{firstName}} {{lastName}} |
|
{{/each}} |
|
``` |
|
|
|
Although you are binding to the controller, the behavior of this controller |
|
is to pass through any methods or properties to the underlying array. This |
|
capability comes from `Ember.ArrayProxy`, which this class inherits from. |
|
|
|
Sometimes you want to display computed properties within the body of an |
|
`#each` helper that depend on the underlying items in `content`, but are not |
|
present on those items. To do this, set `itemController` to the name of a |
|
controller (probably an `ObjectController`) that will wrap each individual item. |
|
|
|
For example: |
|
|
|
```handlebars |
|
{{#each post in controller}} |
|
<li>{{title}} ({{titleLength}} characters)</li> |
|
{{/each}} |
|
``` |
|
|
|
```javascript |
|
App.PostsController = Ember.ArrayController.extend({ |
|
itemController: 'post' |
|
}); |
|
|
|
App.PostController = Ember.ObjectController.extend({ |
|
// the `title` property will be proxied to the underlying post. |
|
|
|
titleLength: function() { |
|
return this.get('title').length; |
|
}.property('title') |
|
}); |
|
``` |
|
|
|
In some cases it is helpful to return a different `itemController` depending |
|
on the particular item. Subclasses can do this by overriding |
|
`lookupItemController`. |
|
|
|
For example: |
|
|
|
```javascript |
|
App.MyArrayController = Ember.ArrayController.extend({ |
|
lookupItemController: function( object ) { |
|
if (object.get('isSpecial')) { |
|
return "special"; // use App.SpecialController |
|
} else { |
|
return "regular"; // use App.RegularController |
|
} |
|
} |
|
}); |
|
``` |
|
|
|
The itemController instances will have a `parentController` property set to |
|
the `ArrayController` instance. |
|
|
|
@class ArrayController |
|
@namespace Ember |
|
@extends Ember.ArrayProxy |
|
@uses Ember.SortableMixin |
|
@uses Ember.ControllerMixin |
|
*/ |
|
|
|
Ember.ArrayController = Ember.ArrayProxy.extend(Ember.ControllerMixin, |
|
Ember.SortableMixin, { |
|
|
|
/** |
|
The controller used to wrap items, if any. |
|
|
|
@property itemController |
|
@type String |
|
@default null |
|
*/ |
|
itemController: null, |
|
|
|
/** |
|
Return the name of the controller to wrap items, or `null` if items should |
|
be returned directly. The default implementation simply returns the |
|
`itemController` property, but subclasses can override this method to return |
|
different controllers for different objects. |
|
|
|
For example: |
|
|
|
```javascript |
|
App.MyArrayController = Ember.ArrayController.extend({ |
|
lookupItemController: function( object ) { |
|
if (object.get('isSpecial')) { |
|
return "special"; // use App.SpecialController |
|
} else { |
|
return "regular"; // use App.RegularController |
|
} |
|
} |
|
}); |
|
``` |
|
|
|
@method lookupItemController |
|
@param {Object} object |
|
@return {String} |
|
*/ |
|
lookupItemController: function(object) { |
|
return get(this, 'itemController'); |
|
}, |
|
|
|
objectAtContent: function(idx) { |
|
var length = get(this, 'length'), |
|
arrangedContent = get(this,'arrangedContent'), |
|
object = arrangedContent && arrangedContent.objectAt(idx); |
|
|
|
if (idx >= 0 && idx < length) { |
|
var controllerClass = this.lookupItemController(object); |
|
if (controllerClass) { |
|
return this.controllerAt(idx, object, controllerClass); |
|
} |
|
} |
|
|
|
// When `controllerClass` is falsy, we have not opted in to using item |
|
// controllers, so return the object directly. |
|
|
|
// When the index is out of range, we want to return the "out of range" |
|
// value, whatever that might be. Rather than make assumptions |
|
// (e.g. guessing `null` or `undefined`) we defer this to `arrangedContent`. |
|
return object; |
|
}, |
|
|
|
arrangedContentDidChange: function() { |
|
this._super(); |
|
this._resetSubControllers(); |
|
}, |
|
|
|
arrayContentDidChange: function(idx, removedCnt, addedCnt) { |
|
var subControllers = get(this, '_subControllers'), |
|
subControllersToRemove = subControllers.slice(idx, idx+removedCnt); |
|
|
|
forEach(subControllersToRemove, function(subController) { |
|
if (subController) { subController.destroy(); } |
|
}); |
|
|
|
replace(subControllers, idx, removedCnt, new Array(addedCnt)); |
|
|
|
// The shadow array of subcontrollers must be updated before we trigger |
|
// observers, otherwise observers will get the wrong subcontainer when |
|
// calling `objectAt` |
|
this._super(idx, removedCnt, addedCnt); |
|
}, |
|
|
|
init: function() { |
|
this._super(); |
|
|
|
this.set('_subControllers', Ember.A()); |
|
}, |
|
|
|
content: Ember.computed(function () { |
|
return Ember.A(); |
|
}), |
|
|
|
/** |
|
* Flag to mark as being "virtual". Used to keep this instance |
|
* from participating in the parentController hierarchy. |
|
* |
|
* @private |
|
* @type Boolean |
|
*/ |
|
_isVirtual: false, |
|
|
|
controllerAt: function(idx, object, controllerClass) { |
|
var container = get(this, 'container'), |
|
subControllers = get(this, '_subControllers'), |
|
subController = subControllers[idx], |
|
fullName; |
|
|
|
if (subController) { return subController; } |
|
|
|
fullName = "controller:" + controllerClass; |
|
|
|
if (!container.has(fullName)) { |
|
throw new Ember.Error('Could not resolve itemController: "' + controllerClass + '"'); |
|
} |
|
var parentController; |
|
if (this._isVirtual) { |
|
parentController = get(this, 'parentController'); |
|
} |
|
parentController = parentController || this; |
|
subController = container.lookupFactory(fullName).create({ |
|
target: this, |
|
parentController: parentController, |
|
content: object |
|
}); |
|
|
|
subControllers[idx] = subController; |
|
|
|
return subController; |
|
}, |
|
|
|
_subControllers: null, |
|
|
|
_resetSubControllers: function() { |
|
var subControllers = get(this, '_subControllers'); |
|
if (subControllers) { |
|
forEach(subControllers, function(subController) { |
|
if (subController) { subController.destroy(); } |
|
}); |
|
} |
|
|
|
this.set('_subControllers', Ember.A()); |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-runtime |
|
*/ |
|
|
|
/** |
|
`Ember.ObjectController` is part of Ember's Controller layer. It is intended |
|
to wrap a single object, proxying unhandled attempts to `get` and `set` to the underlying |
|
content object, and to forward unhandled action attempts to its `target`. |
|
|
|
`Ember.ObjectController` derives this functionality from its superclass |
|
`Ember.ObjectProxy` and the `Ember.ControllerMixin` mixin. |
|
|
|
@class ObjectController |
|
@namespace Ember |
|
@extends Ember.ObjectProxy |
|
@uses Ember.ControllerMixin |
|
**/ |
|
Ember.ObjectController = Ember.ObjectProxy.extend(Ember.ControllerMixin); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
Ember Runtime |
|
|
|
@module ember |
|
@submodule ember-runtime |
|
@requires ember-metal |
|
*/ |
|
|
|
})(); |
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var jQuery = (Ember.imports && Ember.imports.jQuery) || (this && this.jQuery); |
|
if (!jQuery && typeof require === 'function') { |
|
jQuery = require('jquery'); |
|
} |
|
|
|
Ember.assert("Ember Views require jQuery between 1.7 and 2.1", jQuery && (jQuery().jquery.match(/^((1\.(7|8|9|10|11))|(2\.(0|1)))(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); |
|
|
|
/** |
|
Alias for jQuery |
|
|
|
@method $ |
|
@for Ember |
|
*/ |
|
Ember.$ = jQuery; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
if (Ember.$) { |
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#dndevents |
|
var dragEvents = Ember.String.w('dragstart drag dragenter dragleave dragover drop dragend'); |
|
|
|
// Copies the `dataTransfer` property from a browser event object onto the |
|
// jQuery event object for the specified events |
|
Ember.EnumerableUtils.forEach(dragEvents, function(eventName) { |
|
Ember.$.event.fixHooks[eventName] = { props: ['dataTransfer'] }; |
|
}); |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
/* BEGIN METAMORPH HELPERS */ |
|
|
|
// Internet Explorer prior to 9 does not allow setting innerHTML if the first element |
|
// is a "zero-scope" element. This problem can be worked around by making |
|
// the first node an invisible text node. We, like Modernizr, use ­ |
|
|
|
var needsShy = typeof document !== 'undefined' && (function() { |
|
var testEl = document.createElement('div'); |
|
testEl.innerHTML = "<div></div>"; |
|
testEl.firstChild.innerHTML = "<script></script>"; |
|
return testEl.firstChild.innerHTML === ''; |
|
})(); |
|
|
|
// IE 8 (and likely earlier) likes to move whitespace preceeding |
|
// a script tag to appear after it. This means that we can |
|
// accidentally remove whitespace when updating a morph. |
|
var movesWhitespace = typeof document !== 'undefined' && (function() { |
|
var testEl = document.createElement('div'); |
|
testEl.innerHTML = "Test: <script type='text/x-placeholder'></script>Value"; |
|
return testEl.childNodes[0].nodeValue === 'Test:' && |
|
testEl.childNodes[2].nodeValue === ' Value'; |
|
})(); |
|
|
|
// Use this to find children by ID instead of using jQuery |
|
var findChildById = function(element, id) { |
|
if (element.getAttribute('id') === id) { return element; } |
|
|
|
var len = element.childNodes.length, idx, node, found; |
|
for (idx=0; idx<len; idx++) { |
|
node = element.childNodes[idx]; |
|
found = node.nodeType === 1 && findChildById(node, id); |
|
if (found) { return found; } |
|
} |
|
}; |
|
|
|
var setInnerHTMLWithoutFix = function(element, html) { |
|
if (needsShy) { |
|
html = '­' + html; |
|
} |
|
|
|
var matches = []; |
|
if (movesWhitespace) { |
|
// Right now we only check for script tags with ids with the |
|
// goal of targeting morphs. |
|
html = html.replace(/(\s+)(<script id='([^']+)')/g, function(match, spaces, tag, id) { |
|
matches.push([id, spaces]); |
|
return tag; |
|
}); |
|
} |
|
|
|
element.innerHTML = html; |
|
|
|
// If we have to do any whitespace adjustments do them now |
|
if (matches.length > 0) { |
|
var len = matches.length, idx; |
|
for (idx=0; idx<len; idx++) { |
|
var script = findChildById(element, matches[idx][0]), |
|
node = document.createTextNode(matches[idx][1]); |
|
script.parentNode.insertBefore(node, script); |
|
} |
|
} |
|
|
|
if (needsShy) { |
|
var shyElement = element.firstChild; |
|
while (shyElement.nodeType === 1 && !shyElement.nodeName) { |
|
shyElement = shyElement.firstChild; |
|
} |
|
if (shyElement.nodeType === 3 && shyElement.nodeValue.charAt(0) === "\u00AD") { |
|
shyElement.nodeValue = shyElement.nodeValue.slice(1); |
|
} |
|
} |
|
}; |
|
|
|
/* END METAMORPH HELPERS */ |
|
|
|
|
|
var innerHTMLTags = {}; |
|
var canSetInnerHTML = function(tagName) { |
|
if (innerHTMLTags[tagName] !== undefined) { |
|
return innerHTMLTags[tagName]; |
|
} |
|
|
|
var canSet = true; |
|
|
|
// IE 8 and earlier don't allow us to do innerHTML on select |
|
if (tagName.toLowerCase() === 'select') { |
|
var el = document.createElement('select'); |
|
setInnerHTMLWithoutFix(el, '<option value="test">Test</option>'); |
|
canSet = el.options.length === 1; |
|
} |
|
|
|
innerHTMLTags[tagName] = canSet; |
|
|
|
return canSet; |
|
}; |
|
|
|
var setInnerHTML = function(element, html) { |
|
var tagName = element.tagName; |
|
|
|
if (canSetInnerHTML(tagName)) { |
|
setInnerHTMLWithoutFix(element, html); |
|
} else { |
|
// Firefox versions < 11 do not have support for element.outerHTML. |
|
var outerHTML = element.outerHTML || new XMLSerializer().serializeToString(element); |
|
Ember.assert("Can't set innerHTML on "+element.tagName+" in this browser", outerHTML); |
|
|
|
var startTag = outerHTML.match(new RegExp("<"+tagName+"([^>]*)>", 'i'))[0], |
|
endTag = '</'+tagName+'>'; |
|
|
|
var wrapper = document.createElement('div'); |
|
setInnerHTMLWithoutFix(wrapper, startTag + html + endTag); |
|
element = wrapper.firstChild; |
|
while (element.tagName !== tagName) { |
|
element = element.nextSibling; |
|
} |
|
} |
|
|
|
return element; |
|
}; |
|
|
|
function isSimpleClick(event) { |
|
var modifier = event.shiftKey || event.metaKey || event.altKey || event.ctrlKey, |
|
secondaryClick = event.which > 1; // IE9 may return undefined |
|
|
|
return !modifier && !secondaryClick; |
|
} |
|
|
|
Ember.ViewUtils = { |
|
setInnerHTML: setInnerHTML, |
|
isSimpleClick: isSimpleClick |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
var ClassSet = function() { |
|
this.seen = {}; |
|
this.list = []; |
|
}; |
|
|
|
ClassSet.prototype = { |
|
add: function(string) { |
|
if (string in this.seen) { return; } |
|
this.seen[string] = true; |
|
|
|
this.list.push(string); |
|
}, |
|
|
|
toDOM: function() { |
|
return this.list.join(" "); |
|
} |
|
}; |
|
|
|
var BAD_TAG_NAME_TEST_REGEXP = /[^a-zA-Z0-9\-]/; |
|
var BAD_TAG_NAME_REPLACE_REGEXP = /[^a-zA-Z0-9\-]/g; |
|
|
|
function stripTagName(tagName) { |
|
if (!tagName) { |
|
return tagName; |
|
} |
|
|
|
if (!BAD_TAG_NAME_TEST_REGEXP.test(tagName)) { |
|
return tagName; |
|
} |
|
|
|
return tagName.replace(BAD_TAG_NAME_REPLACE_REGEXP, ''); |
|
} |
|
|
|
var BAD_CHARS_REGEXP = /&(?!\w+;)|[<>"'`]/g; |
|
var POSSIBLE_CHARS_REGEXP = /[&<>"'`]/; |
|
|
|
function escapeAttribute(value) { |
|
// Stolen shamelessly from Handlebars |
|
|
|
var escape = { |
|
"<": "<", |
|
">": ">", |
|
'"': """, |
|
"'": "'", |
|
"`": "`" |
|
}; |
|
|
|
var escapeChar = function(chr) { |
|
return escape[chr] || "&"; |
|
}; |
|
|
|
var string = value.toString(); |
|
|
|
if(!POSSIBLE_CHARS_REGEXP.test(string)) { return string; } |
|
return string.replace(BAD_CHARS_REGEXP, escapeChar); |
|
} |
|
|
|
// IE 6/7 have bugs around setting names on inputs during creation. |
|
// From http://msdn.microsoft.com/en-us/library/ie/ms536389(v=vs.85).aspx: |
|
// "To include the NAME attribute at run time on objects created with the createElement method, use the eTag." |
|
var canSetNameOnInputs = (function() { |
|
var div = document.createElement('div'), |
|
el = document.createElement('input'); |
|
|
|
el.setAttribute('name', 'foo'); |
|
div.appendChild(el); |
|
|
|
return !!div.innerHTML.match('foo'); |
|
})(); |
|
|
|
/** |
|
`Ember.RenderBuffer` gathers information regarding the a view and generates the |
|
final representation. `Ember.RenderBuffer` will generate HTML which can be pushed |
|
to the DOM. |
|
|
|
```javascript |
|
var buffer = Ember.RenderBuffer('div'); |
|
``` |
|
|
|
@class RenderBuffer |
|
@namespace Ember |
|
@constructor |
|
@param {String} tagName tag name (such as 'div' or 'p') used for the buffer |
|
*/ |
|
Ember.RenderBuffer = function(tagName) { |
|
return new Ember._RenderBuffer(tagName); |
|
}; |
|
|
|
Ember._RenderBuffer = function(tagName) { |
|
this.tagNames = [tagName || null]; |
|
this.buffer = ""; |
|
}; |
|
|
|
Ember._RenderBuffer.prototype = { |
|
|
|
// The root view's element |
|
_element: null, |
|
|
|
_hasElement: true, |
|
|
|
/** |
|
An internal set used to de-dupe class names when `addClass()` is |
|
used. After each call to `addClass()`, the `classes` property |
|
will be updated. |
|
|
|
@private |
|
@property elementClasses |
|
@type Array |
|
@default [] |
|
*/ |
|
elementClasses: null, |
|
|
|
/** |
|
Array of class names which will be applied in the class attribute. |
|
|
|
You can use `setClasses()` to set this property directly. If you |
|
use `addClass()`, it will be maintained for you. |
|
|
|
@property classes |
|
@type Array |
|
@default [] |
|
*/ |
|
classes: null, |
|
|
|
/** |
|
The id in of the element, to be applied in the id attribute. |
|
|
|
You should not set this property yourself, rather, you should use |
|
the `id()` method of `Ember.RenderBuffer`. |
|
|
|
@property elementId |
|
@type String |
|
@default null |
|
*/ |
|
elementId: null, |
|
|
|
/** |
|
A hash keyed on the name of the attribute and whose value will be |
|
applied to that attribute. For example, if you wanted to apply a |
|
`data-view="Foo.bar"` property to an element, you would set the |
|
elementAttributes hash to `{'data-view':'Foo.bar'}`. |
|
|
|
You should not maintain this hash yourself, rather, you should use |
|
the `attr()` method of `Ember.RenderBuffer`. |
|
|
|
@property elementAttributes |
|
@type Hash |
|
@default {} |
|
*/ |
|
elementAttributes: null, |
|
|
|
/** |
|
A hash keyed on the name of the properties and whose value will be |
|
applied to that property. For example, if you wanted to apply a |
|
`checked=true` property to an element, you would set the |
|
elementProperties hash to `{'checked':true}`. |
|
|
|
You should not maintain this hash yourself, rather, you should use |
|
the `prop()` method of `Ember.RenderBuffer`. |
|
|
|
@property elementProperties |
|
@type Hash |
|
@default {} |
|
*/ |
|
elementProperties: null, |
|
|
|
/** |
|
The tagname of the element an instance of `Ember.RenderBuffer` represents. |
|
|
|
Usually, this gets set as the first parameter to `Ember.RenderBuffer`. For |
|
example, if you wanted to create a `p` tag, then you would call |
|
|
|
```javascript |
|
Ember.RenderBuffer('p') |
|
``` |
|
|
|
@property elementTag |
|
@type String |
|
@default null |
|
*/ |
|
elementTag: null, |
|
|
|
/** |
|
A hash keyed on the name of the style attribute and whose value will |
|
be applied to that attribute. For example, if you wanted to apply a |
|
`background-color:black;` style to an element, you would set the |
|
elementStyle hash to `{'background-color':'black'}`. |
|
|
|
You should not maintain this hash yourself, rather, you should use |
|
the `style()` method of `Ember.RenderBuffer`. |
|
|
|
@property elementStyle |
|
@type Hash |
|
@default {} |
|
*/ |
|
elementStyle: null, |
|
|
|
/** |
|
Nested `RenderBuffers` will set this to their parent `RenderBuffer` |
|
instance. |
|
|
|
@property parentBuffer |
|
@type Ember._RenderBuffer |
|
*/ |
|
parentBuffer: null, |
|
|
|
/** |
|
Adds a string of HTML to the `RenderBuffer`. |
|
|
|
@method push |
|
@param {String} string HTML to push into the buffer |
|
@chainable |
|
*/ |
|
push: function(string) { |
|
this.buffer += string; |
|
return this; |
|
}, |
|
|
|
/** |
|
Adds a class to the buffer, which will be rendered to the class attribute. |
|
|
|
@method addClass |
|
@param {String} className Class name to add to the buffer |
|
@chainable |
|
*/ |
|
addClass: function(className) { |
|
// lazily create elementClasses |
|
this.elementClasses = (this.elementClasses || new ClassSet()); |
|
this.elementClasses.add(className); |
|
this.classes = this.elementClasses.list; |
|
|
|
return this; |
|
}, |
|
|
|
setClasses: function(classNames) { |
|
this.elementClasses = null; |
|
var len = classNames.length, i; |
|
for (i = 0; i < len; i++) { |
|
this.addClass(classNames[i]); |
|
} |
|
}, |
|
|
|
/** |
|
Sets the elementID to be used for the element. |
|
|
|
@method id |
|
@param {String} id |
|
@chainable |
|
*/ |
|
id: function(id) { |
|
this.elementId = id; |
|
return this; |
|
}, |
|
|
|
// duck type attribute functionality like jQuery so a render buffer |
|
// can be used like a jQuery object in attribute binding scenarios. |
|
|
|
/** |
|
Adds an attribute which will be rendered to the element. |
|
|
|
@method attr |
|
@param {String} name The name of the attribute |
|
@param {String} value The value to add to the attribute |
|
@chainable |
|
@return {Ember.RenderBuffer|String} this or the current attribute value |
|
*/ |
|
attr: function(name, value) { |
|
var attributes = this.elementAttributes = (this.elementAttributes || {}); |
|
|
|
if (arguments.length === 1) { |
|
return attributes[name]; |
|
} else { |
|
attributes[name] = value; |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Remove an attribute from the list of attributes to render. |
|
|
|
@method removeAttr |
|
@param {String} name The name of the attribute |
|
@chainable |
|
*/ |
|
removeAttr: function(name) { |
|
var attributes = this.elementAttributes; |
|
if (attributes) { delete attributes[name]; } |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Adds a property which will be rendered to the element. |
|
|
|
@method prop |
|
@param {String} name The name of the property |
|
@param {String} value The value to add to the property |
|
@chainable |
|
@return {Ember.RenderBuffer|String} this or the current property value |
|
*/ |
|
prop: function(name, value) { |
|
var properties = this.elementProperties = (this.elementProperties || {}); |
|
|
|
if (arguments.length === 1) { |
|
return properties[name]; |
|
} else { |
|
properties[name] = value; |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Remove an property from the list of properties to render. |
|
|
|
@method removeProp |
|
@param {String} name The name of the property |
|
@chainable |
|
*/ |
|
removeProp: function(name) { |
|
var properties = this.elementProperties; |
|
if (properties) { delete properties[name]; } |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Adds a style to the style attribute which will be rendered to the element. |
|
|
|
@method style |
|
@param {String} name Name of the style |
|
@param {String} value |
|
@chainable |
|
*/ |
|
style: function(name, value) { |
|
this.elementStyle = (this.elementStyle || {}); |
|
|
|
this.elementStyle[name] = value; |
|
return this; |
|
}, |
|
|
|
begin: function(tagName) { |
|
this.tagNames.push(tagName || null); |
|
return this; |
|
}, |
|
|
|
pushOpeningTag: function() { |
|
var tagName = this.currentTagName(); |
|
if (!tagName) { return; } |
|
|
|
if (this._hasElement && !this._element && this.buffer.length === 0) { |
|
this._element = this.generateElement(); |
|
return; |
|
} |
|
|
|
var buffer = this.buffer, |
|
id = this.elementId, |
|
classes = this.classes, |
|
attrs = this.elementAttributes, |
|
props = this.elementProperties, |
|
style = this.elementStyle, |
|
attr, prop; |
|
|
|
buffer += '<' + stripTagName(tagName); |
|
|
|
if (id) { |
|
buffer += ' id="' + escapeAttribute(id) + '"'; |
|
this.elementId = null; |
|
} |
|
if (classes) { |
|
buffer += ' class="' + escapeAttribute(classes.join(' ')) + '"'; |
|
this.classes = null; |
|
this.elementClasses = null; |
|
} |
|
|
|
if (style) { |
|
buffer += ' style="'; |
|
|
|
for (prop in style) { |
|
if (style.hasOwnProperty(prop)) { |
|
buffer += prop + ':' + escapeAttribute(style[prop]) + ';'; |
|
} |
|
} |
|
|
|
buffer += '"'; |
|
|
|
this.elementStyle = null; |
|
} |
|
|
|
if (attrs) { |
|
for (attr in attrs) { |
|
if (attrs.hasOwnProperty(attr)) { |
|
buffer += ' ' + attr + '="' + escapeAttribute(attrs[attr]) + '"'; |
|
} |
|
} |
|
|
|
this.elementAttributes = null; |
|
} |
|
|
|
if (props) { |
|
for (prop in props) { |
|
if (props.hasOwnProperty(prop)) { |
|
var value = props[prop]; |
|
if (value || typeof(value) === 'number') { |
|
if (value === true) { |
|
buffer += ' ' + prop + '="' + prop + '"'; |
|
} else { |
|
buffer += ' ' + prop + '="' + escapeAttribute(props[prop]) + '"'; |
|
} |
|
} |
|
} |
|
} |
|
|
|
this.elementProperties = null; |
|
} |
|
|
|
buffer += '>'; |
|
this.buffer = buffer; |
|
}, |
|
|
|
pushClosingTag: function() { |
|
var tagName = this.tagNames.pop(); |
|
if (tagName) { this.buffer += '</' + stripTagName(tagName) + '>'; } |
|
}, |
|
|
|
currentTagName: function() { |
|
return this.tagNames[this.tagNames.length-1]; |
|
}, |
|
|
|
generateElement: function() { |
|
var tagName = this.tagNames.pop(), // pop since we don't need to close |
|
id = this.elementId, |
|
classes = this.classes, |
|
attrs = this.elementAttributes, |
|
props = this.elementProperties, |
|
style = this.elementStyle, |
|
styleBuffer = '', attr, prop, tagString; |
|
|
|
if (attrs && attrs.name && !canSetNameOnInputs) { |
|
// IE allows passing a tag to createElement. See note on `canSetNameOnInputs` above as well. |
|
tagString = '<'+stripTagName(tagName)+' name="'+escapeAttribute(attrs.name)+'">'; |
|
} else { |
|
tagString = tagName; |
|
} |
|
|
|
var element = document.createElement(tagString), |
|
$element = Ember.$(element); |
|
|
|
if (id) { |
|
$element.attr('id', id); |
|
this.elementId = null; |
|
} |
|
if (classes) { |
|
$element.attr('class', classes.join(' ')); |
|
this.classes = null; |
|
this.elementClasses = null; |
|
} |
|
|
|
if (style) { |
|
for (prop in style) { |
|
if (style.hasOwnProperty(prop)) { |
|
styleBuffer += (prop + ':' + style[prop] + ';'); |
|
} |
|
} |
|
|
|
$element.attr('style', styleBuffer); |
|
|
|
this.elementStyle = null; |
|
} |
|
|
|
if (attrs) { |
|
for (attr in attrs) { |
|
if (attrs.hasOwnProperty(attr)) { |
|
$element.attr(attr, attrs[attr]); |
|
} |
|
} |
|
|
|
this.elementAttributes = null; |
|
} |
|
|
|
if (props) { |
|
for (prop in props) { |
|
if (props.hasOwnProperty(prop)) { |
|
$element.prop(prop, props[prop]); |
|
} |
|
} |
|
|
|
this.elementProperties = null; |
|
} |
|
|
|
return element; |
|
}, |
|
|
|
/** |
|
@method element |
|
@return {DOMElement} The element corresponding to the generated HTML |
|
of this buffer |
|
*/ |
|
element: function() { |
|
var html = this.innerString(); |
|
|
|
if (html) { |
|
this._element = Ember.ViewUtils.setInnerHTML(this._element, html); |
|
} |
|
|
|
return this._element; |
|
}, |
|
|
|
/** |
|
Generates the HTML content for this buffer. |
|
|
|
@method string |
|
@return {String} The generated HTML |
|
*/ |
|
string: function() { |
|
if (this._hasElement && this._element) { |
|
// Firefox versions < 11 do not have support for element.outerHTML. |
|
var thisElement = this.element(), outerHTML = thisElement.outerHTML; |
|
if (typeof outerHTML === 'undefined') { |
|
return Ember.$('<div/>').append(thisElement).html(); |
|
} |
|
return outerHTML; |
|
} else { |
|
return this.innerString(); |
|
} |
|
}, |
|
|
|
innerString: function() { |
|
return this.buffer; |
|
} |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; |
|
|
|
/** |
|
`Ember.EventDispatcher` handles delegating browser events to their |
|
corresponding `Ember.Views.` For example, when you click on a view, |
|
`Ember.EventDispatcher` ensures that that view's `mouseDown` method gets |
|
called. |
|
|
|
@class EventDispatcher |
|
@namespace Ember |
|
@private |
|
@extends Ember.Object |
|
*/ |
|
Ember.EventDispatcher = Ember.Object.extend({ |
|
|
|
/** |
|
The set of events names (and associated handler function names) to be setup |
|
and dispatched by the `EventDispatcher`. Custom events can added to this list at setup |
|
time, generally via the `Ember.Application.customEvents` hash. Only override this |
|
default set to prevent the EventDispatcher from listening on some events all together. |
|
|
|
This set will be modified by `setup` to also include any events added at that time. |
|
|
|
@property events |
|
@type Object |
|
*/ |
|
events: { |
|
touchstart : 'touchStart', |
|
touchmove : 'touchMove', |
|
touchend : 'touchEnd', |
|
touchcancel : 'touchCancel', |
|
keydown : 'keyDown', |
|
keyup : 'keyUp', |
|
keypress : 'keyPress', |
|
mousedown : 'mouseDown', |
|
mouseup : 'mouseUp', |
|
contextmenu : 'contextMenu', |
|
click : 'click', |
|
dblclick : 'doubleClick', |
|
mousemove : 'mouseMove', |
|
focusin : 'focusIn', |
|
focusout : 'focusOut', |
|
mouseenter : 'mouseEnter', |
|
mouseleave : 'mouseLeave', |
|
submit : 'submit', |
|
input : 'input', |
|
change : 'change', |
|
dragstart : 'dragStart', |
|
drag : 'drag', |
|
dragenter : 'dragEnter', |
|
dragleave : 'dragLeave', |
|
dragover : 'dragOver', |
|
drop : 'drop', |
|
dragend : 'dragEnd' |
|
}, |
|
|
|
/** |
|
The root DOM element to which event listeners should be attached. Event |
|
listeners will be attached to the document unless this is overridden. |
|
|
|
Can be specified as a DOMElement or a selector string. |
|
|
|
The default body is a string since this may be evaluated before document.body |
|
exists in the DOM. |
|
|
|
@private |
|
@property rootElement |
|
@type DOMElement |
|
@default 'body' |
|
*/ |
|
rootElement: 'body', |
|
|
|
/** |
|
Sets up event listeners for standard browser events. |
|
|
|
This will be called after the browser sends a `DOMContentReady` event. By |
|
default, it will set up all of the listeners on the document body. If you |
|
would like to register the listeners on a different element, set the event |
|
dispatcher's `root` property. |
|
|
|
@private |
|
@method setup |
|
@param addedEvents {Hash} |
|
*/ |
|
setup: function(addedEvents, rootElement) { |
|
var event, events = get(this, 'events'); |
|
|
|
Ember.$.extend(events, addedEvents || {}); |
|
|
|
|
|
if (!Ember.isNone(rootElement)) { |
|
set(this, 'rootElement', rootElement); |
|
} |
|
|
|
rootElement = Ember.$(get(this, 'rootElement')); |
|
|
|
Ember.assert(fmt('You cannot use the same root element (%@) multiple times in an Ember.Application', [rootElement.selector || rootElement[0].tagName]), !rootElement.is('.ember-application')); |
|
Ember.assert('You cannot make a new Ember.Application using a root element that is a descendent of an existing Ember.Application', !rootElement.closest('.ember-application').length); |
|
Ember.assert('You cannot make a new Ember.Application using a root element that is an ancestor of an existing Ember.Application', !rootElement.find('.ember-application').length); |
|
|
|
rootElement.addClass('ember-application'); |
|
|
|
Ember.assert('Unable to add "ember-application" class to rootElement. Make sure you set rootElement to the body or an element in the body.', rootElement.is('.ember-application')); |
|
|
|
for (event in events) { |
|
if (events.hasOwnProperty(event)) { |
|
this.setupHandler(rootElement, event, events[event]); |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
Registers an event listener on the document. If the given event is |
|
triggered, the provided event handler will be triggered on the target view. |
|
|
|
If the target view does not implement the event handler, or if the handler |
|
returns `false`, the parent view will be called. The event will continue to |
|
bubble to each successive parent view until it reaches the top. |
|
|
|
For example, to have the `mouseDown` method called on the target view when |
|
a `mousedown` event is received from the browser, do the following: |
|
|
|
```javascript |
|
setupHandler('mousedown', 'mouseDown'); |
|
``` |
|
|
|
@private |
|
@method setupHandler |
|
@param {Element} rootElement |
|
@param {String} event the browser-originated event to listen to |
|
@param {String} eventName the name of the method to call on the view |
|
*/ |
|
setupHandler: function(rootElement, event, eventName) { |
|
var self = this; |
|
|
|
rootElement.on(event + '.ember', '.ember-view', function(evt, triggeringManager) { |
|
var view = Ember.View.views[this.id], |
|
result = true, manager = null; |
|
|
|
manager = self._findNearestEventManager(view, eventName); |
|
|
|
if (manager && manager !== triggeringManager) { |
|
result = self._dispatchEvent(manager, evt, eventName, view); |
|
} else if (view) { |
|
result = self._bubbleEvent(view, evt, eventName); |
|
} else { |
|
evt.stopPropagation(); |
|
} |
|
|
|
return result; |
|
}); |
|
|
|
rootElement.on(event + '.ember', '[data-ember-action]', function(evt) { |
|
var actionId = Ember.$(evt.currentTarget).attr('data-ember-action'), |
|
action = Ember.Handlebars.ActionHelper.registeredActions[actionId]; |
|
|
|
// We have to check for action here since in some cases, jQuery will trigger |
|
// an event on `removeChild` (i.e. focusout) after we've already torn down the |
|
// action handlers for the view. |
|
if (action && action.eventName === eventName) { |
|
return action.handler(evt); |
|
} |
|
}); |
|
}, |
|
|
|
_findNearestEventManager: function(view, eventName) { |
|
var manager = null; |
|
|
|
while (view) { |
|
manager = get(view, 'eventManager'); |
|
if (manager && manager[eventName]) { break; } |
|
|
|
view = get(view, 'parentView'); |
|
} |
|
|
|
return manager; |
|
}, |
|
|
|
_dispatchEvent: function(object, evt, eventName, view) { |
|
var result = true; |
|
|
|
var handler = object[eventName]; |
|
if (Ember.typeOf(handler) === 'function') { |
|
result = Ember.run(object, handler, evt, view); |
|
// Do not preventDefault in eventManagers. |
|
evt.stopPropagation(); |
|
} |
|
else { |
|
result = this._bubbleEvent(view, evt, eventName); |
|
} |
|
|
|
return result; |
|
}, |
|
|
|
_bubbleEvent: function(view, evt, eventName) { |
|
return Ember.run(view, view.handleEvent, eventName, evt); |
|
}, |
|
|
|
destroy: function() { |
|
var rootElement = get(this, 'rootElement'); |
|
Ember.$(rootElement).off('.ember', '**').removeClass('ember-application'); |
|
return this._super(); |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
// Add a new named queue for rendering views that happens |
|
// after bindings have synced, and a queue for scheduling actions |
|
// that that should occur after view rendering. |
|
var queues = Ember.run.queues, |
|
indexOf = Ember.ArrayPolyfills.indexOf; |
|
queues.splice(indexOf.call(queues, 'actions')+1, 0, 'render', 'afterRender'); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var states = {}; |
|
|
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set, |
|
guidFor = Ember.guidFor, |
|
a_forEach = Ember.EnumerableUtils.forEach, |
|
a_addObject = Ember.EnumerableUtils.addObject, |
|
meta = Ember.meta, |
|
defineProperty = Ember.defineProperty; |
|
|
|
function nullViewsBuffer(view) { |
|
view.buffer = null; |
|
} |
|
|
|
var childViewsProperty = Ember.computed(function() { |
|
var childViews = this._childViews, ret = Ember.A(), view = this; |
|
|
|
a_forEach(childViews, function(view) { |
|
var currentChildViews; |
|
if (view.isVirtual) { |
|
if (currentChildViews = get(view, 'childViews')) { |
|
ret.pushObjects(currentChildViews); |
|
} |
|
} else { |
|
ret.push(view); |
|
} |
|
}); |
|
|
|
ret.replace = function (idx, removedCount, addedViews) { |
|
if (view instanceof Ember.ContainerView) { |
|
Ember.deprecate("Manipulating an Ember.ContainerView through its childViews property is deprecated. Please use the ContainerView instance itself as an Ember.MutableArray."); |
|
return view.replace(idx, removedCount, addedViews); |
|
} |
|
throw new Ember.Error("childViews is immutable"); |
|
}; |
|
|
|
return ret; |
|
}); |
|
|
|
Ember.warn("The VIEW_PRESERVES_CONTEXT flag has been removed and the functionality can no longer be disabled.", Ember.ENV.VIEW_PRESERVES_CONTEXT !== false); |
|
|
|
/** |
|
Global hash of shared templates. This will automatically be populated |
|
by the build tools so that you can store your Handlebars templates in |
|
separate files that get loaded into JavaScript at buildtime. |
|
|
|
@property TEMPLATES |
|
@for Ember |
|
@type Hash |
|
*/ |
|
Ember.TEMPLATES = {}; |
|
|
|
/** |
|
`Ember.CoreView` is an abstract class that exists to give view-like behavior |
|
to both Ember's main view class `Ember.View` and other classes like |
|
`Ember._SimpleMetamorphView` that don't need the fully functionaltiy of |
|
`Ember.View`. |
|
|
|
Unless you have specific needs for `CoreView`, you will use `Ember.View` |
|
in your applications. |
|
|
|
@class CoreView |
|
@namespace Ember |
|
@extends Ember.Object |
|
@uses Ember.Evented |
|
@uses Ember.ActionHandler |
|
*/ |
|
|
|
Ember.CoreView = Ember.Object.extend(Ember.Evented, Ember.ActionHandler, { |
|
isView: true, |
|
|
|
states: states, |
|
|
|
init: function() { |
|
this._super(); |
|
this.transitionTo('preRender'); |
|
this._isVisible = get(this, 'isVisible'); |
|
}, |
|
|
|
/** |
|
If the view is currently inserted into the DOM of a parent view, this |
|
property will point to the parent of the view. |
|
|
|
@property parentView |
|
@type Ember.View |
|
@default null |
|
*/ |
|
parentView: Ember.computed('_parentView', function() { |
|
var parent = this._parentView; |
|
|
|
if (parent && parent.isVirtual) { |
|
return get(parent, 'parentView'); |
|
} else { |
|
return parent; |
|
} |
|
}), |
|
|
|
state: null, |
|
|
|
_parentView: null, |
|
|
|
// return the current view, not including virtual views |
|
concreteView: Ember.computed('parentView', function() { |
|
if (!this.isVirtual) { return this; } |
|
else { return get(this, 'parentView'); } |
|
}), |
|
|
|
instrumentName: 'core_view', |
|
|
|
instrumentDetails: function(hash) { |
|
hash.object = this.toString(); |
|
}, |
|
|
|
/** |
|
Invoked by the view system when this view needs to produce an HTML |
|
representation. This method will create a new render buffer, if needed, |
|
then apply any default attributes, such as class names and visibility. |
|
Finally, the `render()` method is invoked, which is responsible for |
|
doing the bulk of the rendering. |
|
|
|
You should not need to override this method; instead, implement the |
|
`template` property, or if you need more control, override the `render` |
|
method. |
|
|
|
@method renderToBuffer |
|
@param {Ember.RenderBuffer} buffer the render buffer. If no buffer is |
|
passed, a default buffer, using the current view's `tagName`, will |
|
be used. |
|
@private |
|
*/ |
|
renderToBuffer: function(parentBuffer, bufferOperation) { |
|
var name = 'render.' + this.instrumentName, |
|
details = {}; |
|
|
|
this.instrumentDetails(details); |
|
|
|
return Ember.instrument(name, details, function instrumentRenderToBuffer() { |
|
return this._renderToBuffer(parentBuffer, bufferOperation); |
|
}, this); |
|
}, |
|
|
|
_renderToBuffer: function(parentBuffer, bufferOperation) { |
|
// If this is the top-most view, start a new buffer. Otherwise, |
|
// create a new buffer relative to the original using the |
|
// provided buffer operation (for example, `insertAfter` will |
|
// insert a new buffer after the "parent buffer"). |
|
var tagName = this.tagName; |
|
|
|
if (tagName === null || tagName === undefined) { |
|
tagName = 'div'; |
|
} |
|
|
|
var buffer = this.buffer = parentBuffer && parentBuffer.begin(tagName) || Ember.RenderBuffer(tagName); |
|
this.transitionTo('inBuffer', false); |
|
|
|
this.beforeRender(buffer); |
|
this.render(buffer); |
|
this.afterRender(buffer); |
|
|
|
return buffer; |
|
}, |
|
|
|
/** |
|
Override the default event firing from `Ember.Evented` to |
|
also call methods with the given name. |
|
|
|
@method trigger |
|
@param name {String} |
|
@private |
|
*/ |
|
trigger: function(name) { |
|
this._super.apply(this, arguments); |
|
var method = this[name]; |
|
if (method) { |
|
var args = [], i, l; |
|
for (i = 1, l = arguments.length; i < l; i++) { |
|
args.push(arguments[i]); |
|
} |
|
return method.apply(this, args); |
|
} |
|
}, |
|
|
|
deprecatedSendHandles: function(actionName) { |
|
return !!this[actionName]; |
|
}, |
|
|
|
deprecatedSend: function(actionName) { |
|
var args = [].slice.call(arguments, 1); |
|
Ember.assert('' + this + " has the action " + actionName + " but it is not a function", typeof this[actionName] === 'function'); |
|
Ember.deprecate('Action handlers implemented directly on views are deprecated in favor of action handlers on an `actions` object ( action: `' + actionName + '` on ' + this + ')', false); |
|
this[actionName].apply(this, args); |
|
return; |
|
}, |
|
|
|
has: function(name) { |
|
return Ember.typeOf(this[name]) === 'function' || this._super(name); |
|
}, |
|
|
|
destroy: function() { |
|
var parent = this._parentView; |
|
|
|
if (!this._super()) { return; } |
|
|
|
// destroy the element -- this will avoid each child view destroying |
|
// the element over and over again... |
|
if (!this.removedFromDOM) { this.destroyElement(); } |
|
|
|
// remove from parent if found. Don't call removeFromParent, |
|
// as removeFromParent will try to remove the element from |
|
// the DOM again. |
|
if (parent) { parent.removeChild(this); } |
|
|
|
this.transitionTo('destroying', false); |
|
|
|
return this; |
|
}, |
|
|
|
clearRenderedChildren: Ember.K, |
|
triggerRecursively: Ember.K, |
|
invokeRecursively: Ember.K, |
|
transitionTo: Ember.K, |
|
destroyElement: Ember.K |
|
}); |
|
|
|
var ViewCollection = Ember._ViewCollection = function(initialViews) { |
|
var views = this.views = initialViews || []; |
|
this.length = views.length; |
|
}; |
|
|
|
ViewCollection.prototype = { |
|
length: 0, |
|
|
|
trigger: function(eventName) { |
|
var views = this.views, view; |
|
for (var i = 0, l = views.length; i < l; i++) { |
|
view = views[i]; |
|
if (view.trigger) { view.trigger(eventName); } |
|
} |
|
}, |
|
|
|
triggerRecursively: function(eventName) { |
|
var views = this.views; |
|
for (var i = 0, l = views.length; i < l; i++) { |
|
views[i].triggerRecursively(eventName); |
|
} |
|
}, |
|
|
|
invokeRecursively: function(fn) { |
|
var views = this.views, view; |
|
|
|
for (var i = 0, l = views.length; i < l; i++) { |
|
view = views[i]; |
|
fn(view); |
|
} |
|
}, |
|
|
|
transitionTo: function(state, children) { |
|
var views = this.views; |
|
for (var i = 0, l = views.length; i < l; i++) { |
|
views[i].transitionTo(state, children); |
|
} |
|
}, |
|
|
|
push: function() { |
|
this.length += arguments.length; |
|
var views = this.views; |
|
return views.push.apply(views, arguments); |
|
}, |
|
|
|
objectAt: function(idx) { |
|
return this.views[idx]; |
|
}, |
|
|
|
forEach: function(callback) { |
|
var views = this.views; |
|
return a_forEach(views, callback); |
|
}, |
|
|
|
clear: function() { |
|
this.length = 0; |
|
this.views.length = 0; |
|
} |
|
}; |
|
|
|
var EMPTY_ARRAY = []; |
|
|
|
/** |
|
`Ember.View` is the class in Ember responsible for encapsulating templates of |
|
HTML content, combining templates with data to render as sections of a page's |
|
DOM, and registering and responding to user-initiated events. |
|
|
|
## HTML Tag |
|
|
|
The default HTML tag name used for a view's DOM representation is `div`. This |
|
can be customized by setting the `tagName` property. The following view |
|
class: |
|
|
|
```javascript |
|
ParagraphView = Ember.View.extend({ |
|
tagName: 'em' |
|
}); |
|
``` |
|
|
|
Would result in instances with the following HTML: |
|
|
|
```html |
|
<em id="ember1" class="ember-view"></em> |
|
``` |
|
|
|
## HTML `class` Attribute |
|
|
|
The HTML `class` attribute of a view's tag can be set by providing a |
|
`classNames` property that is set to an array of strings: |
|
|
|
```javascript |
|
MyView = Ember.View.extend({ |
|
classNames: ['my-class', 'my-other-class'] |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view my-class my-other-class"></div> |
|
``` |
|
|
|
`class` attribute values can also be set by providing a `classNameBindings` |
|
property set to an array of properties names for the view. The return value |
|
of these properties will be added as part of the value for the view's `class` |
|
attribute. These properties can be computed properties: |
|
|
|
```javascript |
|
MyView = Ember.View.extend({ |
|
classNameBindings: ['propertyA', 'propertyB'], |
|
propertyA: 'from-a', |
|
propertyB: function() { |
|
if (someLogic) { return 'from-b'; } |
|
}.property() |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view from-a from-b"></div> |
|
``` |
|
|
|
If the value of a class name binding returns a boolean the property name |
|
itself will be used as the class name if the property is true. The class name |
|
will not be added if the value is `false` or `undefined`. |
|
|
|
```javascript |
|
MyView = Ember.View.extend({ |
|
classNameBindings: ['hovered'], |
|
hovered: true |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view hovered"></div> |
|
``` |
|
|
|
When using boolean class name bindings you can supply a string value other |
|
than the property name for use as the `class` HTML attribute by appending the |
|
preferred value after a ":" character when defining the binding: |
|
|
|
```javascript |
|
MyView = Ember.View.extend({ |
|
classNameBindings: ['awesome:so-very-cool'], |
|
awesome: true |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view so-very-cool"></div> |
|
``` |
|
|
|
Boolean value class name bindings whose property names are in a |
|
camelCase-style format will be converted to a dasherized format: |
|
|
|
```javascript |
|
MyView = Ember.View.extend({ |
|
classNameBindings: ['isUrgent'], |
|
isUrgent: true |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view is-urgent"></div> |
|
``` |
|
|
|
Class name bindings can also refer to object values that are found by |
|
traversing a path relative to the view itself: |
|
|
|
```javascript |
|
MyView = Ember.View.extend({ |
|
classNameBindings: ['messages.empty'] |
|
messages: Ember.Object.create({ |
|
empty: true |
|
}) |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view empty"></div> |
|
``` |
|
|
|
If you want to add a class name for a property which evaluates to true and |
|
and a different class name if it evaluates to false, you can pass a binding |
|
like this: |
|
|
|
```javascript |
|
// Applies 'enabled' class when isEnabled is true and 'disabled' when isEnabled is false |
|
Ember.View.extend({ |
|
classNameBindings: ['isEnabled:enabled:disabled'] |
|
isEnabled: true |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view enabled"></div> |
|
``` |
|
|
|
When isEnabled is `false`, the resulting HTML reprensentation looks like |
|
this: |
|
|
|
```html |
|
<div id="ember1" class="ember-view disabled"></div> |
|
``` |
|
|
|
This syntax offers the convenience to add a class if a property is `false`: |
|
|
|
```javascript |
|
// Applies no class when isEnabled is true and class 'disabled' when isEnabled is false |
|
Ember.View.extend({ |
|
classNameBindings: ['isEnabled::disabled'] |
|
isEnabled: true |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view"></div> |
|
``` |
|
|
|
When the `isEnabled` property on the view is set to `false`, it will result |
|
in view instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view disabled"></div> |
|
``` |
|
|
|
Updates to the the value of a class name binding will result in automatic |
|
update of the HTML `class` attribute in the view's rendered HTML |
|
representation. If the value becomes `false` or `undefined` the class name |
|
will be removed. |
|
|
|
Both `classNames` and `classNameBindings` are concatenated properties. See |
|
[Ember.Object](/api/classes/Ember.Object.html) documentation for more |
|
information about concatenated properties. |
|
|
|
## HTML Attributes |
|
|
|
The HTML attribute section of a view's tag can be set by providing an |
|
`attributeBindings` property set to an array of property names on the view. |
|
The return value of these properties will be used as the value of the view's |
|
HTML associated attribute: |
|
|
|
```javascript |
|
AnchorView = Ember.View.extend({ |
|
tagName: 'a', |
|
attributeBindings: ['href'], |
|
href: 'http://google.com' |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<a id="ember1" class="ember-view" href="http://google.com"></a> |
|
``` |
|
|
|
If the return value of an `attributeBindings` monitored property is a boolean |
|
the property will follow HTML's pattern of repeating the attribute's name as |
|
its value: |
|
|
|
```javascript |
|
MyTextInput = Ember.View.extend({ |
|
tagName: 'input', |
|
attributeBindings: ['disabled'], |
|
disabled: true |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<input id="ember1" class="ember-view" disabled="disabled" /> |
|
``` |
|
|
|
`attributeBindings` can refer to computed properties: |
|
|
|
```javascript |
|
MyTextInput = Ember.View.extend({ |
|
tagName: 'input', |
|
attributeBindings: ['disabled'], |
|
disabled: function() { |
|
if (someLogic) { |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
}.property() |
|
}); |
|
``` |
|
|
|
Updates to the the property of an attribute binding will result in automatic |
|
update of the HTML attribute in the view's rendered HTML representation. |
|
|
|
`attributeBindings` is a concatenated property. See [Ember.Object](/api/classes/Ember.Object.html) |
|
documentation for more information about concatenated properties. |
|
|
|
## Templates |
|
|
|
The HTML contents of a view's rendered representation are determined by its |
|
template. Templates can be any function that accepts an optional context |
|
parameter and returns a string of HTML that will be inserted within the |
|
view's tag. Most typically in Ember this function will be a compiled |
|
`Ember.Handlebars` template. |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
template: Ember.Handlebars.compile('I am the template') |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view">I am the template</div> |
|
``` |
|
|
|
Within an Ember application is more common to define a Handlebars templates as |
|
part of a page: |
|
|
|
```html |
|
<script type='text/x-handlebars' data-template-name='some-template'> |
|
Hello |
|
</script> |
|
``` |
|
|
|
And associate it by name using a view's `templateName` property: |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
templateName: 'some-template' |
|
}); |
|
``` |
|
|
|
If you have nested resources, your Handlebars template will look like this: |
|
|
|
```html |
|
<script type='text/x-handlebars' data-template-name='posts/new'> |
|
<h1>New Post</h1> |
|
</script> |
|
``` |
|
|
|
And `templateName` property: |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
templateName: 'posts/new' |
|
}); |
|
``` |
|
|
|
Using a value for `templateName` that does not have a Handlebars template |
|
with a matching `data-template-name` attribute will throw an error. |
|
|
|
For views classes that may have a template later defined (e.g. as the block |
|
portion of a `{{view}}` Handlebars helper call in another template or in |
|
a subclass), you can provide a `defaultTemplate` property set to compiled |
|
template function. If a template is not later provided for the view instance |
|
the `defaultTemplate` value will be used: |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
defaultTemplate: Ember.Handlebars.compile('I was the default'), |
|
template: null, |
|
templateName: null |
|
}); |
|
``` |
|
|
|
Will result in instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view">I was the default</div> |
|
``` |
|
|
|
If a `template` or `templateName` is provided it will take precedence over |
|
`defaultTemplate`: |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
defaultTemplate: Ember.Handlebars.compile('I was the default') |
|
}); |
|
|
|
aView = AView.create({ |
|
template: Ember.Handlebars.compile('I was the template, not default') |
|
}); |
|
``` |
|
|
|
Will result in the following HTML representation when rendered: |
|
|
|
```html |
|
<div id="ember1" class="ember-view">I was the template, not default</div> |
|
``` |
|
|
|
## View Context |
|
|
|
The default context of the compiled template is the view's controller: |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
template: Ember.Handlebars.compile('Hello {{excitedGreeting}}') |
|
}); |
|
|
|
aController = Ember.Object.create({ |
|
firstName: 'Barry', |
|
excitedGreeting: function() { |
|
return this.get("content.firstName") + "!!!" |
|
}.property() |
|
}); |
|
|
|
aView = AView.create({ |
|
controller: aController, |
|
}); |
|
``` |
|
|
|
Will result in an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view">Hello Barry!!!</div> |
|
``` |
|
|
|
A context can also be explicitly supplied through the view's `context` |
|
property. If the view has neither `context` nor `controller` properties, the |
|
`parentView`'s context will be used. |
|
|
|
## Layouts |
|
|
|
Views can have a secondary template that wraps their main template. Like |
|
primary templates, layouts can be any function that accepts an optional |
|
context parameter and returns a string of HTML that will be inserted inside |
|
view's tag. Views whose HTML element is self closing (e.g. `<input />`) |
|
cannot have a layout and this property will be ignored. |
|
|
|
Most typically in Ember a layout will be a compiled `Ember.Handlebars` |
|
template. |
|
|
|
A view's layout can be set directly with the `layout` property or reference |
|
an existing Handlebars template by name with the `layoutName` property. |
|
|
|
A template used as a layout must contain a single use of the Handlebars |
|
`{{yield}}` helper. The HTML contents of a view's rendered `template` will be |
|
inserted at this location: |
|
|
|
```javascript |
|
AViewWithLayout = Ember.View.extend({ |
|
layout: Ember.Handlebars.compile("<div class='my-decorative-class'>{{yield}}</div>") |
|
template: Ember.Handlebars.compile("I got wrapped"), |
|
}); |
|
``` |
|
|
|
Will result in view instances with an HTML representation of: |
|
|
|
```html |
|
<div id="ember1" class="ember-view"> |
|
<div class="my-decorative-class"> |
|
I got wrapped |
|
</div> |
|
</div> |
|
``` |
|
|
|
See [Ember.Handlebars.helpers.yield](/api/classes/Ember.Handlebars.helpers.html#method_yield) |
|
for more information. |
|
|
|
## Responding to Browser Events |
|
|
|
Views can respond to user-initiated events in one of three ways: method |
|
implementation, through an event manager, and through `{{action}}` helper use |
|
in their template or layout. |
|
|
|
### Method Implementation |
|
|
|
Views can respond to user-initiated events by implementing a method that |
|
matches the event name. A `jQuery.Event` object will be passed as the |
|
argument to this method. |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
click: function(event) { |
|
// will be called when when an instance's |
|
// rendered element is clicked |
|
} |
|
}); |
|
``` |
|
|
|
### Event Managers |
|
|
|
Views can define an object as their `eventManager` property. This object can |
|
then implement methods that match the desired event names. Matching events |
|
that occur on the view's rendered HTML or the rendered HTML of any of its DOM |
|
descendants will trigger this method. A `jQuery.Event` object will be passed |
|
as the first argument to the method and an `Ember.View` object as the |
|
second. The `Ember.View` will be the view whose rendered HTML was interacted |
|
with. This may be the view with the `eventManager` property or one of its |
|
descendent views. |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
eventManager: Ember.Object.create({ |
|
doubleClick: function(event, view) { |
|
// will be called when when an instance's |
|
// rendered element or any rendering |
|
// of this views's descendent |
|
// elements is clicked |
|
} |
|
}) |
|
}); |
|
``` |
|
|
|
An event defined for an event manager takes precedence over events of the |
|
same name handled through methods on the view. |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
mouseEnter: function(event) { |
|
// will never trigger. |
|
}, |
|
eventManager: Ember.Object.create({ |
|
mouseEnter: function(event, view) { |
|
// takes precedence over AView#mouseEnter |
|
} |
|
}) |
|
}); |
|
``` |
|
|
|
Similarly a view's event manager will take precedence for events of any views |
|
rendered as a descendent. A method name that matches an event name will not |
|
be called if the view instance was rendered inside the HTML representation of |
|
a view that has an `eventManager` property defined that handles events of the |
|
name. Events not handled by the event manager will still trigger method calls |
|
on the descendent. |
|
|
|
```javascript |
|
OuterView = Ember.View.extend({ |
|
template: Ember.Handlebars.compile("outer {{#view InnerView}}inner{{/view}} outer"), |
|
eventManager: Ember.Object.create({ |
|
mouseEnter: function(event, view) { |
|
// view might be instance of either |
|
// OuterView or InnerView depending on |
|
// where on the page the user interaction occured |
|
} |
|
}) |
|
}); |
|
|
|
InnerView = Ember.View.extend({ |
|
click: function(event) { |
|
// will be called if rendered inside |
|
// an OuterView because OuterView's |
|
// eventManager doesn't handle click events |
|
}, |
|
mouseEnter: function(event) { |
|
// will never be called if rendered inside |
|
// an OuterView. |
|
} |
|
}); |
|
``` |
|
|
|
### Handlebars `{{action}}` Helper |
|
|
|
See [Handlebars.helpers.action](/api/classes/Ember.Handlebars.helpers.html#method_action). |
|
|
|
### Event Names |
|
|
|
All of the event handling approaches described above respond to the same set |
|
of events. The names of the built-in events are listed below. (The hash of |
|
built-in events exists in `Ember.EventDispatcher`.) Additional, custom events |
|
can be registered by using `Ember.Application.customEvents`. |
|
|
|
Touch events: |
|
|
|
* `touchStart` |
|
* `touchMove` |
|
* `touchEnd` |
|
* `touchCancel` |
|
|
|
Keyboard events |
|
|
|
* `keyDown` |
|
* `keyUp` |
|
* `keyPress` |
|
|
|
Mouse events |
|
|
|
* `mouseDown` |
|
* `mouseUp` |
|
* `contextMenu` |
|
* `click` |
|
* `doubleClick` |
|
* `mouseMove` |
|
* `focusIn` |
|
* `focusOut` |
|
* `mouseEnter` |
|
* `mouseLeave` |
|
|
|
Form events: |
|
|
|
* `submit` |
|
* `change` |
|
* `focusIn` |
|
* `focusOut` |
|
* `input` |
|
|
|
HTML5 drag and drop events: |
|
|
|
* `dragStart` |
|
* `drag` |
|
* `dragEnter` |
|
* `dragLeave` |
|
* `drop` |
|
* `dragEnd` |
|
|
|
## Handlebars `{{view}}` Helper |
|
|
|
Other `Ember.View` instances can be included as part of a view's template by |
|
using the `{{view}}` Handlebars helper. See [Ember.Handlebars.helpers.view](/api/classes/Ember.Handlebars.helpers.html#method_view) |
|
for additional information. |
|
|
|
@class View |
|
@namespace Ember |
|
@extends Ember.CoreView |
|
*/ |
|
Ember.View = Ember.CoreView.extend({ |
|
|
|
concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'], |
|
|
|
/** |
|
@property isView |
|
@type Boolean |
|
@default true |
|
@static |
|
*/ |
|
isView: true, |
|
|
|
// .......................................................... |
|
// TEMPLATE SUPPORT |
|
// |
|
|
|
/** |
|
The name of the template to lookup if no template is provided. |
|
|
|
By default `Ember.View` will lookup a template with this name in |
|
`Ember.TEMPLATES` (a shared global object). |
|
|
|
@property templateName |
|
@type String |
|
@default null |
|
*/ |
|
templateName: null, |
|
|
|
/** |
|
The name of the layout to lookup if no layout is provided. |
|
|
|
By default `Ember.View` will lookup a template with this name in |
|
`Ember.TEMPLATES` (a shared global object). |
|
|
|
@property layoutName |
|
@type String |
|
@default null |
|
*/ |
|
layoutName: null, |
|
|
|
/** |
|
The template used to render the view. This should be a function that |
|
accepts an optional context parameter and returns a string of HTML that |
|
will be inserted into the DOM relative to its parent view. |
|
|
|
In general, you should set the `templateName` property instead of setting |
|
the template yourself. |
|
|
|
@property template |
|
@type Function |
|
*/ |
|
template: Ember.computed('templateName', function(key, value) { |
|
if (value !== undefined) { return value; } |
|
|
|
var templateName = get(this, 'templateName'), |
|
template = this.templateForName(templateName, 'template'); |
|
|
|
Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template); |
|
|
|
return template || get(this, 'defaultTemplate'); |
|
}), |
|
|
|
/** |
|
The controller managing this view. If this property is set, it will be |
|
made available for use by the template. |
|
|
|
@property controller |
|
@type Object |
|
*/ |
|
controller: Ember.computed('_parentView', function(key) { |
|
var parentView = get(this, '_parentView'); |
|
return parentView ? get(parentView, 'controller') : null; |
|
}), |
|
|
|
/** |
|
A view may contain a layout. A layout is a regular template but |
|
supersedes the `template` property during rendering. It is the |
|
responsibility of the layout template to retrieve the `template` |
|
property from the view (or alternatively, call `Handlebars.helpers.yield`, |
|
`{{yield}}`) to render it in the correct location. |
|
|
|
This is useful for a view that has a shared wrapper, but which delegates |
|
the rendering of the contents of the wrapper to the `template` property |
|
on a subclass. |
|
|
|
@property layout |
|
@type Function |
|
*/ |
|
layout: Ember.computed(function(key) { |
|
var layoutName = get(this, 'layoutName'), |
|
layout = this.templateForName(layoutName, 'layout'); |
|
|
|
Ember.assert("You specified the layoutName " + layoutName + " for " + this + ", but it did not exist.", !layoutName || layout); |
|
|
|
return layout || get(this, 'defaultLayout'); |
|
}).property('layoutName'), |
|
|
|
_yield: function(context, options) { |
|
var template = get(this, 'template'); |
|
if (template) { template(context, options); } |
|
}, |
|
|
|
templateForName: function(name, type) { |
|
if (!name) { return; } |
|
Ember.assert("templateNames are not allowed to contain periods: "+name, name.indexOf('.') === -1); |
|
|
|
// the defaultContainer is deprecated |
|
var container = this.container || (Ember.Container && Ember.Container.defaultContainer); |
|
return container && container.lookup('template:' + name); |
|
}, |
|
|
|
/** |
|
The object from which templates should access properties. |
|
|
|
This object will be passed to the template function each time the render |
|
method is called, but it is up to the individual function to decide what |
|
to do with it. |
|
|
|
By default, this will be the view's controller. |
|
|
|
@property context |
|
@type Object |
|
*/ |
|
context: Ember.computed(function(key, value) { |
|
if (arguments.length === 2) { |
|
set(this, '_context', value); |
|
return value; |
|
} else { |
|
return get(this, '_context'); |
|
} |
|
}).volatile(), |
|
|
|
/** |
|
Private copy of the view's template context. This can be set directly |
|
by Handlebars without triggering the observer that causes the view |
|
to be re-rendered. |
|
|
|
The context of a view is looked up as follows: |
|
|
|
1. Supplied context (usually by Handlebars) |
|
2. Specified controller |
|
3. `parentView`'s context (for a child of a ContainerView) |
|
|
|
The code in Handlebars that overrides the `_context` property first |
|
checks to see whether the view has a specified controller. This is |
|
something of a hack and should be revisited. |
|
|
|
@property _context |
|
@private |
|
*/ |
|
_context: Ember.computed(function(key) { |
|
var parentView, controller; |
|
|
|
if (controller = get(this, 'controller')) { |
|
return controller; |
|
} |
|
|
|
parentView = this._parentView; |
|
if (parentView) { |
|
return get(parentView, '_context'); |
|
} |
|
|
|
return null; |
|
}), |
|
|
|
/** |
|
If a value that affects template rendering changes, the view should be |
|
re-rendered to reflect the new value. |
|
|
|
@method _contextDidChange |
|
@private |
|
*/ |
|
_contextDidChange: Ember.observer('context', function() { |
|
this.rerender(); |
|
}), |
|
|
|
/** |
|
If `false`, the view will appear hidden in DOM. |
|
|
|
@property isVisible |
|
@type Boolean |
|
@default null |
|
*/ |
|
isVisible: true, |
|
|
|
/** |
|
Array of child views. You should never edit this array directly. |
|
Instead, use `appendChild` and `removeFromParent`. |
|
|
|
@property childViews |
|
@type Array |
|
@default [] |
|
@private |
|
*/ |
|
childViews: childViewsProperty, |
|
|
|
_childViews: EMPTY_ARRAY, |
|
|
|
// When it's a virtual view, we need to notify the parent that their |
|
// childViews will change. |
|
_childViewsWillChange: Ember.beforeObserver('childViews', function() { |
|
if (this.isVirtual) { |
|
var parentView = get(this, 'parentView'); |
|
if (parentView) { Ember.propertyWillChange(parentView, 'childViews'); } |
|
} |
|
}), |
|
|
|
// When it's a virtual view, we need to notify the parent that their |
|
// childViews did change. |
|
_childViewsDidChange: Ember.observer('childViews', function() { |
|
if (this.isVirtual) { |
|
var parentView = get(this, 'parentView'); |
|
if (parentView) { Ember.propertyDidChange(parentView, 'childViews'); } |
|
} |
|
}), |
|
|
|
/** |
|
Return the nearest ancestor that is an instance of the provided |
|
class. |
|
|
|
@method nearestInstanceOf |
|
@param {Class} klass Subclass of Ember.View (or Ember.View itself) |
|
@return Ember.View |
|
@deprecated |
|
*/ |
|
nearestInstanceOf: function(klass) { |
|
Ember.deprecate("nearestInstanceOf is deprecated and will be removed from future releases. Use nearestOfType."); |
|
var view = get(this, 'parentView'); |
|
|
|
while (view) { |
|
if (view instanceof klass) { return view; } |
|
view = get(view, 'parentView'); |
|
} |
|
}, |
|
|
|
/** |
|
Return the nearest ancestor that is an instance of the provided |
|
class or mixin. |
|
|
|
@method nearestOfType |
|
@param {Class,Mixin} klass Subclass of Ember.View (or Ember.View itself), |
|
or an instance of Ember.Mixin. |
|
@return Ember.View |
|
*/ |
|
nearestOfType: function(klass) { |
|
var view = get(this, 'parentView'), |
|
isOfType = klass instanceof Ember.Mixin ? |
|
function(view) { return klass.detect(view); } : |
|
function(view) { return klass.detect(view.constructor); }; |
|
|
|
while (view) { |
|
if (isOfType(view)) { return view; } |
|
view = get(view, 'parentView'); |
|
} |
|
}, |
|
|
|
/** |
|
Return the nearest ancestor that has a given property. |
|
|
|
@function nearestWithProperty |
|
@param {String} property A property name |
|
@return Ember.View |
|
*/ |
|
nearestWithProperty: function(property) { |
|
var view = get(this, 'parentView'); |
|
|
|
while (view) { |
|
if (property in view) { return view; } |
|
view = get(view, 'parentView'); |
|
} |
|
}, |
|
|
|
/** |
|
Return the nearest ancestor whose parent is an instance of |
|
`klass`. |
|
|
|
@method nearestChildOf |
|
@param {Class} klass Subclass of Ember.View (or Ember.View itself) |
|
@return Ember.View |
|
*/ |
|
nearestChildOf: function(klass) { |
|
var view = get(this, 'parentView'); |
|
|
|
while (view) { |
|
if (get(view, 'parentView') instanceof klass) { return view; } |
|
view = get(view, 'parentView'); |
|
} |
|
}, |
|
|
|
/** |
|
When the parent view changes, recursively invalidate `controller` |
|
|
|
@method _parentViewDidChange |
|
@private |
|
*/ |
|
_parentViewDidChange: Ember.observer('_parentView', function() { |
|
if (this.isDestroying) { return; } |
|
|
|
this.trigger('parentViewDidChange'); |
|
|
|
if (get(this, 'parentView.controller') && !get(this, 'controller')) { |
|
this.notifyPropertyChange('controller'); |
|
} |
|
}), |
|
|
|
_controllerDidChange: Ember.observer('controller', function() { |
|
if (this.isDestroying) { return; } |
|
|
|
this.rerender(); |
|
|
|
this.forEachChildView(function(view) { |
|
view.propertyDidChange('controller'); |
|
}); |
|
}), |
|
|
|
cloneKeywords: function() { |
|
var templateData = get(this, 'templateData'); |
|
|
|
var keywords = templateData ? Ember.copy(templateData.keywords) : {}; |
|
set(keywords, 'view', get(this, 'concreteView')); |
|
set(keywords, '_view', this); |
|
set(keywords, 'controller', get(this, 'controller')); |
|
|
|
return keywords; |
|
}, |
|
|
|
/** |
|
Called on your view when it should push strings of HTML into a |
|
`Ember.RenderBuffer`. Most users will want to override the `template` |
|
or `templateName` properties instead of this method. |
|
|
|
By default, `Ember.View` will look for a function in the `template` |
|
property and invoke it with the value of `context`. The value of |
|
`context` will be the view's controller unless you override it. |
|
|
|
@method render |
|
@param {Ember.RenderBuffer} buffer The render buffer |
|
*/ |
|
render: function(buffer) { |
|
// If this view has a layout, it is the responsibility of the |
|
// the layout to render the view's template. Otherwise, render the template |
|
// directly. |
|
var template = get(this, 'layout') || get(this, 'template'); |
|
|
|
if (template) { |
|
var context = get(this, 'context'); |
|
var keywords = this.cloneKeywords(); |
|
var output; |
|
|
|
var data = { |
|
view: this, |
|
buffer: buffer, |
|
isRenderData: true, |
|
keywords: keywords, |
|
insideGroup: get(this, 'templateData.insideGroup') |
|
}; |
|
|
|
// Invoke the template with the provided template context, which |
|
// is the view's controller by default. A hash of data is also passed that provides |
|
// the template with access to the view and render buffer. |
|
|
|
Ember.assert('template must be a function. Did you mean to call Ember.Handlebars.compile("...") or specify templateName instead?', typeof template === 'function'); |
|
// The template should write directly to the render buffer instead |
|
// of returning a string. |
|
output = template(context, { data: data }); |
|
|
|
// If the template returned a string instead of writing to the buffer, |
|
// push the string onto the buffer. |
|
if (output !== undefined) { buffer.push(output); } |
|
} |
|
}, |
|
|
|
/** |
|
Renders the view again. This will work regardless of whether the |
|
view is already in the DOM or not. If the view is in the DOM, the |
|
rendering process will be deferred to give bindings a chance |
|
to synchronize. |
|
|
|
If children were added during the rendering process using `appendChild`, |
|
`rerender` will remove them, because they will be added again |
|
if needed by the next `render`. |
|
|
|
In general, if the display of your view changes, you should modify |
|
the DOM element directly instead of manually calling `rerender`, which can |
|
be slow. |
|
|
|
@method rerender |
|
*/ |
|
rerender: function() { |
|
return this.currentState.rerender(this); |
|
}, |
|
|
|
clearRenderedChildren: function() { |
|
var lengthBefore = this.lengthBeforeRender, |
|
lengthAfter = this.lengthAfterRender; |
|
|
|
// If there were child views created during the last call to render(), |
|
// remove them under the assumption that they will be re-created when |
|
// we re-render. |
|
|
|
// VIEW-TODO: Unit test this path. |
|
var childViews = this._childViews; |
|
for (var i=lengthAfter-1; i>=lengthBefore; i--) { |
|
if (childViews[i]) { childViews[i].destroy(); } |
|
} |
|
}, |
|
|
|
/** |
|
Iterates over the view's `classNameBindings` array, inserts the value |
|
of the specified property into the `classNames` array, then creates an |
|
observer to update the view's element if the bound property ever changes |
|
in the future. |
|
|
|
@method _applyClassNameBindings |
|
@private |
|
*/ |
|
_applyClassNameBindings: function(classBindings) { |
|
var classNames = this.classNames, |
|
elem, newClass, dasherizedClass; |
|
|
|
// Loop through all of the configured bindings. These will be either |
|
// property names ('isUrgent') or property paths relative to the view |
|
// ('content.isUrgent') |
|
a_forEach(classBindings, function(binding) { |
|
|
|
Ember.assert("classNameBindings must not have spaces in them. Multiple class name bindings can be provided as elements of an array, e.g. ['foo', ':bar']", binding.indexOf(' ') === -1); |
|
|
|
// Variable in which the old class value is saved. The observer function |
|
// closes over this variable, so it knows which string to remove when |
|
// the property changes. |
|
var oldClass; |
|
// Extract just the property name from bindings like 'foo:bar' |
|
var parsedPath = Ember.View._parsePropertyPath(binding); |
|
|
|
// Set up an observer on the context. If the property changes, toggle the |
|
// class name. |
|
var observer = function() { |
|
// Get the current value of the property |
|
newClass = this._classStringForProperty(binding); |
|
elem = this.$(); |
|
|
|
// If we had previously added a class to the element, remove it. |
|
if (oldClass) { |
|
elem.removeClass(oldClass); |
|
// Also remove from classNames so that if the view gets rerendered, |
|
// the class doesn't get added back to the DOM. |
|
classNames.removeObject(oldClass); |
|
} |
|
|
|
// If necessary, add a new class. Make sure we keep track of it so |
|
// it can be removed in the future. |
|
if (newClass) { |
|
elem.addClass(newClass); |
|
oldClass = newClass; |
|
} else { |
|
oldClass = null; |
|
} |
|
}; |
|
|
|
// Get the class name for the property at its current value |
|
dasherizedClass = this._classStringForProperty(binding); |
|
|
|
if (dasherizedClass) { |
|
// Ensure that it gets into the classNames array |
|
// so it is displayed when we render. |
|
a_addObject(classNames, dasherizedClass); |
|
|
|
// Save a reference to the class name so we can remove it |
|
// if the observer fires. Remember that this variable has |
|
// been closed over by the observer. |
|
oldClass = dasherizedClass; |
|
} |
|
|
|
this.registerObserver(this, parsedPath.path, observer); |
|
// Remove className so when the view is rerendered, |
|
// the className is added based on binding reevaluation |
|
this.one('willClearRender', function() { |
|
if (oldClass) { |
|
classNames.removeObject(oldClass); |
|
oldClass = null; |
|
} |
|
}); |
|
|
|
}, this); |
|
}, |
|
|
|
_unspecifiedAttributeBindings: null, |
|
|
|
/** |
|
Iterates through the view's attribute bindings, sets up observers for each, |
|
then applies the current value of the attributes to the passed render buffer. |
|
|
|
@method _applyAttributeBindings |
|
@param {Ember.RenderBuffer} buffer |
|
@private |
|
*/ |
|
_applyAttributeBindings: function(buffer, attributeBindings) { |
|
var attributeValue, |
|
unspecifiedAttributeBindings = this._unspecifiedAttributeBindings = this._unspecifiedAttributeBindings || {}; |
|
|
|
a_forEach(attributeBindings, function(binding) { |
|
var split = binding.split(':'), |
|
property = split[0], |
|
attributeName = split[1] || property; |
|
|
|
if (property in this) { |
|
this._setupAttributeBindingObservation(property, attributeName); |
|
|
|
// Determine the current value and add it to the render buffer |
|
// if necessary. |
|
attributeValue = get(this, property); |
|
Ember.View.applyAttributeBindings(buffer, attributeName, attributeValue); |
|
} else { |
|
unspecifiedAttributeBindings[property] = attributeName; |
|
} |
|
}, this); |
|
|
|
// Lazily setup setUnknownProperty after attributeBindings are initially applied |
|
this.setUnknownProperty = this._setUnknownProperty; |
|
}, |
|
|
|
_setupAttributeBindingObservation: function(property, attributeName) { |
|
var attributeValue, elem; |
|
|
|
// Create an observer to add/remove/change the attribute if the |
|
// JavaScript property changes. |
|
var observer = function() { |
|
elem = this.$(); |
|
|
|
attributeValue = get(this, property); |
|
|
|
Ember.View.applyAttributeBindings(elem, attributeName, attributeValue); |
|
}; |
|
|
|
this.registerObserver(this, property, observer); |
|
}, |
|
|
|
/** |
|
We're using setUnknownProperty as a hook to setup attributeBinding observers for |
|
properties that aren't defined on a view at initialization time. |
|
|
|
Note: setUnknownProperty will only be called once for each property. |
|
|
|
@method setUnknownProperty |
|
@param key |
|
@param value |
|
@private |
|
*/ |
|
setUnknownProperty: null, // Gets defined after initialization by _applyAttributeBindings |
|
|
|
_setUnknownProperty: function(key, value) { |
|
var attributeName = this._unspecifiedAttributeBindings && this._unspecifiedAttributeBindings[key]; |
|
if (attributeName) { |
|
this._setupAttributeBindingObservation(key, attributeName); |
|
} |
|
|
|
defineProperty(this, key); |
|
return set(this, key, value); |
|
}, |
|
|
|
/** |
|
Given a property name, returns a dasherized version of that |
|
property name if the property evaluates to a non-falsy value. |
|
|
|
For example, if the view has property `isUrgent` that evaluates to true, |
|
passing `isUrgent` to this method will return `"is-urgent"`. |
|
|
|
@method _classStringForProperty |
|
@param property |
|
@private |
|
*/ |
|
_classStringForProperty: function(property) { |
|
var parsedPath = Ember.View._parsePropertyPath(property); |
|
var path = parsedPath.path; |
|
|
|
var val = get(this, path); |
|
if (val === undefined && Ember.isGlobalPath(path)) { |
|
val = get(Ember.lookup, path); |
|
} |
|
|
|
return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); |
|
}, |
|
|
|
// .......................................................... |
|
// ELEMENT SUPPORT |
|
// |
|
|
|
/** |
|
Returns the current DOM element for the view. |
|
|
|
@property element |
|
@type DOMElement |
|
*/ |
|
element: Ember.computed('_parentView', function(key, value) { |
|
if (value !== undefined) { |
|
return this.currentState.setElement(this, value); |
|
} else { |
|
return this.currentState.getElement(this); |
|
} |
|
}), |
|
|
|
/** |
|
Returns a jQuery object for this view's element. If you pass in a selector |
|
string, this method will return a jQuery object, using the current element |
|
as its buffer. |
|
|
|
For example, calling `view.$('li')` will return a jQuery object containing |
|
all of the `li` elements inside the DOM element of this view. |
|
|
|
@method $ |
|
@param {String} [selector] a jQuery-compatible selector string |
|
@return {jQuery} the jQuery object for the DOM node |
|
*/ |
|
$: function(sel) { |
|
return this.currentState.$(this, sel); |
|
}, |
|
|
|
mutateChildViews: function(callback) { |
|
var childViews = this._childViews, |
|
idx = childViews.length, |
|
view; |
|
|
|
while(--idx >= 0) { |
|
view = childViews[idx]; |
|
callback(this, view, idx); |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
forEachChildView: function(callback) { |
|
var childViews = this._childViews; |
|
|
|
if (!childViews) { return this; } |
|
|
|
var len = childViews.length, |
|
view, idx; |
|
|
|
for (idx = 0; idx < len; idx++) { |
|
view = childViews[idx]; |
|
callback(view); |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Appends the view's element to the specified parent element. |
|
|
|
If the view does not have an HTML representation yet, `createElement()` |
|
will be called automatically. |
|
|
|
Note that this method just schedules the view to be appended; the DOM |
|
element will not be appended to the given element until all bindings have |
|
finished synchronizing. |
|
|
|
This is not typically a function that you will need to call directly when |
|
building your application. You might consider using `Ember.ContainerView` |
|
instead. If you do need to use `appendTo`, be sure that the target element |
|
you are providing is associated with an `Ember.Application` and does not |
|
have an ancestor element that is associated with an Ember view. |
|
|
|
@method appendTo |
|
@param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object |
|
@return {Ember.View} receiver |
|
*/ |
|
appendTo: function(target) { |
|
// Schedule the DOM element to be created and appended to the given |
|
// element after bindings have synchronized. |
|
this._insertElementLater(function() { |
|
Ember.assert("You tried to append to (" + target + ") but that isn't in the DOM", Ember.$(target).length > 0); |
|
Ember.assert("You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view')); |
|
this.$().appendTo(target); |
|
}); |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Replaces the content of the specified parent element with this view's |
|
element. If the view does not have an HTML representation yet, |
|
`createElement()` will be called automatically. |
|
|
|
Note that this method just schedules the view to be appended; the DOM |
|
element will not be appended to the given element until all bindings have |
|
finished synchronizing |
|
|
|
@method replaceIn |
|
@param {String|DOMElement|jQuery} target A selector, element, HTML string, or jQuery object |
|
@return {Ember.View} received |
|
*/ |
|
replaceIn: function(target) { |
|
Ember.assert("You tried to replace in (" + target + ") but that isn't in the DOM", Ember.$(target).length > 0); |
|
Ember.assert("You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view')); |
|
|
|
this._insertElementLater(function() { |
|
Ember.$(target).empty(); |
|
this.$().appendTo(target); |
|
}); |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Schedules a DOM operation to occur during the next render phase. This |
|
ensures that all bindings have finished synchronizing before the view is |
|
rendered. |
|
|
|
To use, pass a function that performs a DOM operation. |
|
|
|
Before your function is called, this view and all child views will receive |
|
the `willInsertElement` event. After your function is invoked, this view |
|
and all of its child views will receive the `didInsertElement` event. |
|
|
|
```javascript |
|
view._insertElementLater(function() { |
|
this.createElement(); |
|
this.$().appendTo('body'); |
|
}); |
|
``` |
|
|
|
@method _insertElementLater |
|
@param {Function} fn the function that inserts the element into the DOM |
|
@private |
|
*/ |
|
_insertElementLater: function(fn) { |
|
this._scheduledInsert = Ember.run.scheduleOnce('render', this, '_insertElement', fn); |
|
}, |
|
|
|
_insertElement: function (fn) { |
|
this._scheduledInsert = null; |
|
this.currentState.insertElement(this, fn); |
|
}, |
|
|
|
/** |
|
Appends the view's element to the document body. If the view does |
|
not have an HTML representation yet, `createElement()` will be called |
|
automatically. |
|
|
|
If your application uses the `rootElement` property, you must append |
|
the view within that element. Rendering views outside of the `rootElement` |
|
is not supported. |
|
|
|
Note that this method just schedules the view to be appended; the DOM |
|
element will not be appended to the document body until all bindings have |
|
finished synchronizing. |
|
|
|
@method append |
|
@return {Ember.View} receiver |
|
*/ |
|
append: function() { |
|
return this.appendTo(document.body); |
|
}, |
|
|
|
/** |
|
Removes the view's element from the element to which it is attached. |
|
|
|
@method remove |
|
@return {Ember.View} receiver |
|
*/ |
|
remove: function() { |
|
// What we should really do here is wait until the end of the run loop |
|
// to determine if the element has been re-appended to a different |
|
// element. |
|
// In the interim, we will just re-render if that happens. It is more |
|
// important than elements get garbage collected. |
|
if (!this.removedFromDOM) { this.destroyElement(); } |
|
this.invokeRecursively(function(view) { |
|
if (view.clearRenderedChildren) { view.clearRenderedChildren(); } |
|
}); |
|
}, |
|
|
|
elementId: null, |
|
|
|
/** |
|
Attempts to discover the element in the parent element. The default |
|
implementation looks for an element with an ID of `elementId` (or the |
|
view's guid if `elementId` is null). You can override this method to |
|
provide your own form of lookup. For example, if you want to discover your |
|
element using a CSS class name instead of an ID. |
|
|
|
@method findElementInParentElement |
|
@param {DOMElement} parentElement The parent's DOM element |
|
@return {DOMElement} The discovered element |
|
*/ |
|
findElementInParentElement: function(parentElem) { |
|
var id = "#" + this.elementId; |
|
return Ember.$(id)[0] || Ember.$(id, parentElem)[0]; |
|
}, |
|
|
|
/** |
|
Creates a DOM representation of the view and all of its |
|
child views by recursively calling the `render()` method. |
|
|
|
After the element has been created, `didInsertElement` will |
|
be called on this view and all of its child views. |
|
|
|
@method createElement |
|
@return {Ember.View} receiver |
|
*/ |
|
createElement: function() { |
|
if (get(this, 'element')) { return this; } |
|
|
|
var buffer = this.renderToBuffer(); |
|
set(this, 'element', buffer.element()); |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Called when a view is going to insert an element into the DOM. |
|
|
|
@event willInsertElement |
|
*/ |
|
willInsertElement: Ember.K, |
|
|
|
/** |
|
Called when the element of the view has been inserted into the DOM |
|
or after the view was re-rendered. Override this function to do any |
|
set up that requires an element in the document body. |
|
|
|
@event didInsertElement |
|
*/ |
|
didInsertElement: Ember.K, |
|
|
|
/** |
|
Called when the view is about to rerender, but before anything has |
|
been torn down. This is a good opportunity to tear down any manual |
|
observers you have installed based on the DOM state |
|
|
|
@event willClearRender |
|
*/ |
|
willClearRender: Ember.K, |
|
|
|
/** |
|
Run this callback on the current view (unless includeSelf is false) and recursively on child views. |
|
|
|
@method invokeRecursively |
|
@param fn {Function} |
|
@param includeSelf {Boolean} Includes itself if true. |
|
@private |
|
*/ |
|
invokeRecursively: function(fn, includeSelf) { |
|
var childViews = (includeSelf === false) ? this._childViews : [this]; |
|
var currentViews, view, currentChildViews; |
|
|
|
while (childViews.length) { |
|
currentViews = childViews.slice(); |
|
childViews = []; |
|
|
|
for (var i=0, l=currentViews.length; i<l; i++) { |
|
view = currentViews[i]; |
|
currentChildViews = view._childViews ? view._childViews.slice(0) : null; |
|
fn(view); |
|
if (currentChildViews) { |
|
childViews.push.apply(childViews, currentChildViews); |
|
} |
|
} |
|
} |
|
}, |
|
|
|
triggerRecursively: function(eventName) { |
|
var childViews = [this], currentViews, view, currentChildViews; |
|
|
|
while (childViews.length) { |
|
currentViews = childViews.slice(); |
|
childViews = []; |
|
|
|
for (var i=0, l=currentViews.length; i<l; i++) { |
|
view = currentViews[i]; |
|
currentChildViews = view._childViews ? view._childViews.slice(0) : null; |
|
if (view.trigger) { view.trigger(eventName); } |
|
if (currentChildViews) { |
|
childViews.push.apply(childViews, currentChildViews); |
|
} |
|
|
|
} |
|
} |
|
}, |
|
|
|
viewHierarchyCollection: function() { |
|
var currentView, viewCollection = new ViewCollection([this]); |
|
|
|
for (var i = 0; i < viewCollection.length; i++) { |
|
currentView = viewCollection.objectAt(i); |
|
if (currentView._childViews) { |
|
viewCollection.push.apply(viewCollection, currentView._childViews); |
|
} |
|
} |
|
|
|
return viewCollection; |
|
}, |
|
|
|
/** |
|
Destroys any existing element along with the element for any child views |
|
as well. If the view does not currently have a element, then this method |
|
will do nothing. |
|
|
|
If you implement `willDestroyElement()` on your view, then this method will |
|
be invoked on your view before your element is destroyed to give you a |
|
chance to clean up any event handlers, etc. |
|
|
|
If you write a `willDestroyElement()` handler, you can assume that your |
|
`didInsertElement()` handler was called earlier for the same element. |
|
|
|
You should not call or override this method yourself, but you may |
|
want to implement the above callbacks. |
|
|
|
@method destroyElement |
|
@return {Ember.View} receiver |
|
*/ |
|
destroyElement: function() { |
|
return this.currentState.destroyElement(this); |
|
}, |
|
|
|
/** |
|
Called when the element of the view is going to be destroyed. Override |
|
this function to do any teardown that requires an element, like removing |
|
event listeners. |
|
|
|
@event willDestroyElement |
|
*/ |
|
willDestroyElement: Ember.K, |
|
|
|
/** |
|
Triggers the `willDestroyElement` event (which invokes the |
|
`willDestroyElement()` method if it exists) on this view and all child |
|
views. |
|
|
|
Before triggering `willDestroyElement`, it first triggers the |
|
`willClearRender` event recursively. |
|
|
|
@method _notifyWillDestroyElement |
|
@private |
|
*/ |
|
_notifyWillDestroyElement: function() { |
|
var viewCollection = this.viewHierarchyCollection(); |
|
viewCollection.trigger('willClearRender'); |
|
viewCollection.trigger('willDestroyElement'); |
|
return viewCollection; |
|
}, |
|
|
|
/** |
|
If this view's element changes, we need to invalidate the caches of our |
|
child views so that we do not retain references to DOM elements that are |
|
no longer needed. |
|
|
|
@method _elementDidChange |
|
@private |
|
*/ |
|
_elementDidChange: Ember.observer('element', function() { |
|
this.forEachChildView(function(view) { |
|
delete meta(view).cache.element; |
|
}); |
|
}), |
|
|
|
/** |
|
Called when the parentView property has changed. |
|
|
|
@event parentViewDidChange |
|
*/ |
|
parentViewDidChange: Ember.K, |
|
|
|
instrumentName: 'view', |
|
|
|
instrumentDetails: function(hash) { |
|
hash.template = get(this, 'templateName'); |
|
this._super(hash); |
|
}, |
|
|
|
_renderToBuffer: function(parentBuffer, bufferOperation) { |
|
this.lengthBeforeRender = this._childViews.length; |
|
var buffer = this._super(parentBuffer, bufferOperation); |
|
this.lengthAfterRender = this._childViews.length; |
|
|
|
return buffer; |
|
}, |
|
|
|
renderToBufferIfNeeded: function (buffer) { |
|
return this.currentState.renderToBufferIfNeeded(this, buffer); |
|
}, |
|
|
|
beforeRender: function(buffer) { |
|
this.applyAttributesToBuffer(buffer); |
|
buffer.pushOpeningTag(); |
|
}, |
|
|
|
afterRender: function(buffer) { |
|
buffer.pushClosingTag(); |
|
}, |
|
|
|
applyAttributesToBuffer: function(buffer) { |
|
// Creates observers for all registered class name and attribute bindings, |
|
// then adds them to the element. |
|
var classNameBindings = get(this, 'classNameBindings'); |
|
if (classNameBindings.length) { |
|
this._applyClassNameBindings(classNameBindings); |
|
} |
|
|
|
// Pass the render buffer so the method can apply attributes directly. |
|
// This isn't needed for class name bindings because they use the |
|
// existing classNames infrastructure. |
|
var attributeBindings = get(this, 'attributeBindings'); |
|
if (attributeBindings.length) { |
|
this._applyAttributeBindings(buffer, attributeBindings); |
|
} |
|
|
|
buffer.setClasses(this.classNames); |
|
buffer.id(this.elementId); |
|
|
|
var role = get(this, 'ariaRole'); |
|
if (role) { |
|
buffer.attr('role', role); |
|
} |
|
|
|
if (get(this, 'isVisible') === false) { |
|
buffer.style('display', 'none'); |
|
} |
|
}, |
|
|
|
// .......................................................... |
|
// STANDARD RENDER PROPERTIES |
|
// |
|
|
|
/** |
|
Tag name for the view's outer element. The tag name is only used when an |
|
element is first created. If you change the `tagName` for an element, you |
|
must destroy and recreate the view element. |
|
|
|
By default, the render buffer will use a `<div>` tag for views. |
|
|
|
@property tagName |
|
@type String |
|
@default null |
|
*/ |
|
|
|
// We leave this null by default so we can tell the difference between |
|
// the default case and a user-specified tag. |
|
tagName: null, |
|
|
|
/** |
|
The WAI-ARIA role of the control represented by this view. For example, a |
|
button may have a role of type 'button', or a pane may have a role of |
|
type 'alertdialog'. This property is used by assistive software to help |
|
visually challenged users navigate rich web applications. |
|
|
|
The full list of valid WAI-ARIA roles is available at: |
|
[http://www.w3.org/TR/wai-aria/roles#roles_categorization](http://www.w3.org/TR/wai-aria/roles#roles_categorization) |
|
|
|
@property ariaRole |
|
@type String |
|
@default null |
|
*/ |
|
ariaRole: null, |
|
|
|
/** |
|
Standard CSS class names to apply to the view's outer element. This |
|
property automatically inherits any class names defined by the view's |
|
superclasses as well. |
|
|
|
@property classNames |
|
@type Array |
|
@default ['ember-view'] |
|
*/ |
|
classNames: ['ember-view'], |
|
|
|
/** |
|
A list of properties of the view to apply as class names. If the property |
|
is a string value, the value of that string will be applied as a class |
|
name. |
|
|
|
```javascript |
|
// Applies the 'high' class to the view element |
|
Ember.View.extend({ |
|
classNameBindings: ['priority'] |
|
priority: 'high' |
|
}); |
|
``` |
|
|
|
If the value of the property is a Boolean, the name of that property is |
|
added as a dasherized class name. |
|
|
|
```javascript |
|
// Applies the 'is-urgent' class to the view element |
|
Ember.View.extend({ |
|
classNameBindings: ['isUrgent'] |
|
isUrgent: true |
|
}); |
|
``` |
|
|
|
If you would prefer to use a custom value instead of the dasherized |
|
property name, you can pass a binding like this: |
|
|
|
```javascript |
|
// Applies the 'urgent' class to the view element |
|
Ember.View.extend({ |
|
classNameBindings: ['isUrgent:urgent'] |
|
isUrgent: true |
|
}); |
|
``` |
|
|
|
This list of properties is inherited from the view's superclasses as well. |
|
|
|
@property classNameBindings |
|
@type Array |
|
@default [] |
|
*/ |
|
classNameBindings: EMPTY_ARRAY, |
|
|
|
/** |
|
A list of properties of the view to apply as attributes. If the property is |
|
a string value, the value of that string will be applied as the attribute. |
|
|
|
```javascript |
|
// Applies the type attribute to the element |
|
// with the value "button", like <div type="button"> |
|
Ember.View.extend({ |
|
attributeBindings: ['type'], |
|
type: 'button' |
|
}); |
|
``` |
|
|
|
If the value of the property is a Boolean, the name of that property is |
|
added as an attribute. |
|
|
|
```javascript |
|
// Renders something like <div enabled="enabled"> |
|
Ember.View.extend({ |
|
attributeBindings: ['enabled'], |
|
enabled: true |
|
}); |
|
``` |
|
|
|
@property attributeBindings |
|
*/ |
|
attributeBindings: EMPTY_ARRAY, |
|
|
|
// ....................................................... |
|
// CORE DISPLAY METHODS |
|
// |
|
|
|
/** |
|
Setup a view, but do not finish waking it up. |
|
|
|
* configure `childViews` |
|
* register the view with the global views hash, which is used for event |
|
dispatch |
|
|
|
@method init |
|
@private |
|
*/ |
|
init: function() { |
|
this.elementId = this.elementId || guidFor(this); |
|
|
|
this._super(); |
|
|
|
// setup child views. be sure to clone the child views array first |
|
this._childViews = this._childViews.slice(); |
|
|
|
Ember.assert("Only arrays are allowed for 'classNameBindings'", Ember.typeOf(this.classNameBindings) === 'array'); |
|
this.classNameBindings = Ember.A(this.classNameBindings.slice()); |
|
|
|
Ember.assert("Only arrays are allowed for 'classNames'", Ember.typeOf(this.classNames) === 'array'); |
|
this.classNames = Ember.A(this.classNames.slice()); |
|
}, |
|
|
|
appendChild: function(view, options) { |
|
return this.currentState.appendChild(this, view, options); |
|
}, |
|
|
|
/** |
|
Removes the child view from the parent view. |
|
|
|
@method removeChild |
|
@param {Ember.View} view |
|
@return {Ember.View} receiver |
|
*/ |
|
removeChild: function(view) { |
|
// If we're destroying, the entire subtree will be |
|
// freed, and the DOM will be handled separately, |
|
// so no need to mess with childViews. |
|
if (this.isDestroying) { return; } |
|
|
|
// update parent node |
|
set(view, '_parentView', null); |
|
|
|
// remove view from childViews array. |
|
var childViews = this._childViews; |
|
|
|
Ember.EnumerableUtils.removeObject(childViews, view); |
|
|
|
this.propertyDidChange('childViews'); // HUH?! what happened to will change? |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Removes all children from the `parentView`. |
|
|
|
@method removeAllChildren |
|
@return {Ember.View} receiver |
|
*/ |
|
removeAllChildren: function() { |
|
return this.mutateChildViews(function(parentView, view) { |
|
parentView.removeChild(view); |
|
}); |
|
}, |
|
|
|
destroyAllChildren: function() { |
|
return this.mutateChildViews(function(parentView, view) { |
|
view.destroy(); |
|
}); |
|
}, |
|
|
|
/** |
|
Removes the view from its `parentView`, if one is found. Otherwise |
|
does nothing. |
|
|
|
@method removeFromParent |
|
@return {Ember.View} receiver |
|
*/ |
|
removeFromParent: function() { |
|
var parent = this._parentView; |
|
|
|
// Remove DOM element from parent |
|
this.remove(); |
|
|
|
if (parent) { parent.removeChild(this); } |
|
return this; |
|
}, |
|
|
|
/** |
|
You must call `destroy` on a view to destroy the view (and all of its |
|
child views). This will remove the view from any parent node, then make |
|
sure that the DOM element managed by the view can be released by the |
|
memory manager. |
|
|
|
@method destroy |
|
*/ |
|
destroy: function() { |
|
var childViews = this._childViews, |
|
// get parentView before calling super because it'll be destroyed |
|
nonVirtualParentView = get(this, 'parentView'), |
|
viewName = this.viewName, |
|
childLen, i; |
|
|
|
if (!this._super()) { return; } |
|
|
|
childLen = childViews.length; |
|
for (i=childLen-1; i>=0; i--) { |
|
childViews[i].removedFromDOM = true; |
|
} |
|
|
|
// remove from non-virtual parent view if viewName was specified |
|
if (viewName && nonVirtualParentView) { |
|
nonVirtualParentView.set(viewName, null); |
|
} |
|
|
|
childLen = childViews.length; |
|
for (i=childLen-1; i>=0; i--) { |
|
childViews[i].destroy(); |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Instantiates a view to be added to the childViews array during view |
|
initialization. You generally will not call this method directly unless |
|
you are overriding `createChildViews()`. Note that this method will |
|
automatically configure the correct settings on the new view instance to |
|
act as a child of the parent. |
|
|
|
@method createChildView |
|
@param {Class|String} viewClass |
|
@param {Hash} [attrs] Attributes to add |
|
@return {Ember.View} new instance |
|
*/ |
|
createChildView: function(view, attrs) { |
|
if (!view) { |
|
throw new TypeError("createChildViews first argument must exist"); |
|
} |
|
|
|
if (view.isView && view._parentView === this && view.container === this.container) { |
|
return view; |
|
} |
|
|
|
attrs = attrs || {}; |
|
attrs._parentView = this; |
|
|
|
if (Ember.CoreView.detect(view)) { |
|
attrs.templateData = attrs.templateData || get(this, 'templateData'); |
|
|
|
attrs.container = this.container; |
|
view = view.create(attrs); |
|
|
|
// don't set the property on a virtual view, as they are invisible to |
|
// consumers of the view API |
|
if (view.viewName) { |
|
set(get(this, 'concreteView'), view.viewName, view); |
|
} |
|
} else if ('string' === typeof view) { |
|
var fullName = 'view:' + view; |
|
var View = this.container.lookupFactory(fullName); |
|
|
|
Ember.assert("Could not find view: '" + fullName + "'", !!View); |
|
|
|
attrs.templateData = get(this, 'templateData'); |
|
view = View.create(attrs); |
|
} else { |
|
Ember.assert('You must pass instance or subclass of View', view.isView); |
|
attrs.container = this.container; |
|
|
|
if (!get(view, 'templateData')) { |
|
attrs.templateData = get(this, 'templateData'); |
|
} |
|
|
|
Ember.setProperties(view, attrs); |
|
|
|
} |
|
|
|
return view; |
|
}, |
|
|
|
becameVisible: Ember.K, |
|
becameHidden: Ember.K, |
|
|
|
/** |
|
When the view's `isVisible` property changes, toggle the visibility |
|
element of the actual DOM element. |
|
|
|
@method _isVisibleDidChange |
|
@private |
|
*/ |
|
_isVisibleDidChange: Ember.observer('isVisible', function() { |
|
if (this._isVisible === get(this, 'isVisible')) { return ; } |
|
Ember.run.scheduleOnce('render', this, this._toggleVisibility); |
|
}), |
|
|
|
_toggleVisibility: function() { |
|
var $el = this.$(); |
|
if (!$el) { return; } |
|
|
|
var isVisible = get(this, 'isVisible'); |
|
|
|
if (this._isVisible === isVisible) { return ; } |
|
|
|
$el.toggle(isVisible); |
|
|
|
this._isVisible = isVisible; |
|
|
|
if (this._isAncestorHidden()) { return; } |
|
|
|
if (isVisible) { |
|
this._notifyBecameVisible(); |
|
} else { |
|
this._notifyBecameHidden(); |
|
} |
|
}, |
|
|
|
_notifyBecameVisible: function() { |
|
this.trigger('becameVisible'); |
|
|
|
this.forEachChildView(function(view) { |
|
var isVisible = get(view, 'isVisible'); |
|
|
|
if (isVisible || isVisible === null) { |
|
view._notifyBecameVisible(); |
|
} |
|
}); |
|
}, |
|
|
|
_notifyBecameHidden: function() { |
|
this.trigger('becameHidden'); |
|
this.forEachChildView(function(view) { |
|
var isVisible = get(view, 'isVisible'); |
|
|
|
if (isVisible || isVisible === null) { |
|
view._notifyBecameHidden(); |
|
} |
|
}); |
|
}, |
|
|
|
_isAncestorHidden: function() { |
|
var parent = get(this, 'parentView'); |
|
|
|
while (parent) { |
|
if (get(parent, 'isVisible') === false) { return true; } |
|
|
|
parent = get(parent, 'parentView'); |
|
} |
|
|
|
return false; |
|
}, |
|
|
|
clearBuffer: function() { |
|
this.invokeRecursively(nullViewsBuffer); |
|
}, |
|
|
|
transitionTo: function(state, children) { |
|
var priorState = this.currentState, |
|
currentState = this.currentState = this.states[state]; |
|
this.state = state; |
|
|
|
if (priorState && priorState.exit) { priorState.exit(this); } |
|
if (currentState.enter) { currentState.enter(this); } |
|
if (state === 'inDOM') { delete Ember.meta(this).cache.element; } |
|
|
|
if (children !== false) { |
|
this.forEachChildView(function(view) { |
|
view.transitionTo(state); |
|
}); |
|
} |
|
}, |
|
|
|
// ....................................................... |
|
// EVENT HANDLING |
|
// |
|
|
|
/** |
|
Handle events from `Ember.EventDispatcher` |
|
|
|
@method handleEvent |
|
@param eventName {String} |
|
@param evt {Event} |
|
@private |
|
*/ |
|
handleEvent: function(eventName, evt) { |
|
return this.currentState.handleEvent(this, eventName, evt); |
|
}, |
|
|
|
registerObserver: function(root, path, target, observer) { |
|
if (!observer && 'function' === typeof target) { |
|
observer = target; |
|
target = null; |
|
} |
|
|
|
if (!root || typeof root !== 'object') { |
|
return; |
|
} |
|
|
|
var view = this, |
|
stateCheckedObserver = function() { |
|
view.currentState.invokeObserver(this, observer); |
|
}, |
|
scheduledObserver = function() { |
|
Ember.run.scheduleOnce('render', this, stateCheckedObserver); |
|
}; |
|
|
|
Ember.addObserver(root, path, target, scheduledObserver); |
|
|
|
this.one('willClearRender', function() { |
|
Ember.removeObserver(root, path, target, scheduledObserver); |
|
}); |
|
} |
|
|
|
}); |
|
|
|
/* |
|
Describe how the specified actions should behave in the various |
|
states that a view can exist in. Possible states: |
|
|
|
* preRender: when a view is first instantiated, and after its |
|
element was destroyed, it is in the preRender state |
|
* inBuffer: once a view has been rendered, but before it has |
|
been inserted into the DOM, it is in the inBuffer state |
|
* hasElement: the DOM representation of the view is created, |
|
and is ready to be inserted |
|
* inDOM: once a view has been inserted into the DOM it is in |
|
the inDOM state. A view spends the vast majority of its |
|
existence in this state. |
|
* destroyed: once a view has been destroyed (using the destroy |
|
method), it is in this state. No further actions can be invoked |
|
on a destroyed view. |
|
*/ |
|
|
|
// in the destroyed state, everything is illegal |
|
|
|
// before rendering has begun, all legal manipulations are noops. |
|
|
|
// inside the buffer, legal manipulations are done on the buffer |
|
|
|
// once the view has been inserted into the DOM, legal manipulations |
|
// are done on the DOM element. |
|
|
|
function notifyMutationListeners() { |
|
Ember.run.once(Ember.View, 'notifyMutationListeners'); |
|
} |
|
|
|
var DOMManager = { |
|
prepend: function(view, html) { |
|
view.$().prepend(html); |
|
notifyMutationListeners(); |
|
}, |
|
|
|
after: function(view, html) { |
|
view.$().after(html); |
|
notifyMutationListeners(); |
|
}, |
|
|
|
html: function(view, html) { |
|
view.$().html(html); |
|
notifyMutationListeners(); |
|
}, |
|
|
|
replace: function(view) { |
|
var element = get(view, 'element'); |
|
|
|
set(view, 'element', null); |
|
|
|
view._insertElementLater(function() { |
|
Ember.$(element).replaceWith(get(view, 'element')); |
|
notifyMutationListeners(); |
|
}); |
|
}, |
|
|
|
remove: function(view) { |
|
view.$().remove(); |
|
notifyMutationListeners(); |
|
}, |
|
|
|
empty: function(view) { |
|
view.$().empty(); |
|
notifyMutationListeners(); |
|
} |
|
}; |
|
|
|
Ember.View.reopen({ |
|
domManager: DOMManager |
|
}); |
|
|
|
Ember.View.reopenClass({ |
|
|
|
/** |
|
Parse a path and return an object which holds the parsed properties. |
|
|
|
For example a path like "content.isEnabled:enabled:disabled" will return the |
|
following object: |
|
|
|
```javascript |
|
{ |
|
path: "content.isEnabled", |
|
className: "enabled", |
|
falsyClassName: "disabled", |
|
classNames: ":enabled:disabled" |
|
} |
|
``` |
|
|
|
@method _parsePropertyPath |
|
@static |
|
@private |
|
*/ |
|
_parsePropertyPath: function(path) { |
|
var split = path.split(':'), |
|
propertyPath = split[0], |
|
classNames = "", |
|
className, |
|
falsyClassName; |
|
|
|
// check if the property is defined as prop:class or prop:trueClass:falseClass |
|
if (split.length > 1) { |
|
className = split[1]; |
|
if (split.length === 3) { falsyClassName = split[2]; } |
|
|
|
classNames = ':' + className; |
|
if (falsyClassName) { classNames += ":" + falsyClassName; } |
|
} |
|
|
|
return { |
|
path: propertyPath, |
|
classNames: classNames, |
|
className: (className === '') ? undefined : className, |
|
falsyClassName: falsyClassName |
|
}; |
|
}, |
|
|
|
/** |
|
Get the class name for a given value, based on the path, optional |
|
`className` and optional `falsyClassName`. |
|
|
|
- if a `className` or `falsyClassName` has been specified: |
|
- if the value is truthy and `className` has been specified, |
|
`className` is returned |
|
- if the value is falsy and `falsyClassName` has been specified, |
|
`falsyClassName` is returned |
|
- otherwise `null` is returned |
|
- if the value is `true`, the dasherized last part of the supplied path |
|
is returned |
|
- if the value is not `false`, `undefined` or `null`, the `value` |
|
is returned |
|
- if none of the above rules apply, `null` is returned |
|
|
|
@method _classStringForValue |
|
@param path |
|
@param val |
|
@param className |
|
@param falsyClassName |
|
@static |
|
@private |
|
*/ |
|
_classStringForValue: function(path, val, className, falsyClassName) { |
|
// When using the colon syntax, evaluate the truthiness or falsiness |
|
// of the value to determine which className to return |
|
if (className || falsyClassName) { |
|
if (className && !!val) { |
|
return className; |
|
|
|
} else if (falsyClassName && !val) { |
|
return falsyClassName; |
|
|
|
} else { |
|
return null; |
|
} |
|
|
|
// If value is a Boolean and true, return the dasherized property |
|
// name. |
|
} else if (val === true) { |
|
// Normalize property path to be suitable for use |
|
// as a class name. For exaple, content.foo.barBaz |
|
// becomes bar-baz. |
|
var parts = path.split('.'); |
|
return Ember.String.dasherize(parts[parts.length-1]); |
|
|
|
// If the value is not false, undefined, or null, return the current |
|
// value of the property. |
|
} else if (val !== false && val != null) { |
|
return val; |
|
|
|
// Nothing to display. Return null so that the old class is removed |
|
// but no new class is added. |
|
} else { |
|
return null; |
|
} |
|
} |
|
}); |
|
|
|
var mutation = Ember.Object.extend(Ember.Evented).create(); |
|
|
|
Ember.View.addMutationListener = function(callback) { |
|
mutation.on('change', callback); |
|
}; |
|
|
|
Ember.View.removeMutationListener = function(callback) { |
|
mutation.off('change', callback); |
|
}; |
|
|
|
Ember.View.notifyMutationListeners = function() { |
|
mutation.trigger('change'); |
|
}; |
|
|
|
/** |
|
Global views hash |
|
|
|
@property views |
|
@static |
|
@type Hash |
|
*/ |
|
Ember.View.views = {}; |
|
|
|
// If someone overrides the child views computed property when |
|
// defining their class, we want to be able to process the user's |
|
// supplied childViews and then restore the original computed property |
|
// at view initialization time. This happens in Ember.ContainerView's init |
|
// method. |
|
Ember.View.childViewsProperty = childViewsProperty; |
|
|
|
Ember.View.applyAttributeBindings = function(elem, name, value) { |
|
var type = Ember.typeOf(value); |
|
|
|
// if this changes, also change the logic in ember-handlebars/lib/helpers/binding.js |
|
if (name !== 'value' && (type === 'string' || (type === 'number' && !isNaN(value)))) { |
|
if (value !== elem.attr(name)) { |
|
elem.attr(name, value); |
|
} |
|
} else if (name === 'value' || type === 'boolean') { |
|
if (Ember.isNone(value) || value === false) { |
|
// `null`, `undefined` or `false` should remove attribute |
|
elem.removeAttr(name); |
|
elem.prop(name, ''); |
|
} else if (value !== elem.prop(name)) { |
|
// value should always be properties |
|
elem.prop(name, value); |
|
} |
|
} else if (!value) { |
|
elem.removeAttr(name); |
|
} |
|
}; |
|
|
|
Ember.View.states = states; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
Ember.View.states._default = { |
|
// appendChild is only legal while rendering the buffer. |
|
appendChild: function() { |
|
throw "You can't use appendChild outside of the rendering process"; |
|
}, |
|
|
|
$: function() { |
|
return undefined; |
|
}, |
|
|
|
getElement: function() { |
|
return null; |
|
}, |
|
|
|
// Handle events from `Ember.EventDispatcher` |
|
handleEvent: function() { |
|
return true; // continue event propagation |
|
}, |
|
|
|
destroyElement: function(view) { |
|
set(view, 'element', null); |
|
if (view._scheduledInsert) { |
|
Ember.run.cancel(view._scheduledInsert); |
|
view._scheduledInsert = null; |
|
} |
|
return view; |
|
}, |
|
|
|
renderToBufferIfNeeded: function () { |
|
return false; |
|
}, |
|
|
|
rerender: Ember.K, |
|
invokeObserver: Ember.K |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var preRender = Ember.View.states.preRender = Ember.create(Ember.View.states._default); |
|
|
|
Ember.merge(preRender, { |
|
// a view leaves the preRender state once its element has been |
|
// created (createElement). |
|
insertElement: function(view, fn) { |
|
view.createElement(); |
|
var viewCollection = view.viewHierarchyCollection(); |
|
|
|
viewCollection.trigger('willInsertElement'); |
|
|
|
fn.call(view); |
|
|
|
// We transition to `inDOM` if the element exists in the DOM |
|
var element = view.get('element'); |
|
if (document.body.contains(element)) { |
|
viewCollection.transitionTo('inDOM', false); |
|
viewCollection.trigger('didInsertElement'); |
|
} |
|
}, |
|
|
|
renderToBufferIfNeeded: function(view, buffer) { |
|
view.renderToBuffer(buffer); |
|
return true; |
|
}, |
|
|
|
empty: Ember.K, |
|
|
|
setElement: function(view, value) { |
|
if (value !== null) { |
|
view.transitionTo('hasElement'); |
|
} |
|
return value; |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
var inBuffer = Ember.View.states.inBuffer = Ember.create(Ember.View.states._default); |
|
|
|
Ember.merge(inBuffer, { |
|
$: function(view, sel) { |
|
// if we don't have an element yet, someone calling this.$() is |
|
// trying to update an element that isn't in the DOM. Instead, |
|
// rerender the view to allow the render method to reflect the |
|
// changes. |
|
view.rerender(); |
|
return Ember.$(); |
|
}, |
|
|
|
// when a view is rendered in a buffer, rerendering it simply |
|
// replaces the existing buffer with a new one |
|
rerender: function(view) { |
|
throw new Ember.Error("Something you did caused a view to re-render after it rendered but before it was inserted into the DOM."); |
|
}, |
|
|
|
// when a view is rendered in a buffer, appending a child |
|
// view will render that view and append the resulting |
|
// buffer into its buffer. |
|
appendChild: function(view, childView, options) { |
|
var buffer = view.buffer, _childViews = view._childViews; |
|
|
|
childView = view.createChildView(childView, options); |
|
if (!_childViews.length) { _childViews = view._childViews = _childViews.slice(); } |
|
_childViews.push(childView); |
|
|
|
childView.renderToBuffer(buffer); |
|
|
|
view.propertyDidChange('childViews'); |
|
|
|
return childView; |
|
}, |
|
|
|
// when a view is rendered in a buffer, destroying the |
|
// element will simply destroy the buffer and put the |
|
// state back into the preRender state. |
|
destroyElement: function(view) { |
|
view.clearBuffer(); |
|
var viewCollection = view._notifyWillDestroyElement(); |
|
viewCollection.transitionTo('preRender', false); |
|
|
|
return view; |
|
}, |
|
|
|
empty: function() { |
|
Ember.assert("Emptying a view in the inBuffer state is not allowed and " + |
|
"should not happen under normal circumstances. Most likely " + |
|
"there is a bug in your application. This may be due to " + |
|
"excessive property change notifications."); |
|
}, |
|
|
|
renderToBufferIfNeeded: function (view, buffer) { |
|
return false; |
|
}, |
|
|
|
// It should be impossible for a rendered view to be scheduled for |
|
// insertion. |
|
insertElement: function() { |
|
throw "You can't insert an element that has already been rendered"; |
|
}, |
|
|
|
setElement: function(view, value) { |
|
if (value === null) { |
|
view.transitionTo('preRender'); |
|
} else { |
|
view.clearBuffer(); |
|
view.transitionTo('hasElement'); |
|
} |
|
|
|
return value; |
|
}, |
|
|
|
invokeObserver: function(target, observer) { |
|
observer.call(target); |
|
} |
|
}); |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
var hasElement = Ember.View.states.hasElement = Ember.create(Ember.View.states._default); |
|
|
|
Ember.merge(hasElement, { |
|
$: function(view, sel) { |
|
var elem = get(view, 'element'); |
|
return sel ? Ember.$(sel, elem) : Ember.$(elem); |
|
}, |
|
|
|
getElement: function(view) { |
|
var parent = get(view, 'parentView'); |
|
if (parent) { parent = get(parent, 'element'); } |
|
if (parent) { return view.findElementInParentElement(parent); } |
|
return Ember.$("#" + get(view, 'elementId'))[0]; |
|
}, |
|
|
|
setElement: function(view, value) { |
|
if (value === null) { |
|
view.transitionTo('preRender'); |
|
} else { |
|
throw "You cannot set an element to a non-null value when the element is already in the DOM."; |
|
} |
|
|
|
return value; |
|
}, |
|
|
|
// once the view has been inserted into the DOM, rerendering is |
|
// deferred to allow bindings to synchronize. |
|
rerender: function(view) { |
|
view.triggerRecursively('willClearRender'); |
|
|
|
view.clearRenderedChildren(); |
|
|
|
view.domManager.replace(view); |
|
return view; |
|
}, |
|
|
|
// once the view is already in the DOM, destroying it removes it |
|
// from the DOM, nukes its element, and puts it back into the |
|
// preRender state if inDOM. |
|
|
|
destroyElement: function(view) { |
|
view._notifyWillDestroyElement(); |
|
view.domManager.remove(view); |
|
set(view, 'element', null); |
|
if (view._scheduledInsert) { |
|
Ember.run.cancel(view._scheduledInsert); |
|
view._scheduledInsert = null; |
|
} |
|
return view; |
|
}, |
|
|
|
empty: function(view) { |
|
var _childViews = view._childViews, len, idx; |
|
if (_childViews) { |
|
len = _childViews.length; |
|
for (idx = 0; idx < len; idx++) { |
|
_childViews[idx]._notifyWillDestroyElement(); |
|
} |
|
} |
|
view.domManager.empty(view); |
|
}, |
|
|
|
// Handle events from `Ember.EventDispatcher` |
|
handleEvent: function(view, eventName, evt) { |
|
if (view.has(eventName)) { |
|
// Handler should be able to re-dispatch events, so we don't |
|
// preventDefault or stopPropagation. |
|
return view.trigger(eventName, evt); |
|
} else { |
|
return true; // continue event propagation |
|
} |
|
}, |
|
|
|
invokeObserver: function(target, observer) { |
|
observer.call(target); |
|
} |
|
}); |
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var hasElement = Ember.View.states.hasElement; |
|
var inDOM = Ember.View.states.inDOM = Ember.create(hasElement); |
|
|
|
Ember.merge(inDOM, { |
|
enter: function(view) { |
|
// Register the view for event handling. This hash is used by |
|
// Ember.EventDispatcher to dispatch incoming events. |
|
if (!view.isVirtual) { |
|
Ember.assert("Attempted to register a view with an id already in use: "+view.elementId, !Ember.View.views[view.elementId]); |
|
Ember.View.views[view.elementId] = view; |
|
} |
|
|
|
view.addBeforeObserver('elementId', function() { |
|
throw new Ember.Error("Changing a view's elementId after creation is not allowed"); |
|
}); |
|
}, |
|
|
|
exit: function(view) { |
|
if (!this.isVirtual) delete Ember.View.views[view.elementId]; |
|
}, |
|
|
|
insertElement: function(view, fn) { |
|
throw "You can't insert an element into the DOM that has already been inserted"; |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var destroyingError = "You can't call %@ on a view being destroyed", fmt = Ember.String.fmt; |
|
|
|
var destroying = Ember.View.states.destroying = Ember.create(Ember.View.states._default); |
|
|
|
Ember.merge(destroying, { |
|
appendChild: function() { |
|
throw fmt(destroyingError, ['appendChild']); |
|
}, |
|
rerender: function() { |
|
throw fmt(destroyingError, ['rerender']); |
|
}, |
|
destroyElement: function() { |
|
throw fmt(destroyingError, ['destroyElement']); |
|
}, |
|
empty: function() { |
|
throw fmt(destroyingError, ['empty']); |
|
}, |
|
|
|
setElement: function() { |
|
throw fmt(destroyingError, ["set('element', ...)"]); |
|
}, |
|
|
|
renderToBufferIfNeeded: function() { |
|
return false; |
|
}, |
|
|
|
// Since element insertion is scheduled, don't do anything if |
|
// the view has been destroyed between scheduling and execution |
|
insertElement: Ember.K |
|
}); |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
Ember.View.cloneStates = function(from) { |
|
var into = {}; |
|
|
|
into._default = {}; |
|
into.preRender = Ember.create(into._default); |
|
into.destroying = Ember.create(into._default); |
|
into.inBuffer = Ember.create(into._default); |
|
into.hasElement = Ember.create(into._default); |
|
into.inDOM = Ember.create(into.hasElement); |
|
|
|
for (var stateName in from) { |
|
if (!from.hasOwnProperty(stateName)) { continue; } |
|
Ember.merge(into[stateName], from[stateName]); |
|
} |
|
|
|
return into; |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var states = Ember.View.cloneStates(Ember.View.states); |
|
|
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
var forEach = Ember.EnumerableUtils.forEach; |
|
var ViewCollection = Ember._ViewCollection; |
|
|
|
/** |
|
A `ContainerView` is an `Ember.View` subclass that implements `Ember.MutableArray` |
|
allowing programmatic management of its child views. |
|
|
|
## Setting Initial Child Views |
|
|
|
The initial array of child views can be set in one of two ways. You can |
|
provide a `childViews` property at creation time that contains instance of |
|
`Ember.View`: |
|
|
|
```javascript |
|
aContainer = Ember.ContainerView.create({ |
|
childViews: [Ember.View.create(), Ember.View.create()] |
|
}); |
|
``` |
|
|
|
You can also provide a list of property names whose values are instances of |
|
`Ember.View`: |
|
|
|
```javascript |
|
aContainer = Ember.ContainerView.create({ |
|
childViews: ['aView', 'bView', 'cView'], |
|
aView: Ember.View.create(), |
|
bView: Ember.View.create(), |
|
cView: Ember.View.create() |
|
}); |
|
``` |
|
|
|
The two strategies can be combined: |
|
|
|
```javascript |
|
aContainer = Ember.ContainerView.create({ |
|
childViews: ['aView', Ember.View.create()], |
|
aView: Ember.View.create() |
|
}); |
|
``` |
|
|
|
Each child view's rendering will be inserted into the container's rendered |
|
HTML in the same order as its position in the `childViews` property. |
|
|
|
## Adding and Removing Child Views |
|
|
|
The container view implements `Ember.MutableArray` allowing programmatic management of its child views. |
|
|
|
To remove a view, pass that view into a `removeObject` call on the container view. |
|
|
|
Given an empty `<body>` the following code |
|
|
|
```javascript |
|
aContainer = Ember.ContainerView.create({ |
|
classNames: ['the-container'], |
|
childViews: ['aView', 'bView'], |
|
aView: Ember.View.create({ |
|
template: Ember.Handlebars.compile("A") |
|
}), |
|
bView: Ember.View.create({ |
|
template: Ember.Handlebars.compile("B") |
|
}) |
|
}); |
|
|
|
aContainer.appendTo('body'); |
|
``` |
|
|
|
Results in the HTML |
|
|
|
```html |
|
<div class="ember-view the-container"> |
|
<div class="ember-view">A</div> |
|
<div class="ember-view">B</div> |
|
</div> |
|
``` |
|
|
|
Removing a view |
|
|
|
```javascript |
|
aContainer.toArray(); // [aContainer.aView, aContainer.bView] |
|
aContainer.removeObject(aContainer.get('bView')); |
|
aContainer.toArray(); // [aContainer.aView] |
|
``` |
|
|
|
Will result in the following HTML |
|
|
|
```html |
|
<div class="ember-view the-container"> |
|
<div class="ember-view">A</div> |
|
</div> |
|
``` |
|
|
|
Similarly, adding a child view is accomplished by adding `Ember.View` instances to the |
|
container view. |
|
|
|
Given an empty `<body>` the following code |
|
|
|
```javascript |
|
aContainer = Ember.ContainerView.create({ |
|
classNames: ['the-container'], |
|
childViews: ['aView', 'bView'], |
|
aView: Ember.View.create({ |
|
template: Ember.Handlebars.compile("A") |
|
}), |
|
bView: Ember.View.create({ |
|
template: Ember.Handlebars.compile("B") |
|
}) |
|
}); |
|
|
|
aContainer.appendTo('body'); |
|
``` |
|
|
|
Results in the HTML |
|
|
|
```html |
|
<div class="ember-view the-container"> |
|
<div class="ember-view">A</div> |
|
<div class="ember-view">B</div> |
|
</div> |
|
``` |
|
|
|
Adding a view |
|
|
|
```javascript |
|
AnotherViewClass = Ember.View.extend({ |
|
template: Ember.Handlebars.compile("Another view") |
|
}); |
|
|
|
aContainer.toArray(); // [aContainer.aView, aContainer.bView] |
|
aContainer.pushObject(AnotherViewClass.create()); |
|
aContainer.toArray(); // [aContainer.aView, aContainer.bView, <AnotherViewClass instance>] |
|
``` |
|
|
|
Will result in the following HTML |
|
|
|
```html |
|
<div class="ember-view the-container"> |
|
<div class="ember-view">A</div> |
|
<div class="ember-view">B</div> |
|
<div class="ember-view">Another view</div> |
|
</div> |
|
``` |
|
|
|
## Templates and Layout |
|
|
|
A `template`, `templateName`, `defaultTemplate`, `layout`, `layoutName` or |
|
`defaultLayout` property on a container view will not result in the template |
|
or layout being rendered. The HTML contents of a `Ember.ContainerView`'s DOM |
|
representation will only be the rendered HTML of its child views. |
|
|
|
@class ContainerView |
|
@namespace Ember |
|
@extends Ember.View |
|
*/ |
|
Ember.ContainerView = Ember.View.extend(Ember.MutableArray, { |
|
states: states, |
|
|
|
init: function() { |
|
this._super(); |
|
|
|
var childViews = get(this, 'childViews'); |
|
|
|
// redefine view's childViews property that was obliterated |
|
Ember.defineProperty(this, 'childViews', Ember.View.childViewsProperty); |
|
|
|
var _childViews = this._childViews; |
|
|
|
forEach(childViews, function(viewName, idx) { |
|
var view; |
|
|
|
if ('string' === typeof viewName) { |
|
view = get(this, viewName); |
|
view = this.createChildView(view); |
|
set(this, viewName, view); |
|
} else { |
|
view = this.createChildView(viewName); |
|
} |
|
|
|
_childViews[idx] = view; |
|
}, this); |
|
|
|
var currentView = get(this, 'currentView'); |
|
if (currentView) { |
|
if (!_childViews.length) { _childViews = this._childViews = this._childViews.slice(); } |
|
_childViews.push(this.createChildView(currentView)); |
|
} |
|
}, |
|
|
|
replace: function(idx, removedCount, addedViews) { |
|
var addedCount = addedViews ? get(addedViews, 'length') : 0; |
|
var self = this; |
|
Ember.assert("You can't add a child to a container that is already a child of another view", Ember.A(addedViews).every(function(item) { return !get(item, '_parentView') || get(item, '_parentView') === self; })); |
|
|
|
this.arrayContentWillChange(idx, removedCount, addedCount); |
|
this.childViewsWillChange(this._childViews, idx, removedCount); |
|
|
|
if (addedCount === 0) { |
|
this._childViews.splice(idx, removedCount) ; |
|
} else { |
|
var args = [idx, removedCount].concat(addedViews); |
|
if (addedViews.length && !this._childViews.length) { this._childViews = this._childViews.slice(); } |
|
this._childViews.splice.apply(this._childViews, args); |
|
} |
|
|
|
this.arrayContentDidChange(idx, removedCount, addedCount); |
|
this.childViewsDidChange(this._childViews, idx, removedCount, addedCount); |
|
|
|
return this; |
|
}, |
|
|
|
objectAt: function(idx) { |
|
return this._childViews[idx]; |
|
}, |
|
|
|
length: Ember.computed(function () { |
|
return this._childViews.length; |
|
}).volatile(), |
|
|
|
/** |
|
Instructs each child view to render to the passed render buffer. |
|
|
|
@private |
|
@method render |
|
@param {Ember.RenderBuffer} buffer the buffer to render to |
|
*/ |
|
render: function(buffer) { |
|
this.forEachChildView(function(view) { |
|
view.renderToBuffer(buffer); |
|
}); |
|
}, |
|
|
|
instrumentName: 'container', |
|
|
|
/** |
|
When a child view is removed, destroy its element so that |
|
it is removed from the DOM. |
|
|
|
The array observer that triggers this action is set up in the |
|
`renderToBuffer` method. |
|
|
|
@private |
|
@method childViewsWillChange |
|
@param {Ember.Array} views the child views array before mutation |
|
@param {Number} start the start position of the mutation |
|
@param {Number} removed the number of child views removed |
|
**/ |
|
childViewsWillChange: function(views, start, removed) { |
|
this.propertyWillChange('childViews'); |
|
|
|
if (removed > 0) { |
|
var changedViews = views.slice(start, start+removed); |
|
// transition to preRender before clearing parentView |
|
this.currentState.childViewsWillChange(this, views, start, removed); |
|
this.initializeViews(changedViews, null, null); |
|
} |
|
}, |
|
|
|
removeChild: function(child) { |
|
this.removeObject(child); |
|
return this; |
|
}, |
|
|
|
/** |
|
When a child view is added, make sure the DOM gets updated appropriately. |
|
|
|
If the view has already rendered an element, we tell the child view to |
|
create an element and insert it into the DOM. If the enclosing container |
|
view has already written to a buffer, but not yet converted that buffer |
|
into an element, we insert the string representation of the child into the |
|
appropriate place in the buffer. |
|
|
|
@private |
|
@method childViewsDidChange |
|
@param {Ember.Array} views the array of child views afte the mutation has occurred |
|
@param {Number} start the start position of the mutation |
|
@param {Number} removed the number of child views removed |
|
@param {Number} the number of child views added |
|
*/ |
|
childViewsDidChange: function(views, start, removed, added) { |
|
if (added > 0) { |
|
var changedViews = views.slice(start, start+added); |
|
this.initializeViews(changedViews, this, get(this, 'templateData')); |
|
this.currentState.childViewsDidChange(this, views, start, added); |
|
} |
|
this.propertyDidChange('childViews'); |
|
}, |
|
|
|
initializeViews: function(views, parentView, templateData) { |
|
forEach(views, function(view) { |
|
set(view, '_parentView', parentView); |
|
|
|
if (!view.container && parentView) { |
|
set(view, 'container', parentView.container); |
|
} |
|
|
|
if (!get(view, 'templateData')) { |
|
set(view, 'templateData', templateData); |
|
} |
|
}); |
|
}, |
|
|
|
currentView: null, |
|
|
|
_currentViewWillChange: Ember.beforeObserver('currentView', function() { |
|
var currentView = get(this, 'currentView'); |
|
if (currentView) { |
|
currentView.destroy(); |
|
} |
|
}), |
|
|
|
_currentViewDidChange: Ember.observer('currentView', function() { |
|
var currentView = get(this, 'currentView'); |
|
if (currentView) { |
|
Ember.assert("You tried to set a current view that already has a parent. Make sure you don't have multiple outlets in the same view.", !get(currentView, '_parentView')); |
|
this.pushObject(currentView); |
|
} |
|
}), |
|
|
|
_ensureChildrenAreInDOM: function () { |
|
this.currentState.ensureChildrenAreInDOM(this); |
|
} |
|
}); |
|
|
|
Ember.merge(states._default, { |
|
childViewsWillChange: Ember.K, |
|
childViewsDidChange: Ember.K, |
|
ensureChildrenAreInDOM: Ember.K |
|
}); |
|
|
|
Ember.merge(states.inBuffer, { |
|
childViewsDidChange: function(parentView, views, start, added) { |
|
throw new Ember.Error('You cannot modify child views while in the inBuffer state'); |
|
} |
|
}); |
|
|
|
Ember.merge(states.hasElement, { |
|
childViewsWillChange: function(view, views, start, removed) { |
|
for (var i=start; i<start+removed; i++) { |
|
views[i].remove(); |
|
} |
|
}, |
|
|
|
childViewsDidChange: function(view, views, start, added) { |
|
Ember.run.scheduleOnce('render', view, '_ensureChildrenAreInDOM'); |
|
}, |
|
|
|
ensureChildrenAreInDOM: function(view) { |
|
var childViews = view._childViews, i, len, childView, previous, buffer, viewCollection = new ViewCollection(); |
|
|
|
for (i = 0, len = childViews.length; i < len; i++) { |
|
childView = childViews[i]; |
|
|
|
if (!buffer) { buffer = Ember.RenderBuffer(); buffer._hasElement = false; } |
|
|
|
if (childView.renderToBufferIfNeeded(buffer)) { |
|
viewCollection.push(childView); |
|
} else if (viewCollection.length) { |
|
insertViewCollection(view, viewCollection, previous, buffer); |
|
buffer = null; |
|
previous = childView; |
|
viewCollection.clear(); |
|
} else { |
|
previous = childView; |
|
} |
|
} |
|
|
|
if (viewCollection.length) { |
|
insertViewCollection(view, viewCollection, previous, buffer); |
|
} |
|
} |
|
}); |
|
|
|
function insertViewCollection(view, viewCollection, previous, buffer) { |
|
viewCollection.triggerRecursively('willInsertElement'); |
|
|
|
if (previous) { |
|
previous.domManager.after(previous, buffer.string()); |
|
} else { |
|
view.domManager.prepend(view, buffer.string()); |
|
} |
|
|
|
viewCollection.forEach(function(v) { |
|
v.transitionTo('inDOM'); |
|
v.propertyDidChange('element'); |
|
v.triggerRecursively('didInsertElement'); |
|
}); |
|
} |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; |
|
|
|
/** |
|
`Ember.CollectionView` is an `Ember.View` descendent responsible for managing |
|
a collection (an array or array-like object) by maintaining a child view object |
|
and associated DOM representation for each item in the array and ensuring |
|
that child views and their associated rendered HTML are updated when items in |
|
the array are added, removed, or replaced. |
|
|
|
## Setting content |
|
|
|
The managed collection of objects is referenced as the `Ember.CollectionView` |
|
instance's `content` property. |
|
|
|
```javascript |
|
someItemsView = Ember.CollectionView.create({ |
|
content: ['A', 'B','C'] |
|
}) |
|
``` |
|
|
|
The view for each item in the collection will have its `content` property set |
|
to the item. |
|
|
|
## Specifying itemViewClass |
|
|
|
By default the view class for each item in the managed collection will be an |
|
instance of `Ember.View`. You can supply a different class by setting the |
|
`CollectionView`'s `itemViewClass` property. |
|
|
|
Given an empty `<body>` and the following code: |
|
|
|
```javascript |
|
someItemsView = Ember.CollectionView.create({ |
|
classNames: ['a-collection'], |
|
content: ['A','B','C'], |
|
itemViewClass: Ember.View.extend({ |
|
template: Ember.Handlebars.compile("the letter: {{view.content}}") |
|
}) |
|
}); |
|
|
|
someItemsView.appendTo('body'); |
|
``` |
|
|
|
Will result in the following HTML structure |
|
|
|
```html |
|
<div class="ember-view a-collection"> |
|
<div class="ember-view">the letter: A</div> |
|
<div class="ember-view">the letter: B</div> |
|
<div class="ember-view">the letter: C</div> |
|
</div> |
|
``` |
|
|
|
## Automatic matching of parent/child tagNames |
|
|
|
Setting the `tagName` property of a `CollectionView` to any of |
|
"ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result |
|
in the item views receiving an appropriately matched `tagName` property. |
|
|
|
Given an empty `<body>` and the following code: |
|
|
|
```javascript |
|
anUnorderedListView = Ember.CollectionView.create({ |
|
tagName: 'ul', |
|
content: ['A','B','C'], |
|
itemViewClass: Ember.View.extend({ |
|
template: Ember.Handlebars.compile("the letter: {{view.content}}") |
|
}) |
|
}); |
|
|
|
anUnorderedListView.appendTo('body'); |
|
``` |
|
|
|
Will result in the following HTML structure |
|
|
|
```html |
|
<ul class="ember-view a-collection"> |
|
<li class="ember-view">the letter: A</li> |
|
<li class="ember-view">the letter: B</li> |
|
<li class="ember-view">the letter: C</li> |
|
</ul> |
|
``` |
|
|
|
Additional `tagName` pairs can be provided by adding to |
|
`Ember.CollectionView.CONTAINER_MAP ` |
|
|
|
```javascript |
|
Ember.CollectionView.CONTAINER_MAP['article'] = 'section' |
|
``` |
|
|
|
## Programmatic creation of child views |
|
|
|
For cases where additional customization beyond the use of a single |
|
`itemViewClass` or `tagName` matching is required CollectionView's |
|
`createChildView` method can be overidden: |
|
|
|
```javascript |
|
CustomCollectionView = Ember.CollectionView.extend({ |
|
createChildView: function(viewClass, attrs) { |
|
if (attrs.content.kind == 'album') { |
|
viewClass = App.AlbumView; |
|
} else { |
|
viewClass = App.SongView; |
|
} |
|
return this._super(viewClass, attrs); |
|
} |
|
}); |
|
``` |
|
|
|
## Empty View |
|
|
|
You can provide an `Ember.View` subclass to the `Ember.CollectionView` |
|
instance as its `emptyView` property. If the `content` property of a |
|
`CollectionView` is set to `null` or an empty array, an instance of this view |
|
will be the `CollectionView`s only child. |
|
|
|
```javascript |
|
aListWithNothing = Ember.CollectionView.create({ |
|
classNames: ['nothing'] |
|
content: null, |
|
emptyView: Ember.View.extend({ |
|
template: Ember.Handlebars.compile("The collection is empty") |
|
}) |
|
}); |
|
|
|
aListWithNothing.appendTo('body'); |
|
``` |
|
|
|
Will result in the following HTML structure |
|
|
|
```html |
|
<div class="ember-view nothing"> |
|
<div class="ember-view"> |
|
The collection is empty |
|
</div> |
|
</div> |
|
``` |
|
|
|
## Adding and Removing items |
|
|
|
The `childViews` property of a `CollectionView` should not be directly |
|
manipulated. Instead, add, remove, replace items from its `content` property. |
|
This will trigger appropriate changes to its rendered HTML. |
|
|
|
|
|
@class CollectionView |
|
@namespace Ember |
|
@extends Ember.ContainerView |
|
@since Ember 0.9 |
|
*/ |
|
Ember.CollectionView = Ember.ContainerView.extend({ |
|
|
|
/** |
|
A list of items to be displayed by the `Ember.CollectionView`. |
|
|
|
@property content |
|
@type Ember.Array |
|
@default null |
|
*/ |
|
content: null, |
|
|
|
/** |
|
This provides metadata about what kind of empty view class this |
|
collection would like if it is being instantiated from another |
|
system (like Handlebars) |
|
|
|
@private |
|
@property emptyViewClass |
|
*/ |
|
emptyViewClass: Ember.View, |
|
|
|
/** |
|
An optional view to display if content is set to an empty array. |
|
|
|
@property emptyView |
|
@type Ember.View |
|
@default null |
|
*/ |
|
emptyView: null, |
|
|
|
/** |
|
@property itemViewClass |
|
@type Ember.View |
|
@default Ember.View |
|
*/ |
|
itemViewClass: Ember.View, |
|
|
|
/** |
|
Setup a CollectionView |
|
|
|
@method init |
|
*/ |
|
init: function() { |
|
var ret = this._super(); |
|
this._contentDidChange(); |
|
return ret; |
|
}, |
|
|
|
/** |
|
Invoked when the content property is about to change. Notifies observers that the |
|
entire array content will change. |
|
|
|
@private |
|
@method _contentWillChange |
|
*/ |
|
_contentWillChange: Ember.beforeObserver('content', function() { |
|
var content = this.get('content'); |
|
|
|
if (content) { content.removeArrayObserver(this); } |
|
var len = content ? get(content, 'length') : 0; |
|
this.arrayWillChange(content, 0, len); |
|
}), |
|
|
|
/** |
|
Check to make sure that the content has changed, and if so, |
|
update the children directly. This is always scheduled |
|
asynchronously, to allow the element to be created before |
|
bindings have synchronized and vice versa. |
|
|
|
@private |
|
@method _contentDidChange |
|
*/ |
|
_contentDidChange: Ember.observer('content', function() { |
|
var content = get(this, 'content'); |
|
|
|
if (content) { |
|
this._assertArrayLike(content); |
|
content.addArrayObserver(this); |
|
} |
|
|
|
var len = content ? get(content, 'length') : 0; |
|
this.arrayDidChange(content, 0, null, len); |
|
}), |
|
|
|
/** |
|
Ensure that the content implements Ember.Array |
|
|
|
@private |
|
@method _assertArrayLike |
|
*/ |
|
_assertArrayLike: function(content) { |
|
Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content)); |
|
}, |
|
|
|
/** |
|
Removes the content and content observers. |
|
|
|
@method destroy |
|
*/ |
|
destroy: function() { |
|
if (!this._super()) { return; } |
|
|
|
var content = get(this, 'content'); |
|
if (content) { content.removeArrayObserver(this); } |
|
|
|
if (this._createdEmptyView) { |
|
this._createdEmptyView.destroy(); |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
/** |
|
Called when a mutation to the underlying content array will occur. |
|
|
|
This method will remove any views that are no longer in the underlying |
|
content array. |
|
|
|
Invokes whenever the content array itself will change. |
|
|
|
@method arrayWillChange |
|
@param {Array} content the managed collection of objects |
|
@param {Number} start the index at which the changes will occurr |
|
@param {Number} removed number of object to be removed from content |
|
*/ |
|
arrayWillChange: function(content, start, removedCount) { |
|
// If the contents were empty before and this template collection has an |
|
// empty view remove it now. |
|
var emptyView = get(this, 'emptyView'); |
|
if (emptyView && emptyView instanceof Ember.View) { |
|
emptyView.removeFromParent(); |
|
} |
|
|
|
// Loop through child views that correspond with the removed items. |
|
// Note that we loop from the end of the array to the beginning because |
|
// we are mutating it as we go. |
|
var childViews = this._childViews, childView, idx, len; |
|
|
|
len = this._childViews.length; |
|
|
|
var removingAll = removedCount === len; |
|
|
|
if (removingAll) { |
|
this.currentState.empty(this); |
|
this.invokeRecursively(function(view) { |
|
view.removedFromDOM = true; |
|
}, false); |
|
} |
|
|
|
for (idx = start + removedCount - 1; idx >= start; idx--) { |
|
childView = childViews[idx]; |
|
childView.destroy(); |
|
} |
|
}, |
|
|
|
/** |
|
Called when a mutation to the underlying content array occurs. |
|
|
|
This method will replay that mutation against the views that compose the |
|
`Ember.CollectionView`, ensuring that the view reflects the model. |
|
|
|
This array observer is added in `contentDidChange`. |
|
|
|
@method arrayDidChange |
|
@param {Array} content the managed collection of objects |
|
@param {Number} start the index at which the changes occurred |
|
@param {Number} removed number of object removed from content |
|
@param {Number} added number of object added to content |
|
*/ |
|
arrayDidChange: function(content, start, removed, added) { |
|
var addedViews = [], view, item, idx, len, itemViewClass, |
|
emptyView; |
|
|
|
len = content ? get(content, 'length') : 0; |
|
|
|
if (len) { |
|
itemViewClass = get(this, 'itemViewClass'); |
|
|
|
if ('string' === typeof itemViewClass) { |
|
itemViewClass = get(itemViewClass) || itemViewClass; |
|
} |
|
|
|
Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", |
|
[itemViewClass]), |
|
'string' === typeof itemViewClass || Ember.View.detect(itemViewClass)); |
|
|
|
for (idx = start; idx < start+added; idx++) { |
|
item = content.objectAt(idx); |
|
|
|
view = this.createChildView(itemViewClass, { |
|
content: item, |
|
contentIndex: idx |
|
}); |
|
|
|
addedViews.push(view); |
|
} |
|
} else { |
|
emptyView = get(this, 'emptyView'); |
|
|
|
if (!emptyView) { return; } |
|
|
|
if ('string' === typeof emptyView) { |
|
emptyView = get(emptyView) || emptyView; |
|
} |
|
|
|
emptyView = this.createChildView(emptyView); |
|
addedViews.push(emptyView); |
|
set(this, 'emptyView', emptyView); |
|
|
|
if (Ember.CoreView.detect(emptyView)) { |
|
this._createdEmptyView = emptyView; |
|
} |
|
} |
|
|
|
this.replace(start, 0, addedViews); |
|
}, |
|
|
|
/** |
|
Instantiates a view to be added to the childViews array during view |
|
initialization. You generally will not call this method directly unless |
|
you are overriding `createChildViews()`. Note that this method will |
|
automatically configure the correct settings on the new view instance to |
|
act as a child of the parent. |
|
|
|
The tag name for the view will be set to the tagName of the viewClass |
|
passed in. |
|
|
|
@method createChildView |
|
@param {Class} viewClass |
|
@param {Hash} [attrs] Attributes to add |
|
@return {Ember.View} new instance |
|
*/ |
|
createChildView: function(view, attrs) { |
|
view = this._super(view, attrs); |
|
|
|
var itemTagName = get(view, 'tagName'); |
|
|
|
if (itemTagName === null || itemTagName === undefined) { |
|
itemTagName = Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')]; |
|
set(view, 'tagName', itemTagName); |
|
} |
|
|
|
return view; |
|
} |
|
}); |
|
|
|
/** |
|
A map of parent tags to their default child tags. You can add |
|
additional parent tags if you want collection views that use |
|
a particular parent tag to default to a child tag. |
|
|
|
@property CONTAINER_MAP |
|
@type Hash |
|
@static |
|
@final |
|
*/ |
|
Ember.CollectionView.CONTAINER_MAP = { |
|
ul: 'li', |
|
ol: 'li', |
|
table: 'tr', |
|
thead: 'tr', |
|
tbody: 'tr', |
|
tfoot: 'tr', |
|
tr: 'td', |
|
select: 'option' |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var get = Ember.get; |
|
|
|
/** |
|
The ComponentTemplateDeprecation mixin is used to provide a useful |
|
deprecation warning when using either `template` or `templateName` with |
|
a component. The `template` and `templateName` properties specified at |
|
extend time are moved to `layout` and `layoutName` respectively. |
|
|
|
`Ember.ComponentTemplateDeprecation` is used internally by Ember in |
|
`Ember.Component`. |
|
|
|
@class ComponentTemplateDeprecation |
|
@namespace Ember |
|
*/ |
|
Ember.ComponentTemplateDeprecation = Ember.Mixin.create({ |
|
/** |
|
@private |
|
|
|
Moves `templateName` to `layoutName` and `template` to `layout` at extend |
|
time if a layout is not also specified. |
|
|
|
Note that this currently modifies the mixin themselves, which is technically |
|
dubious but is practically of little consequence. This may change in the |
|
future. |
|
|
|
@method willMergeMixin |
|
*/ |
|
willMergeMixin: function(props) { |
|
// must call _super here to ensure that the ActionHandler |
|
// mixin is setup properly (moves actions -> _actions) |
|
// |
|
// Calling super is only OK here since we KNOW that |
|
// there is another Mixin loaded first. |
|
this._super.apply(this, arguments); |
|
|
|
var deprecatedProperty, replacementProperty, |
|
layoutSpecified = (props.layoutName || props.layout || get(this, 'layoutName')); |
|
|
|
if (props.templateName && !layoutSpecified) { |
|
deprecatedProperty = 'templateName'; |
|
replacementProperty = 'layoutName'; |
|
|
|
props.layoutName = props.templateName; |
|
delete props['templateName']; |
|
} |
|
|
|
if (props.template && !layoutSpecified) { |
|
deprecatedProperty = 'template'; |
|
replacementProperty = 'layout'; |
|
|
|
props.layout = props.template; |
|
delete props['template']; |
|
} |
|
|
|
if (deprecatedProperty) { |
|
Ember.deprecate('Do not specify ' + deprecatedProperty + ' on a Component, use ' + replacementProperty + ' instead.', false); |
|
} |
|
} |
|
}); |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
var get = Ember.get, set = Ember.set, isNone = Ember.isNone, |
|
a_slice = Array.prototype.slice; |
|
|
|
|
|
/** |
|
@module ember |
|
@submodule ember-views |
|
*/ |
|
|
|
/** |
|
An `Ember.Component` is a view that is completely |
|
isolated. Property access in its templates go |
|
to the view object and actions are targeted at |
|
the view object. There is no access to the |
|
surrounding context or outer controller; all |
|
contextual information must be passed in. |
|
|
|
The easiest way to create an `Ember.Component` is via |
|
a template. If you name a template |
|
`components/my-foo`, you will be able to use |
|
`{{my-foo}}` in other templates, which will make |
|
an instance of the isolated component. |
|
|
|
```handlebars |
|
{{app-profile person=currentUser}} |
|
``` |
|
|
|
```handlebars |
|
<!-- app-profile template --> |
|
<h1>{{person.title}}</h1> |
|
<img {{bind-attr src=person.avatar}}> |
|
<p class='signature'>{{person.signature}}</p> |
|
``` |
|
|
|
You can use `yield` inside a template to |
|
include the **contents** of any block attached to |
|
the component. The block will be executed in the |
|
context of the surrounding context or outer controller: |
|
|
|
```handlebars |
|
{{#app-profile person=currentUser}} |
|
<p>Admin mode</p> |
|
{{! Executed in the controller's context. }} |
|
{{/app-profile}} |
|
``` |
|
|
|
```handlebars |
|
<!-- app-profile template --> |
|
<h1>{{person.title}}</h1> |
|
{{! Executed in the components context. }} |
|
{{yield}} {{! block contents }} |
|
``` |
|
|
|
If you want to customize the component, in order to |
|
handle events or actions, you implement a subclass |
|
of `Ember.Component` named after the name of the |
|
component. Note that `Component` needs to be appended to the name of |
|
your subclass like `AppProfileComponent`. |
|
|
|
For example, you could implement the action |
|
`hello` for the `app-profile` component: |
|
|
|
```javascript |
|
App.AppProfileComponent = Ember.Component.extend({ |
|
actions: { |
|
hello: function(name) { |
|
console.log("Hello", name); |
|
} |
|
} |
|
}); |
|
``` |
|
|
|
And then use it in the component's template: |
|
|
|
```handlebars |
|
<!-- app-profile template --> |
|
|
|
<h1>{{person.title}}</h1> |
|
{{yield}} <!-- block contents --> |
|
|
|
<button {{action 'hello' person.name}}> |
|
Say Hello to {{person.name}} |
|
</button> |
|
``` |
|
|
|
Components must have a `-` in their name to avoid |
|
conflicts with built-in controls that wrap HTML |
|
elements. This is consistent with the same |
|
requirement in web components. |
|
|
|
@class Component |
|
@namespace Ember |
|
@extends Ember.View |
|
*/ |
|
Ember.Component = Ember.View.extend(Ember.TargetActionSupport, Ember.ComponentTemplateDeprecation, { |
|
init: function() { |
|
this._super(); |
|
set(this, 'context', this); |
|
set(this, 'controller', this); |
|
}, |
|
|
|
defaultLayout: function(context, options){ |
|
Ember.Handlebars.helpers['yield'].call(context, options); |
|
}, |
|
|
|
/** |
|
A components template property is set by passing a block |
|
during its invocation. It is executed within the parent context. |
|
|
|
Example: |
|
|
|
```handlebars |
|
{{#my-component}} |
|
// something that is run in the context |
|
// of the parent context |
|
{{/my-component}} |
|
``` |
|
|
|
Specifying a template directly to a component is deprecated without |
|
also specifying the layout property. |
|
|
|
@deprecated |
|
@property template |
|
*/ |
|
template: Ember.computed(function(key, value) { |
|
if (value !== undefined) { return value; } |
|
|
|
var templateName = get(this, 'templateName'), |
|
template = this.templateForName(templateName, 'template'); |
|
|
|
Ember.assert("You specified the templateName " + templateName + " for " + this + ", but it did not exist.", !templateName || template); |
|
|
|
return template || get(this, 'defaultTemplate'); |
|
}).property('templateName'), |
|
|
|
/** |
|
Specifying a components `templateName` is deprecated without also |
|
providing the `layout` or `layoutName` properties. |
|
|
|
@deprecated |
|
@property templateName |
|
*/ |
|
templateName: null, |
|
|
|
// during render, isolate keywords |
|
cloneKeywords: function() { |
|
return { |
|
view: this, |
|
controller: this |
|
}; |
|
}, |
|
|
|
_yield: function(context, options) { |
|
var view = options.data.view, |
|
parentView = this._parentView, |
|
template = get(this, 'template'); |
|
|
|
if (template) { |
|
Ember.assert("A Component must have a parent view in order to yield.", parentView); |
|
|
|
view.appendChild(Ember.View, { |
|
isVirtual: true, |
|
tagName: '', |
|
_contextView: parentView, |
|
template: template, |
|
context: get(parentView, 'context'), |
|
controller: get(parentView, 'controller'), |
|
templateData: { keywords: parentView.cloneKeywords() } |
|
}); |
|
} |
|
}, |
|
|
|
/** |
|
If the component is currently inserted into the DOM of a parent view, this |
|
property will point to the controller of the parent view. |
|
|
|
@property targetObject |
|
@type Ember.Controller |
|
@default null |
|
*/ |
|
targetObject: Ember.computed(function(key) { |
|
var parentView = get(this, '_parentView'); |
|
return parentView ? get(parentView, 'controller') : null; |
|
}).property('_parentView'), |
|
|
|
/** |
|
Triggers a named action on the controller context where the component is used if |
|
this controller has registered for notifications of the action. |
|
|
|
For example a component for playing or pausing music may translate click events |
|
into action notifications of "play" or "stop" depending on some internal state |
|
of the component: |
|
|
|
|
|
```javascript |
|
App.PlayButtonComponent = Ember.Component.extend({ |
|
click: function(){ |
|
if (this.get('isPlaying')) { |
|
this.sendAction('play'); |
|
} else { |
|
this.sendAction('stop'); |
|
} |
|
} |
|
}); |
|
``` |
|
|
|
When used inside a template these component actions are configured to |
|
trigger actions in the outer application context: |
|
|
|
```handlebars |
|
{{! application.hbs }} |
|
{{play-button play="musicStarted" stop="musicStopped"}} |
|
``` |
|
|
|
When the component receives a browser `click` event it translate this |
|
interaction into application-specific semantics ("play" or "stop") and |
|
triggers the specified action name on the controller for the template |
|
where the component is used: |
|
|
|
|
|
```javascript |
|
App.ApplicationController = Ember.Controller.extend({ |
|
actions: { |
|
musicStarted: function(){ |
|
// called when the play button is clicked |
|
// and the music started playing |
|
}, |
|
musicStopped: function(){ |
|
// called when the play button is clicked |
|
// and the music stopped playing |
|
} |
|
} |
|
}); |
|
``` |
|
|
|
If no action name is passed to `sendAction` a default name of "action" |
|
is assumed. |
|
|
|
```javascript |
|
App.NextButtonComponent = Ember.Component.extend({ |
|
click: function(){ |
|
this.sendAction(); |
|
} |
|
}); |
|
``` |
|
|
|
```handlebars |
|
{{! application.hbs }} |
|
{{next-button action="playNextSongInAlbum"}} |
|
``` |
|
|
|
```javascript |
|
App.ApplicationController = Ember.Controller.extend({ |
|
actions: { |
|
playNextSongInAlbum: function(){ |
|
... |
|
} |
|
} |
|
}); |
|
``` |
|
|
|
@method sendAction |
|
@param [action] {String} the action to trigger |
|
@param [context] {*} a context to send with the action |
|
*/ |
|
sendAction: function(action) { |
|
var actionName, |
|
contexts = a_slice.call(arguments, 1); |
|
|
|
// Send the default action |
|
if (action === undefined) { |
|
actionName = get(this, 'action'); |
|
Ember.assert("The default action was triggered on the component " + this.toString() + |
|
", but the action name (" + actionName + ") was not a string.", |
|
isNone(actionName) || typeof actionName === 'string'); |
|
} else { |
|
actionName = get(this, action); |
|
Ember.assert("The " + action + " action was triggered on the component " + |
|
this.toString() + ", but the action name (" + actionName + |
|
") was not a string.", |
|
isNone(actionName) || typeof actionName === 'string'); |
|
} |
|
|
|
// If no action name for that action could be found, just abort. |
|
if (actionName === undefined) { return; } |
|
|
|
this.triggerAction({ |
|
action: actionName, |
|
actionContext: contexts |
|
}); |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
`Ember.ViewTargetActionSupport` is a mixin that can be included in a |
|
view class to add a `triggerAction` method with semantics similar to |
|
the Handlebars `{{action}}` helper. It provides intelligent defaults |
|
for the action's target: the view's controller; and the context that is |
|
sent with the action: the view's context. |
|
|
|
Note: In normal Ember usage, the `{{action}}` helper is usually the best |
|
choice. This mixin is most often useful when you are doing more complex |
|
event handling in custom View subclasses. |
|
|
|
For example: |
|
|
|
```javascript |
|
App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { |
|
action: 'save', |
|
click: function() { |
|
this.triggerAction(); // Sends the `save` action, along with the current context |
|
// to the current controller |
|
} |
|
}); |
|
``` |
|
|
|
The `action` can be provided as properties of an optional object argument |
|
to `triggerAction` as well. |
|
|
|
```javascript |
|
App.SaveButtonView = Ember.View.extend(Ember.ViewTargetActionSupport, { |
|
click: function() { |
|
this.triggerAction({ |
|
action: 'save' |
|
}); // Sends the `save` action, along with the current context |
|
// to the current controller |
|
} |
|
}); |
|
``` |
|
|
|
@class ViewTargetActionSupport |
|
@namespace Ember |
|
@extends Ember.TargetActionSupport |
|
*/ |
|
Ember.ViewTargetActionSupport = Ember.Mixin.create(Ember.TargetActionSupport, { |
|
/** |
|
@property target |
|
*/ |
|
target: Ember.computed.alias('controller'), |
|
/** |
|
@property actionContext |
|
*/ |
|
actionContext: Ember.computed.alias('context') |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
Ember Views |
|
|
|
@module ember |
|
@submodule ember-views |
|
@requires ember-runtime |
|
@main ember-views |
|
*/ |
|
|
|
})(); |
|
|
|
(function() { |
|
define("metamorph", |
|
[], |
|
function() { |
|
"use strict"; |
|
// ========================================================================== |
|
// Project: metamorph |
|
// Copyright: ©2014 Tilde, Inc. All rights reserved. |
|
// ========================================================================== |
|
|
|
var K = function() {}, |
|
guid = 0, |
|
disableRange = (function(){ |
|
if ('undefined' !== typeof MetamorphENV) { |
|
return MetamorphENV.DISABLE_RANGE_API; |
|
} else if ('undefined' !== ENV) { |
|
return ENV.DISABLE_RANGE_API; |
|
} else { |
|
return false; |
|
} |
|
})(), |
|
|
|
// Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges |
|
supportsRange = (!disableRange) && typeof document !== 'undefined' && ('createRange' in document) && (typeof Range !== 'undefined') && Range.prototype.createContextualFragment, |
|
|
|
// Internet Explorer prior to 9 does not allow setting innerHTML if the first element |
|
// is a "zero-scope" element. This problem can be worked around by making |
|
// the first node an invisible text node. We, like Modernizr, use ­ |
|
needsShy = typeof document !== 'undefined' && (function() { |
|
var testEl = document.createElement('div'); |
|
testEl.innerHTML = "<div></div>"; |
|
testEl.firstChild.innerHTML = "<script></script>"; |
|
return testEl.firstChild.innerHTML === ''; |
|
})(), |
|
|
|
|
|
// IE 8 (and likely earlier) likes to move whitespace preceeding |
|
// a script tag to appear after it. This means that we can |
|
// accidentally remove whitespace when updating a morph. |
|
movesWhitespace = document && (function() { |
|
var testEl = document.createElement('div'); |
|
testEl.innerHTML = "Test: <script type='text/x-placeholder'></script>Value"; |
|
return testEl.childNodes[0].nodeValue === 'Test:' && |
|
testEl.childNodes[2].nodeValue === ' Value'; |
|
})(); |
|
|
|
// Constructor that supports either Metamorph('foo') or new |
|
// Metamorph('foo'); |
|
// |
|
// Takes a string of HTML as the argument. |
|
|
|
var Metamorph = function(html) { |
|
var self; |
|
|
|
if (this instanceof Metamorph) { |
|
self = this; |
|
} else { |
|
self = new K(); |
|
} |
|
|
|
self.innerHTML = html; |
|
var myGuid = 'metamorph-'+(guid++); |
|
self.start = myGuid + '-start'; |
|
self.end = myGuid + '-end'; |
|
|
|
return self; |
|
}; |
|
|
|
K.prototype = Metamorph.prototype; |
|
|
|
var rangeFor, htmlFunc, removeFunc, outerHTMLFunc, appendToFunc, afterFunc, prependFunc, startTagFunc, endTagFunc; |
|
|
|
outerHTMLFunc = function() { |
|
return this.startTag() + this.innerHTML + this.endTag(); |
|
}; |
|
|
|
startTagFunc = function() { |
|
/* |
|
* We replace chevron by its hex code in order to prevent escaping problems. |
|
* Check this thread for more explaination: |
|
* http://stackoverflow.com/questions/8231048/why-use-x3c-instead-of-when-generating-html-from-javascript |
|
*/ |
|
return "<script id='" + this.start + "' type='text/x-placeholder'>\x3C/script>"; |
|
}; |
|
|
|
endTagFunc = function() { |
|
/* |
|
* We replace chevron by its hex code in order to prevent escaping problems. |
|
* Check this thread for more explaination: |
|
* http://stackoverflow.com/questions/8231048/why-use-x3c-instead-of-when-generating-html-from-javascript |
|
*/ |
|
return "<script id='" + this.end + "' type='text/x-placeholder'>\x3C/script>"; |
|
}; |
|
|
|
// If we have the W3C range API, this process is relatively straight forward. |
|
if (supportsRange) { |
|
|
|
// Get a range for the current morph. Optionally include the starting and |
|
// ending placeholders. |
|
rangeFor = function(morph, outerToo) { |
|
var range = document.createRange(); |
|
var before = document.getElementById(morph.start); |
|
var after = document.getElementById(morph.end); |
|
|
|
if (outerToo) { |
|
range.setStartBefore(before); |
|
range.setEndAfter(after); |
|
} else { |
|
range.setStartAfter(before); |
|
range.setEndBefore(after); |
|
} |
|
|
|
return range; |
|
}; |
|
|
|
htmlFunc = function(html, outerToo) { |
|
// get a range for the current metamorph object |
|
var range = rangeFor(this, outerToo); |
|
|
|
// delete the contents of the range, which will be the |
|
// nodes between the starting and ending placeholder. |
|
range.deleteContents(); |
|
|
|
// create a new document fragment for the HTML |
|
var fragment = range.createContextualFragment(html); |
|
|
|
// insert the fragment into the range |
|
range.insertNode(fragment); |
|
}; |
|
|
|
/** |
|
* @public |
|
* |
|
* Remove this object (including starting and ending |
|
* placeholders). |
|
* |
|
* @method remove |
|
*/ |
|
removeFunc = function() { |
|
// get a range for the current metamorph object including |
|
// the starting and ending placeholders. |
|
var range = rangeFor(this, true); |
|
|
|
// delete the entire range. |
|
range.deleteContents(); |
|
}; |
|
|
|
appendToFunc = function(node) { |
|
var range = document.createRange(); |
|
range.setStart(node); |
|
range.collapse(false); |
|
var frag = range.createContextualFragment(this.outerHTML()); |
|
node.appendChild(frag); |
|
}; |
|
|
|
afterFunc = function(html) { |
|
var range = document.createRange(); |
|
var after = document.getElementById(this.end); |
|
|
|
range.setStartAfter(after); |
|
range.setEndAfter(after); |
|
|
|
var fragment = range.createContextualFragment(html); |
|
range.insertNode(fragment); |
|
}; |
|
|
|
prependFunc = function(html) { |
|
var range = document.createRange(); |
|
var start = document.getElementById(this.start); |
|
|
|
range.setStartAfter(start); |
|
range.setEndAfter(start); |
|
|
|
var fragment = range.createContextualFragment(html); |
|
range.insertNode(fragment); |
|
}; |
|
|
|
} else { |
|
/* |
|
* This code is mostly taken from jQuery, with one exception. In jQuery's case, we |
|
* have some HTML and we need to figure out how to convert it into some nodes. |
|
* |
|
* In this case, jQuery needs to scan the HTML looking for an opening tag and use |
|
* that as the key for the wrap map. In our case, we know the parent node, and |
|
* can use its type as the key for the wrap map. |
|
**/ |
|
var wrapMap = { |
|
select: [ 1, "<select multiple='multiple'>", "</select>" ], |
|
fieldset: [ 1, "<fieldset>", "</fieldset>" ], |
|
table: [ 1, "<table>", "</table>" ], |
|
tbody: [ 2, "<table><tbody>", "</tbody></table>" ], |
|
tr: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], |
|
colgroup: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ], |
|
map: [ 1, "<map>", "</map>" ], |
|
_default: [ 0, "", "" ] |
|
}; |
|
|
|
var findChildById = function(element, id) { |
|
if (element.getAttribute('id') === id) { return element; } |
|
|
|
var len = element.childNodes.length, idx, node, found; |
|
for (idx=0; idx<len; idx++) { |
|
node = element.childNodes[idx]; |
|
found = node.nodeType === 1 && findChildById(node, id); |
|
if (found) { return found; } |
|
} |
|
}; |
|
|
|
var setInnerHTML = function(element, html) { |
|
var matches = []; |
|
if (movesWhitespace) { |
|
// Right now we only check for script tags with ids with the |
|
// goal of targeting morphs. |
|
html = html.replace(/(\s+)(<script id='([^']+)')/g, function(match, spaces, tag, id) { |
|
matches.push([id, spaces]); |
|
return tag; |
|
}); |
|
} |
|
|
|
element.innerHTML = html; |
|
|
|
// If we have to do any whitespace adjustments do them now |
|
if (matches.length > 0) { |
|
var len = matches.length, idx; |
|
for (idx=0; idx<len; idx++) { |
|
var script = findChildById(element, matches[idx][0]), |
|
node = document.createTextNode(matches[idx][1]); |
|
script.parentNode.insertBefore(node, script); |
|
} |
|
} |
|
}; |
|
|
|
/* |
|
* Given a parent node and some HTML, generate a set of nodes. Return the first |
|
* node, which will allow us to traverse the rest using nextSibling. |
|
* |
|
* We need to do this because innerHTML in IE does not really parse the nodes. |
|
*/ |
|
var firstNodeFor = function(parentNode, html) { |
|
var arr = wrapMap[parentNode.tagName.toLowerCase()] || wrapMap._default; |
|
var depth = arr[0], start = arr[1], end = arr[2]; |
|
|
|
if (needsShy) { html = '­'+html; } |
|
|
|
var element = document.createElement('div'); |
|
|
|
setInnerHTML(element, start + html + end); |
|
|
|
for (var i=0; i<=depth; i++) { |
|
element = element.firstChild; |
|
} |
|
|
|
// Look for ­ to remove it. |
|
if (needsShy) { |
|
var shyElement = element; |
|
|
|
// Sometimes we get nameless elements with the shy inside |
|
while (shyElement.nodeType === 1 && !shyElement.nodeName) { |
|
shyElement = shyElement.firstChild; |
|
} |
|
|
|
// At this point it's the actual unicode character. |
|
if (shyElement.nodeType === 3 && shyElement.nodeValue.charAt(0) === "\u00AD") { |
|
shyElement.nodeValue = shyElement.nodeValue.slice(1); |
|
} |
|
} |
|
|
|
return element; |
|
}; |
|
|
|
/* |
|
* In some cases, Internet Explorer can create an anonymous node in |
|
* the hierarchy with no tagName. You can create this scenario via: |
|
* |
|
* div = document.createElement("div"); |
|
* div.innerHTML = "<table>­<script></script><tr><td>hi</td></tr></table>"; |
|
* div.firstChild.firstChild.tagName //=> "" |
|
* |
|
* If our script markers are inside such a node, we need to find that |
|
* node and use *it* as the marker. |
|
*/ |
|
var realNode = function(start) { |
|
while (start.parentNode.tagName === "") { |
|
start = start.parentNode; |
|
} |
|
|
|
return start; |
|
}; |
|
|
|
/* |
|
* When automatically adding a tbody, Internet Explorer inserts the |
|
* tbody immediately before the first <tr>. Other browsers create it |
|
* before the first node, no matter what. |
|
* |
|
* This means the the following code: |
|
* |
|
* div = document.createElement("div"); |
|
* div.innerHTML = "<table><script id='first'></script><tr><td>hi</td></tr><script id='last'></script></table> |
|
* |
|
* Generates the following DOM in IE: |
|
* |
|
* + div |
|
* + table |
|
* - script id='first' |
|
* + tbody |
|
* + tr |
|
* + td |
|
* - "hi" |
|
* - script id='last' |
|
* |
|
* Which means that the two script tags, even though they were |
|
* inserted at the same point in the hierarchy in the original |
|
* HTML, now have different parents. |
|
* |
|
* This code reparents the first script tag by making it the tbody's |
|
* first child. |
|
* |
|
*/ |
|
var fixParentage = function(start, end) { |
|
if (start.parentNode !== end.parentNode) { |
|
end.parentNode.insertBefore(start, end.parentNode.firstChild); |
|
} |
|
}; |
|
|
|
htmlFunc = function(html, outerToo) { |
|
// get the real starting node. see realNode for details. |
|
var start = realNode(document.getElementById(this.start)); |
|
var end = document.getElementById(this.end); |
|
var parentNode = end.parentNode; |
|
var node, nextSibling, last; |
|
|
|
// make sure that the start and end nodes share the same |
|
// parent. If not, fix it. |
|
fixParentage(start, end); |
|
|
|
// remove all of the nodes after the starting placeholder and |
|
// before the ending placeholder. |
|
node = start.nextSibling; |
|
while (node) { |
|
nextSibling = node.nextSibling; |
|
last = node === end; |
|
|
|
// if this is the last node, and we want to remove it as well, |
|
// set the `end` node to the next sibling. This is because |
|
// for the rest of the function, we insert the new nodes |
|
// before the end (note that insertBefore(node, null) is |
|
// the same as appendChild(node)). |
|
// |
|
// if we do not want to remove it, just break. |
|
if (last) { |
|
if (outerToo) { end = node.nextSibling; } else { break; } |
|
} |
|
|
|
node.parentNode.removeChild(node); |
|
|
|
// if this is the last node and we didn't break before |
|
// (because we wanted to remove the outer nodes), break |
|
// now. |
|
if (last) { break; } |
|
|
|
node = nextSibling; |
|
} |
|
|
|
// get the first node for the HTML string, even in cases like |
|
// tables and lists where a simple innerHTML on a div would |
|
// swallow some of the content. |
|
node = firstNodeFor(start.parentNode, html); |
|
|
|
if (outerToo) { |
|
start.parentNode.removeChild(start); |
|
} |
|
|
|
// copy the nodes for the HTML between the starting and ending |
|
// placeholder. |
|
while (node) { |
|
nextSibling = node.nextSibling; |
|
parentNode.insertBefore(node, end); |
|
node = nextSibling; |
|
} |
|
}; |
|
|
|
// remove the nodes in the DOM representing this metamorph. |
|
// |
|
// this includes the starting and ending placeholders. |
|
removeFunc = function() { |
|
var start = realNode(document.getElementById(this.start)); |
|
var end = document.getElementById(this.end); |
|
|
|
this.html(''); |
|
start.parentNode.removeChild(start); |
|
end.parentNode.removeChild(end); |
|
}; |
|
|
|
appendToFunc = function(parentNode) { |
|
var node = firstNodeFor(parentNode, this.outerHTML()); |
|
var nextSibling; |
|
|
|
while (node) { |
|
nextSibling = node.nextSibling; |
|
parentNode.appendChild(node); |
|
node = nextSibling; |
|
} |
|
}; |
|
|
|
afterFunc = function(html) { |
|
// get the real starting node. see realNode for details. |
|
var end = document.getElementById(this.end); |
|
var insertBefore = end.nextSibling; |
|
var parentNode = end.parentNode; |
|
var nextSibling; |
|
var node; |
|
|
|
// get the first node for the HTML string, even in cases like |
|
// tables and lists where a simple innerHTML on a div would |
|
// swallow some of the content. |
|
node = firstNodeFor(parentNode, html); |
|
|
|
// copy the nodes for the HTML between the starting and ending |
|
// placeholder. |
|
while (node) { |
|
nextSibling = node.nextSibling; |
|
parentNode.insertBefore(node, insertBefore); |
|
node = nextSibling; |
|
} |
|
}; |
|
|
|
prependFunc = function(html) { |
|
var start = document.getElementById(this.start); |
|
var parentNode = start.parentNode; |
|
var nextSibling; |
|
var node; |
|
|
|
node = firstNodeFor(parentNode, html); |
|
var insertBefore = start.nextSibling; |
|
|
|
while (node) { |
|
nextSibling = node.nextSibling; |
|
parentNode.insertBefore(node, insertBefore); |
|
node = nextSibling; |
|
} |
|
}; |
|
} |
|
|
|
Metamorph.prototype.html = function(html) { |
|
this.checkRemoved(); |
|
if (html === undefined) { return this.innerHTML; } |
|
|
|
htmlFunc.call(this, html); |
|
|
|
this.innerHTML = html; |
|
}; |
|
|
|
Metamorph.prototype.replaceWith = function(html) { |
|
this.checkRemoved(); |
|
htmlFunc.call(this, html, true); |
|
}; |
|
|
|
Metamorph.prototype.remove = removeFunc; |
|
Metamorph.prototype.outerHTML = outerHTMLFunc; |
|
Metamorph.prototype.appendTo = appendToFunc; |
|
Metamorph.prototype.after = afterFunc; |
|
Metamorph.prototype.prepend = prependFunc; |
|
Metamorph.prototype.startTag = startTagFunc; |
|
Metamorph.prototype.endTag = endTagFunc; |
|
|
|
Metamorph.prototype.isRemoved = function() { |
|
var before = document.getElementById(this.start); |
|
var after = document.getElementById(this.end); |
|
|
|
return !before || !after; |
|
}; |
|
|
|
Metamorph.prototype.checkRemoved = function() { |
|
if (this.isRemoved()) { |
|
throw new Error("Cannot perform operations on a Metamorph that is not in the DOM."); |
|
} |
|
}; |
|
|
|
return Metamorph; |
|
}); |
|
|
|
})(); |
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-handlebars-compiler |
|
*/ |
|
|
|
// Eliminate dependency on any Ember to simplify precompilation workflow |
|
var objectCreate = Object.create || function(parent) { |
|
function F() {} |
|
F.prototype = parent; |
|
return new F(); |
|
}; |
|
|
|
var Handlebars = (Ember.imports && Ember.imports.Handlebars) || (this && this.Handlebars); |
|
if (!Handlebars && typeof require === 'function') { |
|
Handlebars = require('handlebars'); |
|
} |
|
|
|
Ember.assert("Ember Handlebars requires Handlebars version 1.0 or 1.1. Include " + |
|
"a SCRIPT tag in the HTML HEAD linking to the Handlebars file " + |
|
"before you link to Ember.", Handlebars); |
|
|
|
Ember.assert("Ember Handlebars requires Handlebars version 1.0 or 1.1, " + |
|
"COMPILER_REVISION expected: 4, got: " + Handlebars.COMPILER_REVISION + |
|
" - Please note: Builds of master may have other COMPILER_REVISION values.", |
|
Handlebars.COMPILER_REVISION === 4); |
|
|
|
/** |
|
Prepares the Handlebars templating library for use inside Ember's view |
|
system. |
|
|
|
The `Ember.Handlebars` object is the standard Handlebars library, extended to |
|
use Ember's `get()` method instead of direct property access, which allows |
|
computed properties to be used inside templates. |
|
|
|
To create an `Ember.Handlebars` template, call `Ember.Handlebars.compile()`. |
|
This will return a function that can be used by `Ember.View` for rendering. |
|
|
|
@class Handlebars |
|
@namespace Ember |
|
*/ |
|
Ember.Handlebars = objectCreate(Handlebars); |
|
|
|
/** |
|
Register a bound helper or custom view helper. |
|
|
|
## Simple bound helper example |
|
|
|
```javascript |
|
Ember.Handlebars.helper('capitalize', function(value) { |
|
return value.toUpperCase(); |
|
}); |
|
``` |
|
|
|
The above bound helper can be used inside of templates as follows: |
|
|
|
```handlebars |
|
{{capitalize name}} |
|
``` |
|
|
|
In this case, when the `name` property of the template's context changes, |
|
the rendered value of the helper will update to reflect this change. |
|
|
|
For more examples of bound helpers, see documentation for |
|
`Ember.Handlebars.registerBoundHelper`. |
|
|
|
## Custom view helper example |
|
|
|
Assuming a view subclass named `App.CalendarView` were defined, a helper |
|
for rendering instances of this view could be registered as follows: |
|
|
|
```javascript |
|
Ember.Handlebars.helper('calendar', App.CalendarView): |
|
``` |
|
|
|
The above bound helper can be used inside of templates as follows: |
|
|
|
```handlebars |
|
{{calendar}} |
|
``` |
|
|
|
Which is functionally equivalent to: |
|
|
|
```handlebars |
|
{{view App.CalendarView}} |
|
``` |
|
|
|
Options in the helper will be passed to the view in exactly the same |
|
manner as with the `view` helper. |
|
|
|
@method helper |
|
@for Ember.Handlebars |
|
@param {String} name |
|
@param {Function|Ember.View} function or view class constructor |
|
@param {String} dependentKeys* |
|
*/ |
|
Ember.Handlebars.helper = function(name, value) { |
|
Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", !Ember.Component.detect(value) || name.match(/-/)); |
|
|
|
if (Ember.View.detect(value)) { |
|
Ember.Handlebars.registerHelper(name, Ember.Handlebars.makeViewHelper(value)); |
|
} else { |
|
Ember.Handlebars.registerBoundHelper.apply(null, arguments); |
|
} |
|
}; |
|
|
|
/** |
|
Returns a helper function that renders the provided ViewClass. |
|
|
|
Used internally by Ember.Handlebars.helper and other methods |
|
involving helper/component registration. |
|
|
|
@private |
|
@method makeViewHelper |
|
@for Ember.Handlebars |
|
@param {Function} ViewClass view class constructor |
|
*/ |
|
Ember.Handlebars.makeViewHelper = function(ViewClass) { |
|
return function(options) { |
|
Ember.assert("You can only pass attributes (such as name=value) not bare values to a helper for a View found in '" + ViewClass.toString() + "'", arguments.length < 2); |
|
return Ember.Handlebars.helpers.view.call(this, ViewClass, options); |
|
}; |
|
}; |
|
|
|
/** |
|
@class helpers |
|
@namespace Ember.Handlebars |
|
*/ |
|
Ember.Handlebars.helpers = objectCreate(Handlebars.helpers); |
|
|
|
/** |
|
Override the the opcode compiler and JavaScript compiler for Handlebars. |
|
|
|
@class Compiler |
|
@namespace Ember.Handlebars |
|
@private |
|
@constructor |
|
*/ |
|
Ember.Handlebars.Compiler = function() {}; |
|
|
|
// Handlebars.Compiler doesn't exist in runtime-only |
|
if (Handlebars.Compiler) { |
|
Ember.Handlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype); |
|
} |
|
|
|
Ember.Handlebars.Compiler.prototype.compiler = Ember.Handlebars.Compiler; |
|
|
|
/** |
|
@class JavaScriptCompiler |
|
@namespace Ember.Handlebars |
|
@private |
|
@constructor |
|
*/ |
|
Ember.Handlebars.JavaScriptCompiler = function() {}; |
|
|
|
// Handlebars.JavaScriptCompiler doesn't exist in runtime-only |
|
if (Handlebars.JavaScriptCompiler) { |
|
Ember.Handlebars.JavaScriptCompiler.prototype = objectCreate(Handlebars.JavaScriptCompiler.prototype); |
|
Ember.Handlebars.JavaScriptCompiler.prototype.compiler = Ember.Handlebars.JavaScriptCompiler; |
|
} |
|
|
|
|
|
Ember.Handlebars.JavaScriptCompiler.prototype.namespace = "Ember.Handlebars"; |
|
|
|
Ember.Handlebars.JavaScriptCompiler.prototype.initializeBuffer = function() { |
|
return "''"; |
|
}; |
|
|
|
/** |
|
Override the default buffer for Ember Handlebars. By default, Handlebars |
|
creates an empty String at the beginning of each invocation and appends to |
|
it. Ember's Handlebars overrides this to append to a single shared buffer. |
|
|
|
@private |
|
@method appendToBuffer |
|
@param string {String} |
|
*/ |
|
Ember.Handlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) { |
|
return "data.buffer.push("+string+");"; |
|
}; |
|
|
|
// Hacks ahead: |
|
// Handlebars presently has a bug where the `blockHelperMissing` hook |
|
// doesn't get passed the name of the missing helper name, but rather |
|
// gets passed the value of that missing helper evaluated on the current |
|
// context, which is most likely `undefined` and totally useless. |
|
// |
|
// So we alter the compiled template function to pass the name of the helper |
|
// instead, as expected. |
|
// |
|
// This can go away once the following is closed: |
|
// https://github.com/wycats/handlebars.js/issues/634 |
|
|
|
var DOT_LOOKUP_REGEX = /helpers\.(.*?)\)/, |
|
BRACKET_STRING_LOOKUP_REGEX = /helpers\['(.*?)'/, |
|
INVOCATION_SPLITTING_REGEX = /(.*blockHelperMissing\.call\(.*)(stack[0-9]+)(,.*)/; |
|
|
|
Ember.Handlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation = function(source) { |
|
var helperInvocation = source[source.length - 1], |
|
helperName = (DOT_LOOKUP_REGEX.exec(helperInvocation) || BRACKET_STRING_LOOKUP_REGEX.exec(helperInvocation))[1], |
|
matches = INVOCATION_SPLITTING_REGEX.exec(helperInvocation); |
|
|
|
source[source.length - 1] = matches[1] + "'" + helperName + "'" + matches[3]; |
|
}; |
|
var stringifyBlockHelperMissing = Ember.Handlebars.JavaScriptCompiler.stringifyLastBlockHelperMissingInvocation; |
|
|
|
var originalBlockValue = Ember.Handlebars.JavaScriptCompiler.prototype.blockValue; |
|
Ember.Handlebars.JavaScriptCompiler.prototype.blockValue = function() { |
|
originalBlockValue.apply(this, arguments); |
|
stringifyBlockHelperMissing(this.source); |
|
}; |
|
|
|
var originalAmbiguousBlockValue = Ember.Handlebars.JavaScriptCompiler.prototype.ambiguousBlockValue; |
|
Ember.Handlebars.JavaScriptCompiler.prototype.ambiguousBlockValue = function() { |
|
originalAmbiguousBlockValue.apply(this, arguments); |
|
stringifyBlockHelperMissing(this.source); |
|
}; |
|
|
|
/** |
|
Rewrite simple mustaches from `{{foo}}` to `{{bind "foo"}}`. This means that |
|
all simple mustaches in Ember's Handlebars will also set up an observer to |
|
keep the DOM up to date when the underlying property changes. |
|
|
|
@private |
|
@method mustache |
|
@for Ember.Handlebars.Compiler |
|
@param mustache |
|
*/ |
|
Ember.Handlebars.Compiler.prototype.mustache = function(mustache) { |
|
if (!(mustache.params.length || mustache.hash)) { |
|
var id = new Handlebars.AST.IdNode([{ part: '_triageMustache' }]); |
|
|
|
// Update the mustache node to include a hash value indicating whether the original node |
|
// was escaped. This will allow us to properly escape values when the underlying value |
|
// changes and we need to re-render the value. |
|
if (!mustache.escaped) { |
|
mustache.hash = mustache.hash || new Handlebars.AST.HashNode([]); |
|
mustache.hash.pairs.push(["unescaped", new Handlebars.AST.StringNode("true")]); |
|
} |
|
mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, !mustache.escaped); |
|
} |
|
|
|
return Handlebars.Compiler.prototype.mustache.call(this, mustache); |
|
}; |
|
|
|
/** |
|
Used for precompilation of Ember Handlebars templates. This will not be used |
|
during normal app execution. |
|
|
|
@method precompile |
|
@for Ember.Handlebars |
|
@static |
|
@param {String} string The template to precompile |
|
*/ |
|
Ember.Handlebars.precompile = function(string) { |
|
var ast = Handlebars.parse(string); |
|
|
|
var options = { |
|
knownHelpers: { |
|
action: true, |
|
unbound: true, |
|
'bind-attr': true, |
|
template: true, |
|
view: true, |
|
_triageMustache: true |
|
}, |
|
data: true, |
|
stringParams: true |
|
}; |
|
|
|
var environment = new Ember.Handlebars.Compiler().compile(ast, options); |
|
return new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); |
|
}; |
|
|
|
// We don't support this for Handlebars runtime-only |
|
if (Handlebars.compile) { |
|
/** |
|
The entry point for Ember Handlebars. This replaces the default |
|
`Handlebars.compile` and turns on template-local data and String |
|
parameters. |
|
|
|
@method compile |
|
@for Ember.Handlebars |
|
@static |
|
@param {String} string The template to compile |
|
@return {Function} |
|
*/ |
|
Ember.Handlebars.compile = function(string) { |
|
var ast = Handlebars.parse(string); |
|
var options = { data: true, stringParams: true }; |
|
var environment = new Ember.Handlebars.Compiler().compile(ast, options); |
|
var templateSpec = new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); |
|
|
|
var template = Ember.Handlebars.template(templateSpec); |
|
template.isMethod = false; //Make sure we don't wrap templates with ._super |
|
|
|
return template; |
|
}; |
|
} |
|
|
|
|
|
})(); |
|
|
|
(function() { |
|
var slice = Array.prototype.slice, |
|
originalTemplate = Ember.Handlebars.template; |
|
|
|
/** |
|
If a path starts with a reserved keyword, returns the root |
|
that should be used. |
|
|
|
@private |
|
@method normalizePath |
|
@for Ember |
|
@param root {Object} |
|
@param path {String} |
|
@param data {Hash} |
|
*/ |
|
var normalizePath = Ember.Handlebars.normalizePath = function(root, path, data) { |
|
var keywords = (data && data.keywords) || {}, |
|
keyword, isKeyword; |
|
|
|
// Get the first segment of the path. For example, if the |
|
// path is "foo.bar.baz", returns "foo". |
|
keyword = path.split('.', 1)[0]; |
|
|
|
// Test to see if the first path is a keyword that has been |
|
// passed along in the view's data hash. If so, we will treat |
|
// that object as the new root. |
|
if (keywords.hasOwnProperty(keyword)) { |
|
// Look up the value in the template's data hash. |
|
root = keywords[keyword]; |
|
isKeyword = true; |
|
|
|
// Handle cases where the entire path is the reserved |
|
// word. In that case, return the object itself. |
|
if (path === keyword) { |
|
path = ''; |
|
} else { |
|
// Strip the keyword from the path and look up |
|
// the remainder from the newly found root. |
|
path = path.substr(keyword.length+1); |
|
} |
|
} |
|
|
|
return { root: root, path: path, isKeyword: isKeyword }; |
|
}; |
|
|
|
|
|
/** |
|
Lookup both on root and on window. If the path starts with |
|
a keyword, the corresponding object will be looked up in the |
|
template's data hash and used to resolve the path. |
|
|
|
@method get |
|
@for Ember.Handlebars |
|
@param {Object} root The object to look up the property on |
|
@param {String} path The path to be lookedup |
|
@param {Object} options The template's option hash |
|
*/ |
|
var handlebarsGet = Ember.Handlebars.get = function(root, path, options) { |
|
var data = options && options.data, |
|
normalizedPath = normalizePath(root, path, data), |
|
value; |
|
|
|
|
|
root = normalizedPath.root; |
|
path = normalizedPath.path; |
|
|
|
value = Ember.get(root, path); |
|
|
|
if (value === undefined && root !== Ember.lookup && Ember.isGlobalPath(path)) { |
|
value = Ember.get(Ember.lookup, path); |
|
} |
|
|
|
|
|
return value; |
|
}; |
|
|
|
/** |
|
This method uses `Ember.Handlebars.get` to lookup a value, then ensures |
|
that the value is escaped properly. |
|
|
|
If `unescaped` is a truthy value then the escaping will not be performed. |
|
|
|
@method getEscaped |
|
@for Ember.Handlebars |
|
@param {Object} root The object to look up the property on |
|
@param {String} path The path to be lookedup |
|
@param {Object} options The template's option hash |
|
*/ |
|
Ember.Handlebars.getEscaped = function(root, path, options) { |
|
var result = handlebarsGet(root, path, options); |
|
|
|
if (result === null || result === undefined) { |
|
result = ""; |
|
} else if (!(result instanceof Handlebars.SafeString)) { |
|
result = String(result); |
|
} |
|
if (!options.hash.unescaped){ |
|
result = Handlebars.Utils.escapeExpression(result); |
|
} |
|
|
|
return result; |
|
}; |
|
|
|
Ember.Handlebars.resolveParams = function(context, params, options) { |
|
var resolvedParams = [], types = options.types, param, type; |
|
|
|
for (var i=0, l=params.length; i<l; i++) { |
|
param = params[i]; |
|
type = types[i]; |
|
|
|
if (type === 'ID') { |
|
resolvedParams.push(handlebarsGet(context, param, options)); |
|
} else { |
|
resolvedParams.push(param); |
|
} |
|
} |
|
|
|
return resolvedParams; |
|
}; |
|
|
|
Ember.Handlebars.resolveHash = function(context, hash, options) { |
|
var resolvedHash = {}, types = options.hashTypes, type; |
|
|
|
for (var key in hash) { |
|
if (!hash.hasOwnProperty(key)) { continue; } |
|
|
|
type = types[key]; |
|
|
|
if (type === 'ID') { |
|
resolvedHash[key] = handlebarsGet(context, hash[key], options); |
|
} else { |
|
resolvedHash[key] = hash[key]; |
|
} |
|
} |
|
|
|
return resolvedHash; |
|
}; |
|
|
|
/** |
|
Registers a helper in Handlebars that will be called if no property with the |
|
given name can be found on the current context object, and no helper with |
|
that name is registered. |
|
|
|
This throws an exception with a more helpful error message so the user can |
|
track down where the problem is happening. |
|
|
|
@private |
|
@method helperMissing |
|
@for Ember.Handlebars.helpers |
|
@param {String} path |
|
@param {Hash} options |
|
*/ |
|
Ember.Handlebars.registerHelper('helperMissing', function(path) { |
|
var error, view = ""; |
|
|
|
var options = arguments[arguments.length - 1]; |
|
|
|
var helper = Ember.Handlebars.resolveHelper(options.data.view.container, path); |
|
|
|
if (helper) { |
|
return helper.apply(this, slice.call(arguments, 1)); |
|
} |
|
|
|
error = "%@ Handlebars error: Could not find property '%@' on object %@."; |
|
if (options.data) { |
|
view = options.data.view; |
|
} |
|
throw new Ember.Error(Ember.String.fmt(error, [view, path, this])); |
|
}); |
|
|
|
/** |
|
Registers a helper in Handlebars that will be called if no property with the |
|
given name can be found on the current context object, and no helper with |
|
that name is registered. |
|
|
|
This throws an exception with a more helpful error message so the user can |
|
track down where the problem is happening. |
|
|
|
@private |
|
@method helperMissing |
|
@for Ember.Handlebars.helpers |
|
@param {String} path |
|
@param {Hash} options |
|
*/ |
|
Ember.Handlebars.registerHelper('blockHelperMissing', function(path) { |
|
|
|
var options = arguments[arguments.length - 1]; |
|
|
|
Ember.assert("`blockHelperMissing` was invoked without a helper name, which " + |
|
"is most likely due to a mismatch between the version of " + |
|
"Ember.js you're running now and the one used to precompile your " + |
|
"templates. Please make sure the version of " + |
|
"`ember-handlebars-compiler` you're using is up to date.", path); |
|
|
|
var helper = Ember.Handlebars.resolveHelper(options.data.view.container, path); |
|
|
|
if (helper) { |
|
return helper.apply(this, slice.call(arguments, 1)); |
|
} else { |
|
return Handlebars.helpers.helperMissing.call(this, path); |
|
} |
|
|
|
return Handlebars.helpers.blockHelperMissing.apply(this, arguments); |
|
}); |
|
|
|
/** |
|
Register a bound handlebars helper. Bound helpers behave similarly to regular |
|
handlebars helpers, with the added ability to re-render when the underlying data |
|
changes. |
|
|
|
## Simple example |
|
|
|
```javascript |
|
Ember.Handlebars.registerBoundHelper('capitalize', function(value) { |
|
return value.toUpperCase(); |
|
}); |
|
``` |
|
|
|
The above bound helper can be used inside of templates as follows: |
|
|
|
```handlebars |
|
{{capitalize name}} |
|
``` |
|
|
|
In this case, when the `name` property of the template's context changes, |
|
the rendered value of the helper will update to reflect this change. |
|
|
|
## Example with options |
|
|
|
Like normal handlebars helpers, bound helpers have access to the options |
|
passed into the helper call. |
|
|
|
```javascript |
|
Ember.Handlebars.registerBoundHelper('repeat', function(value, options) { |
|
var count = options.hash.count; |
|
var a = []; |
|
while(a.length < count) { |
|
a.push(value); |
|
} |
|
return a.join(''); |
|
}); |
|
``` |
|
|
|
This helper could be used in a template as follows: |
|
|
|
```handlebars |
|
{{repeat text count=3}} |
|
``` |
|
|
|
## Example with bound options |
|
|
|
Bound hash options are also supported. Example: |
|
|
|
```handlebars |
|
{{repeat text countBinding="numRepeats"}} |
|
``` |
|
|
|
In this example, count will be bound to the value of |
|
the `numRepeats` property on the context. If that property |
|
changes, the helper will be re-rendered. |
|
|
|
## Example with extra dependencies |
|
|
|
The `Ember.Handlebars.registerBoundHelper` method takes a variable length |
|
third parameter which indicates extra dependencies on the passed in value. |
|
This allows the handlebars helper to update when these dependencies change. |
|
|
|
```javascript |
|
Ember.Handlebars.registerBoundHelper('capitalizeName', function(value) { |
|
return value.get('name').toUpperCase(); |
|
}, 'name'); |
|
``` |
|
|
|
## Example with multiple bound properties |
|
|
|
`Ember.Handlebars.registerBoundHelper` supports binding to |
|
multiple properties, e.g.: |
|
|
|
```javascript |
|
Ember.Handlebars.registerBoundHelper('concatenate', function() { |
|
var values = Array.prototype.slice.call(arguments, 0, -1); |
|
return values.join('||'); |
|
}); |
|
``` |
|
|
|
Which allows for template syntax such as `{{concatenate prop1 prop2}}` or |
|
`{{concatenate prop1 prop2 prop3}}`. If any of the properties change, |
|
the helpr will re-render. Note that dependency keys cannot be |
|
using in conjunction with multi-property helpers, since it is ambiguous |
|
which property the dependent keys would belong to. |
|
|
|
## Use with unbound helper |
|
|
|
The `{{unbound}}` helper can be used with bound helper invocations |
|
to render them in their unbound form, e.g. |
|
|
|
```handlebars |
|
{{unbound capitalize name}} |
|
``` |
|
|
|
In this example, if the name property changes, the helper |
|
will not re-render. |
|
|
|
## Use with blocks not supported |
|
|
|
Bound helpers do not support use with Handlebars blocks or |
|
the addition of child views of any kind. |
|
|
|
@method registerBoundHelper |
|
@for Ember.Handlebars |
|
@param {String} name |
|
@param {Function} function |
|
@param {String} dependentKeys* |
|
*/ |
|
Ember.Handlebars.registerBoundHelper = function(name, fn) { |
|
var boundHelperArgs = slice.call(arguments, 1), |
|
boundFn = Ember.Handlebars.makeBoundHelper.apply(this, boundHelperArgs); |
|
Ember.Handlebars.registerHelper(name, boundFn); |
|
}; |
|
|
|
/** |
|
A (mostly) private helper function to `registerBoundHelper`. Takes the |
|
provided Handlebars helper function fn and returns it in wrapped |
|
bound helper form. |
|
|
|
The main use case for using this outside of `registerBoundHelper` |
|
is for registering helpers on the container: |
|
|
|
```js |
|
var boundHelperFn = Ember.Handlebars.makeBoundHelper(function(word) { |
|
return word.toUpperCase(); |
|
}); |
|
|
|
container.register('helper:my-bound-helper', boundHelperFn); |
|
``` |
|
|
|
In the above example, if the helper function hadn't been wrapped in |
|
`makeBoundHelper`, the registered helper would be unbound. |
|
|
|
@private |
|
@method makeBoundHelper |
|
@for Ember.Handlebars |
|
@param {Function} function |
|
@param {String} dependentKeys* |
|
*/ |
|
Ember.Handlebars.makeBoundHelper = function(fn) { |
|
var dependentKeys = slice.call(arguments, 1); |
|
|
|
function helper() { |
|
var properties = slice.call(arguments, 0, -1), |
|
numProperties = properties.length, |
|
options = arguments[arguments.length - 1], |
|
normalizedProperties = [], |
|
data = options.data, |
|
types = data.isUnbound ? slice.call(options.types, 1) : options.types, |
|
hash = options.hash, |
|
view = data.view, |
|
contexts = options.contexts, |
|
currentContext = (contexts && contexts.length) ? contexts[0] : this, |
|
prefixPathForDependentKeys = '', |
|
loc, len, hashOption, |
|
boundOption, property, |
|
normalizedValue = Ember._SimpleHandlebarsView.prototype.normalizedValue; |
|
|
|
Ember.assert("registerBoundHelper-generated helpers do not support use with Handlebars blocks.", !options.fn); |
|
|
|
// Detect bound options (e.g. countBinding="otherCount") |
|
var boundOptions = hash.boundOptions = {}; |
|
for (hashOption in hash) { |
|
if (Ember.IS_BINDING.test(hashOption)) { |
|
// Lop off 'Binding' suffix. |
|
boundOptions[hashOption.slice(0, -7)] = hash[hashOption]; |
|
} |
|
} |
|
|
|
// Expose property names on data.properties object. |
|
var watchedProperties = []; |
|
data.properties = []; |
|
for (loc = 0; loc < numProperties; ++loc) { |
|
data.properties.push(properties[loc]); |
|
if (types[loc] === 'ID') { |
|
var normalizedProp = normalizePath(currentContext, properties[loc], data); |
|
normalizedProperties.push(normalizedProp); |
|
watchedProperties.push(normalizedProp); |
|
} else { |
|
if(data.isUnbound) { |
|
normalizedProperties.push({path: properties[loc]}); |
|
}else { |
|
normalizedProperties.push(null); |
|
} |
|
} |
|
} |
|
|
|
// Handle case when helper invocation is preceded by `unbound`, e.g. |
|
// {{unbound myHelper foo}} |
|
if (data.isUnbound) { |
|
return evaluateUnboundHelper(this, fn, normalizedProperties, options); |
|
} |
|
|
|
var bindView = new Ember._SimpleHandlebarsView(null, null, !options.hash.unescaped, options.data); |
|
|
|
// Override SimpleHandlebarsView's method for generating the view's content. |
|
bindView.normalizedValue = function() { |
|
var args = [], boundOption; |
|
|
|
// Copy over bound hash options. |
|
for (boundOption in boundOptions) { |
|
if (!boundOptions.hasOwnProperty(boundOption)) { continue; } |
|
property = normalizePath(currentContext, boundOptions[boundOption], data); |
|
bindView.path = property.path; |
|
bindView.pathRoot = property.root; |
|
hash[boundOption] = normalizedValue.call(bindView); |
|
} |
|
|
|
for (loc = 0; loc < numProperties; ++loc) { |
|
property = normalizedProperties[loc]; |
|
if (property) { |
|
bindView.path = property.path; |
|
bindView.pathRoot = property.root; |
|
args.push(normalizedValue.call(bindView)); |
|
} else { |
|
args.push(properties[loc]); |
|
} |
|
} |
|
args.push(options); |
|
|
|
// Run the supplied helper function. |
|
return fn.apply(currentContext, args); |
|
}; |
|
|
|
view.appendChild(bindView); |
|
|
|
// Assemble list of watched properties that'll re-render this helper. |
|
for (boundOption in boundOptions) { |
|
if (boundOptions.hasOwnProperty(boundOption)) { |
|
watchedProperties.push(normalizePath(currentContext, boundOptions[boundOption], data)); |
|
} |
|
} |
|
|
|
// Observe each property. |
|
for (loc = 0, len = watchedProperties.length; loc < len; ++loc) { |
|
property = watchedProperties[loc]; |
|
view.registerObserver(property.root, property.path, bindView, bindView.rerender); |
|
} |
|
|
|
if (types[0] !== 'ID' || normalizedProperties.length === 0) { |
|
return; |
|
} |
|
|
|
// Add dependent key observers to the first param |
|
var normalized = normalizedProperties[0], |
|
pathRoot = normalized.root, |
|
path = normalized.path; |
|
|
|
if(!Ember.isEmpty(path)) { |
|
prefixPathForDependentKeys = path + '.'; |
|
} |
|
for (var i=0, l=dependentKeys.length; i<l; i++) { |
|
view.registerObserver(pathRoot, prefixPathForDependentKeys + dependentKeys[i], bindView, bindView.rerender); |
|
} |
|
} |
|
|
|
helper._rawFunction = fn; |
|
return helper; |
|
}; |
|
|
|
/** |
|
Renders the unbound form of an otherwise bound helper function. |
|
|
|
@private |
|
@method evaluateUnboundHelper |
|
@param {Function} fn |
|
@param {Object} context |
|
@param {Array} normalizedProperties |
|
@param {String} options |
|
*/ |
|
function evaluateUnboundHelper(context, fn, normalizedProperties, options) { |
|
var args = [], |
|
hash = options.hash, |
|
boundOptions = hash.boundOptions, |
|
types = slice.call(options.types, 1), |
|
loc, |
|
len, |
|
property, |
|
propertyType, |
|
boundOption; |
|
|
|
for (boundOption in boundOptions) { |
|
if (!boundOptions.hasOwnProperty(boundOption)) { continue; } |
|
hash[boundOption] = Ember.Handlebars.get(context, boundOptions[boundOption], options); |
|
} |
|
|
|
for(loc = 0, len = normalizedProperties.length; loc < len; ++loc) { |
|
property = normalizedProperties[loc]; |
|
propertyType = types[loc]; |
|
if(propertyType === "ID") { |
|
args.push(Ember.Handlebars.get(property.root, property.path, options)); |
|
} else { |
|
args.push(property.path); |
|
} |
|
} |
|
args.push(options); |
|
return fn.apply(context, args); |
|
} |
|
|
|
/** |
|
Overrides Handlebars.template so that we can distinguish |
|
user-created, top-level templates from inner contexts. |
|
|
|
@private |
|
@method template |
|
@for Ember.Handlebars |
|
@param {String} spec |
|
*/ |
|
Ember.Handlebars.template = function(spec) { |
|
var t = originalTemplate(spec); |
|
t.isTop = true; |
|
return t; |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
Mark a string as safe for unescaped output with Handlebars. If you |
|
return HTML from a Handlebars helper, use this function to |
|
ensure Handlebars does not escape the HTML. |
|
|
|
```javascript |
|
Ember.String.htmlSafe('<div>someString</div>') |
|
``` |
|
|
|
@method htmlSafe |
|
@for Ember.String |
|
@static |
|
@return {Handlebars.SafeString} a string that will not be html escaped by Handlebars |
|
*/ |
|
Ember.String.htmlSafe = function(str) { |
|
return new Handlebars.SafeString(str); |
|
}; |
|
|
|
var htmlSafe = Ember.String.htmlSafe; |
|
|
|
if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.String) { |
|
|
|
/** |
|
Mark a string as being safe for unescaped output with Handlebars. |
|
|
|
```javascript |
|
'<div>someString</div>'.htmlSafe() |
|
``` |
|
|
|
See [Ember.String.htmlSafe](/api/classes/Ember.String.html#method_htmlSafe). |
|
|
|
@method htmlSafe |
|
@for String |
|
@return {Handlebars.SafeString} a string that will not be html escaped by Handlebars |
|
*/ |
|
String.prototype.htmlSafe = function() { |
|
return htmlSafe(this); |
|
}; |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
Ember.Handlebars.resolvePaths = function(options) { |
|
var ret = [], |
|
contexts = options.contexts, |
|
roots = options.roots, |
|
data = options.data; |
|
|
|
for (var i=0, l=contexts.length; i<l; i++) { |
|
ret.push( Ember.Handlebars.get(roots[i], contexts[i], { data: data }) ); |
|
} |
|
|
|
return ret; |
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/*jshint newcap:false*/ |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var set = Ember.set, get = Ember.get; |
|
var Metamorph = requireModule('metamorph'); |
|
|
|
function notifyMutationListeners() { |
|
Ember.run.once(Ember.View, 'notifyMutationListeners'); |
|
} |
|
|
|
// DOMManager should just abstract dom manipulation between jquery and metamorph |
|
var DOMManager = { |
|
remove: function(view) { |
|
view.morph.remove(); |
|
notifyMutationListeners(); |
|
}, |
|
|
|
prepend: function(view, html) { |
|
view.morph.prepend(html); |
|
notifyMutationListeners(); |
|
}, |
|
|
|
after: function(view, html) { |
|
view.morph.after(html); |
|
notifyMutationListeners(); |
|
}, |
|
|
|
html: function(view, html) { |
|
view.morph.html(html); |
|
notifyMutationListeners(); |
|
}, |
|
|
|
// This is messed up. |
|
replace: function(view) { |
|
var morph = view.morph; |
|
|
|
view.transitionTo('preRender'); |
|
|
|
Ember.run.schedule('render', this, function renderMetamorphView() { |
|
if (view.isDestroying) { return; } |
|
|
|
view.clearRenderedChildren(); |
|
var buffer = view.renderToBuffer(); |
|
|
|
view.invokeRecursively(function(view) { |
|
view.propertyWillChange('element'); |
|
}); |
|
view.triggerRecursively('willInsertElement'); |
|
|
|
morph.replaceWith(buffer.string()); |
|
view.transitionTo('inDOM'); |
|
|
|
view.invokeRecursively(function(view) { |
|
view.propertyDidChange('element'); |
|
}); |
|
view.triggerRecursively('didInsertElement'); |
|
|
|
notifyMutationListeners(); |
|
}); |
|
}, |
|
|
|
empty: function(view) { |
|
view.morph.html(""); |
|
notifyMutationListeners(); |
|
} |
|
}; |
|
|
|
// The `morph` and `outerHTML` properties are internal only |
|
// and not observable. |
|
|
|
/** |
|
@class _Metamorph |
|
@namespace Ember |
|
@private |
|
*/ |
|
Ember._Metamorph = Ember.Mixin.create({ |
|
isVirtual: true, |
|
tagName: '', |
|
|
|
instrumentName: 'metamorph', |
|
|
|
init: function() { |
|
this._super(); |
|
this.morph = Metamorph(); |
|
Ember.deprecate('Supplying a tagName to Metamorph views is unreliable and is deprecated. You may be setting the tagName on a Handlebars helper that creates a Metamorph.', !this.tagName); |
|
}, |
|
|
|
beforeRender: function(buffer) { |
|
buffer.push(this.morph.startTag()); |
|
buffer.pushOpeningTag(); |
|
}, |
|
|
|
afterRender: function(buffer) { |
|
buffer.pushClosingTag(); |
|
buffer.push(this.morph.endTag()); |
|
}, |
|
|
|
createElement: function() { |
|
var buffer = this.renderToBuffer(); |
|
this.outerHTML = buffer.string(); |
|
this.clearBuffer(); |
|
}, |
|
|
|
domManager: DOMManager |
|
}); |
|
|
|
/** |
|
@class _MetamorphView |
|
@namespace Ember |
|
@extends Ember.View |
|
@uses Ember._Metamorph |
|
@private |
|
*/ |
|
Ember._MetamorphView = Ember.View.extend(Ember._Metamorph); |
|
|
|
/** |
|
@class _SimpleMetamorphView |
|
@namespace Ember |
|
@extends Ember.CoreView |
|
@uses Ember._Metamorph |
|
@private |
|
*/ |
|
Ember._SimpleMetamorphView = Ember.CoreView.extend(Ember._Metamorph); |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/*globals Handlebars */ |
|
/*jshint newcap:false*/ |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set, handlebarsGet = Ember.Handlebars.get; |
|
var Metamorph = requireModule('metamorph'); |
|
function SimpleHandlebarsView(path, pathRoot, isEscaped, templateData) { |
|
this.path = path; |
|
this.pathRoot = pathRoot; |
|
this.isEscaped = isEscaped; |
|
this.templateData = templateData; |
|
|
|
this.morph = Metamorph(); |
|
this.state = 'preRender'; |
|
this.updateId = null; |
|
this._parentView = null; |
|
this.buffer = null; |
|
} |
|
|
|
Ember._SimpleHandlebarsView = SimpleHandlebarsView; |
|
|
|
SimpleHandlebarsView.prototype = { |
|
isVirtual: true, |
|
isView: true, |
|
|
|
destroy: function () { |
|
if (this.updateId) { |
|
Ember.run.cancel(this.updateId); |
|
this.updateId = null; |
|
} |
|
if (this._parentView) { |
|
this._parentView.removeChild(this); |
|
} |
|
this.morph = null; |
|
this.state = 'destroyed'; |
|
}, |
|
|
|
propertyWillChange: Ember.K, |
|
|
|
propertyDidChange: Ember.K, |
|
|
|
normalizedValue: function() { |
|
var path = this.path, |
|
pathRoot = this.pathRoot, |
|
result, templateData; |
|
|
|
// Use the pathRoot as the result if no path is provided. This |
|
// happens if the path is `this`, which gets normalized into |
|
// a `pathRoot` of the current Handlebars context and a path |
|
// of `''`. |
|
if (path === '') { |
|
result = pathRoot; |
|
} else { |
|
templateData = this.templateData; |
|
result = handlebarsGet(pathRoot, path, { data: templateData }); |
|
} |
|
|
|
return result; |
|
}, |
|
|
|
renderToBuffer: function(buffer) { |
|
var string = ''; |
|
|
|
string += this.morph.startTag(); |
|
string += this.render(); |
|
string += this.morph.endTag(); |
|
|
|
buffer.push(string); |
|
}, |
|
|
|
render: function() { |
|
// If not invoked via a triple-mustache ({{{foo}}}), escape |
|
// the content of the template. |
|
var escape = this.isEscaped; |
|
var result = this.normalizedValue(); |
|
|
|
if (result === null || result === undefined) { |
|
result = ""; |
|
} else if (!(result instanceof Handlebars.SafeString)) { |
|
result = String(result); |
|
} |
|
|
|
if (escape) { result = Handlebars.Utils.escapeExpression(result); } |
|
return result; |
|
}, |
|
|
|
rerender: function() { |
|
switch(this.state) { |
|
case 'preRender': |
|
case 'destroyed': |
|
break; |
|
case 'inBuffer': |
|
throw new Ember.Error("Something you did tried to replace an {{expression}} before it was inserted into the DOM."); |
|
case 'hasElement': |
|
case 'inDOM': |
|
this.updateId = Ember.run.scheduleOnce('render', this, 'update'); |
|
break; |
|
} |
|
|
|
return this; |
|
}, |
|
|
|
update: function () { |
|
this.updateId = null; |
|
this.morph.html(this.render()); |
|
}, |
|
|
|
transitionTo: function(state) { |
|
this.state = state; |
|
} |
|
}; |
|
|
|
var states = Ember.View.cloneStates(Ember.View.states), merge = Ember.merge; |
|
|
|
merge(states._default, { |
|
rerenderIfNeeded: Ember.K |
|
}); |
|
|
|
merge(states.inDOM, { |
|
rerenderIfNeeded: function(view) { |
|
if (view.normalizedValue() !== view._lastNormalizedValue) { |
|
view.rerender(); |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
`Ember._HandlebarsBoundView` is a private view created by the Handlebars |
|
`{{bind}}` helpers that is used to keep track of bound properties. |
|
|
|
Every time a property is bound using a `{{mustache}}`, an anonymous subclass |
|
of `Ember._HandlebarsBoundView` is created with the appropriate sub-template |
|
and context set up. When the associated property changes, just the template |
|
for this view will re-render. |
|
|
|
@class _HandlebarsBoundView |
|
@namespace Ember |
|
@extends Ember._MetamorphView |
|
@private |
|
*/ |
|
Ember._HandlebarsBoundView = Ember._MetamorphView.extend({ |
|
instrumentName: 'boundHandlebars', |
|
states: states, |
|
|
|
/** |
|
The function used to determine if the `displayTemplate` or |
|
`inverseTemplate` should be rendered. This should be a function that takes |
|
a value and returns a Boolean. |
|
|
|
@property shouldDisplayFunc |
|
@type Function |
|
@default null |
|
*/ |
|
shouldDisplayFunc: null, |
|
|
|
/** |
|
Whether the template rendered by this view gets passed the context object |
|
of its parent template, or gets passed the value of retrieving `path` |
|
from the `pathRoot`. |
|
|
|
For example, this is true when using the `{{#if}}` helper, because the |
|
template inside the helper should look up properties relative to the same |
|
object as outside the block. This would be `false` when used with `{{#with |
|
foo}}` because the template should receive the object found by evaluating |
|
`foo`. |
|
|
|
@property preserveContext |
|
@type Boolean |
|
@default false |
|
*/ |
|
preserveContext: false, |
|
|
|
/** |
|
If `preserveContext` is true, this is the object that will be used |
|
to render the template. |
|
|
|
@property previousContext |
|
@type Object |
|
*/ |
|
previousContext: null, |
|
|
|
/** |
|
The template to render when `shouldDisplayFunc` evaluates to `true`. |
|
|
|
@property displayTemplate |
|
@type Function |
|
@default null |
|
*/ |
|
displayTemplate: null, |
|
|
|
/** |
|
The template to render when `shouldDisplayFunc` evaluates to `false`. |
|
|
|
@property inverseTemplate |
|
@type Function |
|
@default null |
|
*/ |
|
inverseTemplate: null, |
|
|
|
|
|
/** |
|
The path to look up on `pathRoot` that is passed to |
|
`shouldDisplayFunc` to determine which template to render. |
|
|
|
In addition, if `preserveContext` is `false,` the object at this path will |
|
be passed to the template when rendering. |
|
|
|
@property path |
|
@type String |
|
@default null |
|
*/ |
|
path: null, |
|
|
|
/** |
|
The object from which the `path` will be looked up. Sometimes this is the |
|
same as the `previousContext`, but in cases where this view has been |
|
generated for paths that start with a keyword such as `view` or |
|
`controller`, the path root will be that resolved object. |
|
|
|
@property pathRoot |
|
@type Object |
|
*/ |
|
pathRoot: null, |
|
|
|
normalizedValue: function() { |
|
var path = get(this, 'path'), |
|
pathRoot = get(this, 'pathRoot'), |
|
valueNormalizer = get(this, 'valueNormalizerFunc'), |
|
result, templateData; |
|
|
|
// Use the pathRoot as the result if no path is provided. This |
|
// happens if the path is `this`, which gets normalized into |
|
// a `pathRoot` of the current Handlebars context and a path |
|
// of `''`. |
|
if (path === '') { |
|
result = pathRoot; |
|
} else { |
|
templateData = get(this, 'templateData'); |
|
result = handlebarsGet(pathRoot, path, { data: templateData }); |
|
} |
|
|
|
return valueNormalizer ? valueNormalizer(result) : result; |
|
}, |
|
|
|
rerenderIfNeeded: function() { |
|
this.currentState.rerenderIfNeeded(this); |
|
}, |
|
|
|
/** |
|
Determines which template to invoke, sets up the correct state based on |
|
that logic, then invokes the default `Ember.View` `render` implementation. |
|
|
|
This method will first look up the `path` key on `pathRoot`, |
|
then pass that value to the `shouldDisplayFunc` function. If that returns |
|
`true,` the `displayTemplate` function will be rendered to DOM. Otherwise, |
|
`inverseTemplate`, if specified, will be rendered. |
|
|
|
For example, if this `Ember._HandlebarsBoundView` represented the `{{#with |
|
foo}}` helper, it would look up the `foo` property of its context, and |
|
`shouldDisplayFunc` would always return true. The object found by looking |
|
up `foo` would be passed to `displayTemplate`. |
|
|
|
@method render |
|
@param {Ember.RenderBuffer} buffer |
|
*/ |
|
render: function(buffer) { |
|
// If not invoked via a triple-mustache ({{{foo}}}), escape |
|
// the content of the template. |
|
var escape = get(this, 'isEscaped'); |
|
|
|
var shouldDisplay = get(this, 'shouldDisplayFunc'), |
|
preserveContext = get(this, 'preserveContext'), |
|
context = get(this, 'previousContext'); |
|
|
|
var _contextController = get(this, '_contextController'); |
|
|
|
var inverseTemplate = get(this, 'inverseTemplate'), |
|
displayTemplate = get(this, 'displayTemplate'); |
|
|
|
var result = this.normalizedValue(); |
|
this._lastNormalizedValue = result; |
|
|
|
// First, test the conditional to see if we should |
|
// render the template or not. |
|
if (shouldDisplay(result)) { |
|
set(this, 'template', displayTemplate); |
|
|
|
// If we are preserving the context (for example, if this |
|
// is an #if block, call the template with the same object. |
|
if (preserveContext) { |
|
set(this, '_context', context); |
|
} else { |
|
// Otherwise, determine if this is a block bind or not. |
|
// If so, pass the specified object to the template |
|
if (displayTemplate) { |
|
if (_contextController) { |
|
set(_contextController, 'content', result); |
|
result = _contextController; |
|
} |
|
set(this, '_context', result); |
|
} else { |
|
// This is not a bind block, just push the result of the |
|
// expression to the render context and return. |
|
if (result === null || result === undefined) { |
|
result = ""; |
|
} else if (!(result instanceof Handlebars.SafeString)) { |
|
result = String(result); |
|
} |
|
|
|
if (escape) { result = Handlebars.Utils.escapeExpression(result); } |
|
buffer.push(result); |
|
return; |
|
} |
|
} |
|
} else if (inverseTemplate) { |
|
set(this, 'template', inverseTemplate); |
|
|
|
if (preserveContext) { |
|
set(this, '_context', context); |
|
} else { |
|
set(this, '_context', result); |
|
} |
|
} else { |
|
set(this, 'template', function() { return ''; }); |
|
} |
|
|
|
return this._super(buffer); |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt; |
|
var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; |
|
var handlebarsGetEscaped = Ember.Handlebars.getEscaped; |
|
var forEach = Ember.ArrayPolyfills.forEach; |
|
var o_create = Ember.create; |
|
|
|
var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; |
|
|
|
function exists(value) { |
|
return !Ember.isNone(value); |
|
} |
|
|
|
// Binds a property into the DOM. This will create a hook in DOM that the |
|
// KVO system will look for and update if the property changes. |
|
function bind(property, options, preserveContext, shouldDisplay, valueNormalizer, childProperties) { |
|
var data = options.data, |
|
fn = options.fn, |
|
inverse = options.inverse, |
|
view = data.view, |
|
currentContext = this, |
|
normalized, observer, i; |
|
|
|
normalized = normalizePath(currentContext, property, data); |
|
|
|
// Set up observers for observable objects |
|
if ('object' === typeof this) { |
|
if (data.insideGroup) { |
|
observer = function() { |
|
Ember.run.once(view, 'rerender'); |
|
}; |
|
|
|
var template, context, result = handlebarsGet(currentContext, property, options); |
|
|
|
result = valueNormalizer ? valueNormalizer(result) : result; |
|
|
|
context = preserveContext ? currentContext : result; |
|
if (shouldDisplay(result)) { |
|
template = fn; |
|
} else if (inverse) { |
|
template = inverse; |
|
} |
|
|
|
template(context, { data: options.data }); |
|
} else { |
|
// Create the view that will wrap the output of this template/property |
|
// and add it to the nearest view's childViews array. |
|
// See the documentation of Ember._HandlebarsBoundView for more. |
|
var bindView = view.createChildView(Ember._HandlebarsBoundView, { |
|
preserveContext: preserveContext, |
|
shouldDisplayFunc: shouldDisplay, |
|
valueNormalizerFunc: valueNormalizer, |
|
displayTemplate: fn, |
|
inverseTemplate: inverse, |
|
path: property, |
|
pathRoot: currentContext, |
|
previousContext: currentContext, |
|
isEscaped: !options.hash.unescaped, |
|
templateData: options.data |
|
}); |
|
|
|
if (options.hash.controller) { |
|
bindView.set('_contextController', this.container.lookupFactory('controller:'+options.hash.controller).create({ |
|
container: currentContext.container, |
|
parentController: currentContext, |
|
target: currentContext |
|
})); |
|
} |
|
|
|
view.appendChild(bindView); |
|
|
|
observer = function() { |
|
Ember.run.scheduleOnce('render', bindView, 'rerenderIfNeeded'); |
|
}; |
|
} |
|
|
|
// Observes the given property on the context and |
|
// tells the Ember._HandlebarsBoundView to re-render. If property |
|
// is an empty string, we are printing the current context |
|
// object ({{this}}) so updating it is not our responsibility. |
|
if (normalized.path !== '') { |
|
view.registerObserver(normalized.root, normalized.path, observer); |
|
if (childProperties) { |
|
for (i=0; i<childProperties.length; i++) { |
|
view.registerObserver(normalized.root, normalized.path+'.'+childProperties[i], observer); |
|
} |
|
} |
|
} |
|
} else { |
|
// The object is not observable, so just render it out and |
|
// be done with it. |
|
data.buffer.push(handlebarsGetEscaped(currentContext, property, options)); |
|
} |
|
} |
|
|
|
EmberHandlebars.bind = bind; |
|
|
|
function simpleBind(currentContext, property, options) { |
|
var data = options.data, |
|
view = data.view, |
|
normalized, observer, pathRoot, output; |
|
|
|
normalized = normalizePath(currentContext, property, data); |
|
pathRoot = normalized.root; |
|
|
|
// Set up observers for observable objects |
|
if (pathRoot && ('object' === typeof pathRoot)) { |
|
if (data.insideGroup) { |
|
observer = function() { |
|
Ember.run.once(view, 'rerender'); |
|
}; |
|
|
|
output = handlebarsGetEscaped(currentContext, property, options); |
|
|
|
data.buffer.push(output); |
|
} else { |
|
var bindView = new Ember._SimpleHandlebarsView( |
|
property, currentContext, !options.hash.unescaped, options.data |
|
); |
|
|
|
bindView._parentView = view; |
|
view.appendChild(bindView); |
|
|
|
observer = function() { |
|
Ember.run.scheduleOnce('render', bindView, 'rerender'); |
|
}; |
|
} |
|
|
|
// Observes the given property on the context and |
|
// tells the Ember._HandlebarsBoundView to re-render. If property |
|
// is an empty string, we are printing the current context |
|
// object ({{this}}) so updating it is not our responsibility. |
|
if (normalized.path !== '') { |
|
view.registerObserver(normalized.root, normalized.path, observer); |
|
} |
|
} else { |
|
// The object is not observable, so just render it out and |
|
// be done with it. |
|
output = handlebarsGetEscaped(currentContext, property, options); |
|
data.buffer.push(output); |
|
} |
|
} |
|
|
|
function shouldDisplayIfHelperContent(result) { |
|
var truthy = result && get(result, 'isTruthy'); |
|
if (typeof truthy === 'boolean') { return truthy; } |
|
|
|
if (Ember.isArray(result)) { |
|
return get(result, 'length') !== 0; |
|
} else { |
|
return !!result; |
|
} |
|
} |
|
|
|
/** |
|
'_triageMustache' is used internally select between a binding, helper, or component for |
|
the given context. Until this point, it would be hard to determine if the |
|
mustache is a property reference or a regular helper reference. This triage |
|
helper resolves that. |
|
|
|
This would not be typically invoked by directly. |
|
|
|
@private |
|
@method _triageMustache |
|
@for Ember.Handlebars.helpers |
|
@param {String} property Property/helperID to triage |
|
@param {Object} options hash of template/rendering options |
|
@return {String} HTML string |
|
*/ |
|
EmberHandlebars.registerHelper('_triageMustache', function(property, options) { |
|
Ember.assert("You cannot pass more than one argument to the _triageMustache helper", arguments.length <= 2); |
|
|
|
if (helpers[property]) { |
|
return helpers[property].call(this, options); |
|
} |
|
|
|
var helper = Ember.Handlebars.resolveHelper(options.data.view.container, property); |
|
if (helper) { |
|
return helper.call(this, options); |
|
} |
|
|
|
return helpers.bind.call(this, property, options); |
|
}); |
|
|
|
Ember.Handlebars.resolveHelper = function(container, name) { |
|
|
|
if (!container || name.indexOf('-') === -1) { |
|
return; |
|
} |
|
|
|
var helper = container.lookup('helper:' + name); |
|
if (!helper) { |
|
var componentLookup = container.lookup('component-lookup:main'); |
|
Ember.assert("Could not find 'component-lookup:main' on the provided container, which is necessary for performing component lookups", componentLookup); |
|
|
|
var Component = componentLookup.lookupFactory(name, container); |
|
if (Component) { |
|
helper = EmberHandlebars.makeViewHelper(Component); |
|
container.register('helper:' + name, helper); |
|
} |
|
} |
|
return helper; |
|
}; |
|
|
|
/** |
|
`bind` can be used to display a value, then update that value if it |
|
changes. For example, if you wanted to print the `title` property of |
|
`content`: |
|
|
|
```handlebars |
|
{{bind "content.title"}} |
|
``` |
|
|
|
This will return the `title` property as a string, then create a new observer |
|
at the specified path. If it changes, it will update the value in DOM. Note |
|
that if you need to support IE7 and IE8 you must modify the model objects |
|
properties using `Ember.get()` and `Ember.set()` for this to work as it |
|
relies on Ember's KVO system. For all other browsers this will be handled for |
|
you automatically. |
|
|
|
@private |
|
@method bind |
|
@for Ember.Handlebars.helpers |
|
@param {String} property Property to bind |
|
@param {Function} fn Context to provide for rendering |
|
@return {String} HTML string |
|
*/ |
|
EmberHandlebars.registerHelper('bind', function bindHelper(property, options) { |
|
Ember.assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2); |
|
|
|
var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this; |
|
|
|
if (!options.fn) { |
|
return simpleBind(context, property, options); |
|
} |
|
|
|
return bind.call(context, property, options, false, exists); |
|
}); |
|
|
|
/** |
|
Use the `boundIf` helper to create a conditional that re-evaluates |
|
whenever the truthiness of the bound value changes. |
|
|
|
```handlebars |
|
{{#boundIf "content.shouldDisplayTitle"}} |
|
{{content.title}} |
|
{{/boundIf}} |
|
``` |
|
|
|
@private |
|
@method boundIf |
|
@for Ember.Handlebars.helpers |
|
@param {String} property Property to bind |
|
@param {Function} fn Context to provide for rendering |
|
@return {String} HTML string |
|
*/ |
|
EmberHandlebars.registerHelper('boundIf', function boundIfHelper(property, fn) { |
|
var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; |
|
|
|
return bind.call(context, property, fn, true, shouldDisplayIfHelperContent, shouldDisplayIfHelperContent, ['isTruthy', 'length']); |
|
}); |
|
|
|
|
|
/** |
|
@private |
|
|
|
Use the `unboundIf` helper to create a conditional that evaluates once. |
|
|
|
```handlebars |
|
{{#unboundIf "content.shouldDisplayTitle"}} |
|
{{content.title}} |
|
{{/unboundIf}} |
|
``` |
|
|
|
@method unboundIf |
|
@for Ember.Handlebars.helpers |
|
@param {String} property Property to bind |
|
@param {Function} fn Context to provide for rendering |
|
@return {String} HTML string |
|
*/ |
|
EmberHandlebars.registerHelper('unboundIf', function unboundIfHelper(property, fn) { |
|
var context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this, |
|
data = fn.data, |
|
template = fn.fn, |
|
inverse = fn.inverse, |
|
normalized, propertyValue, result; |
|
|
|
normalized = normalizePath(context, property, data); |
|
propertyValue = handlebarsGet(context, property, fn); |
|
|
|
if (!shouldDisplayIfHelperContent(propertyValue)) { |
|
template = inverse; |
|
} |
|
|
|
template(context, { data: data }); |
|
}); |
|
|
|
/** |
|
Use the `{{with}}` helper when you want to scope context. Take the following code as an example: |
|
|
|
```handlebars |
|
<h5>{{user.name}}</h5> |
|
|
|
<div class="role"> |
|
<h6>{{user.role.label}}</h6> |
|
<span class="role-id">{{user.role.id}}</span> |
|
|
|
<p class="role-desc">{{user.role.description}}</p> |
|
</div> |
|
``` |
|
|
|
`{{with}}` can be our best friend in these cases, |
|
instead of writing `user.role.*` over and over, we use `{{#with user.role}}`. |
|
Now the context within the `{{#with}} .. {{/with}}` block is `user.role` so you can do the following: |
|
|
|
```handlebars |
|
<h5>{{user.name}}</h5> |
|
|
|
<div class="role"> |
|
{{#with user.role}} |
|
<h6>{{label}}</h6> |
|
<span class="role-id">{{id}}</span> |
|
|
|
<p class="role-desc">{{description}}</p> |
|
{{/with}} |
|
</div> |
|
``` |
|
|
|
### `as` operator |
|
|
|
This operator aliases the scope to a new name. It's helpful for semantic clarity and to retain |
|
default scope or to reference from another `{{with}}` block. |
|
|
|
```handlebars |
|
// posts might not be |
|
{{#with user.posts as blogPosts}} |
|
<div class="notice"> |
|
There are {{blogPosts.length}} blog posts written by {{user.name}}. |
|
</div> |
|
|
|
{{#each post in blogPosts}} |
|
<li>{{post.title}}</li> |
|
{{/each}} |
|
{{/with}} |
|
``` |
|
|
|
Without the `as` operator, it would be impossible to reference `user.name` in the example above. |
|
|
|
NOTE: The alias should not reuse a name from the bound property path. |
|
For example: `{{#with foo.bar as foo}}` is not supported because it attempts to alias using |
|
the first part of the property path, `foo`. Instead, use `{{#with foo.bar as baz}}`. |
|
|
|
### `controller` option |
|
|
|
Adding `controller='something'` instructs the `{{with}}` helper to create and use an instance of |
|
the specified controller with the new context as its content. |
|
|
|
This is very similar to using an `itemController` option with the `{{each}}` helper. |
|
|
|
```handlebars |
|
{{#with users.posts controller='userBlogPosts'}} |
|
{{!- The current context is wrapped in our controller instance }} |
|
{{/with}} |
|
``` |
|
|
|
In the above example, the template provided to the `{{with}}` block is now wrapped in the |
|
`userBlogPost` controller, which provides a very elegant way to decorate the context with custom |
|
functions/properties. |
|
|
|
@method with |
|
@for Ember.Handlebars.helpers |
|
@param {Function} context |
|
@param {Hash} options |
|
@return {String} HTML string |
|
*/ |
|
EmberHandlebars.registerHelper('with', function withHelper(context, options) { |
|
if (arguments.length === 4) { |
|
var keywordName, path, rootPath, normalized, contextPath; |
|
|
|
Ember.assert("If you pass more than one argument to the with helper, it must be in the form #with foo as bar", arguments[1] === "as"); |
|
options = arguments[3]; |
|
keywordName = arguments[2]; |
|
path = arguments[0]; |
|
|
|
Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); |
|
|
|
var localizedOptions = o_create(options); |
|
localizedOptions.data = o_create(options.data); |
|
localizedOptions.data.keywords = o_create(options.data.keywords || {}); |
|
|
|
if (Ember.isGlobalPath(path)) { |
|
contextPath = path; |
|
} else { |
|
normalized = normalizePath(this, path, options.data); |
|
path = normalized.path; |
|
rootPath = normalized.root; |
|
|
|
// This is a workaround for the fact that you cannot bind separate objects |
|
// together. When we implement that functionality, we should use it here. |
|
var contextKey = Ember.$.expando + Ember.guidFor(rootPath); |
|
localizedOptions.data.keywords[contextKey] = rootPath; |
|
// if the path is '' ("this"), just bind directly to the current context |
|
contextPath = path ? contextKey + '.' + path : contextKey; |
|
} |
|
|
|
Ember.bind(localizedOptions.data.keywords, keywordName, contextPath); |
|
|
|
return bind.call(this, path, localizedOptions, true, exists); |
|
} else { |
|
Ember.assert("You must pass exactly one argument to the with helper", arguments.length === 2); |
|
Ember.assert("You must pass a block to the with helper", options.fn && options.fn !== Handlebars.VM.noop); |
|
return helpers.bind.call(options.contexts[0], context, options); |
|
} |
|
}); |
|
|
|
|
|
/** |
|
See [boundIf](/api/classes/Ember.Handlebars.helpers.html#method_boundIf) |
|
and [unboundIf](/api/classes/Ember.Handlebars.helpers.html#method_unboundIf) |
|
|
|
@method if |
|
@for Ember.Handlebars.helpers |
|
@param {Function} context |
|
@param {Hash} options |
|
@return {String} HTML string |
|
*/ |
|
EmberHandlebars.registerHelper('if', function ifHelper(context, options) { |
|
Ember.assert("You must pass exactly one argument to the if helper", arguments.length === 2); |
|
Ember.assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop); |
|
if (options.data.isUnbound) { |
|
return helpers.unboundIf.call(options.contexts[0], context, options); |
|
} else { |
|
return helpers.boundIf.call(options.contexts[0], context, options); |
|
} |
|
}); |
|
|
|
/** |
|
@method unless |
|
@for Ember.Handlebars.helpers |
|
@param {Function} context |
|
@param {Hash} options |
|
@return {String} HTML string |
|
*/ |
|
EmberHandlebars.registerHelper('unless', function unlessHelper(context, options) { |
|
Ember.assert("You must pass exactly one argument to the unless helper", arguments.length === 2); |
|
Ember.assert("You must pass a block to the unless helper", options.fn && options.fn !== Handlebars.VM.noop); |
|
|
|
var fn = options.fn, inverse = options.inverse; |
|
|
|
options.fn = inverse; |
|
options.inverse = fn; |
|
|
|
if (options.data.isUnbound) { |
|
return helpers.unboundIf.call(options.contexts[0], context, options); |
|
} else { |
|
return helpers.boundIf.call(options.contexts[0], context, options); |
|
} |
|
}); |
|
|
|
/** |
|
`bind-attr` allows you to create a binding between DOM element attributes and |
|
Ember objects. For example: |
|
|
|
```handlebars |
|
<img {{bind-attr src="imageUrl" alt="imageTitle"}}> |
|
``` |
|
|
|
The above handlebars template will fill the `<img>`'s `src` attribute will |
|
the value of the property referenced with `"imageUrl"` and its `alt` |
|
attribute with the value of the property referenced with `"imageTitle"`. |
|
|
|
If the rendering context of this template is the following object: |
|
|
|
```javascript |
|
{ |
|
imageUrl: 'http://lolcats.info/haz-a-funny', |
|
imageTitle: 'A humorous image of a cat' |
|
} |
|
``` |
|
|
|
The resulting HTML output will be: |
|
|
|
```html |
|
<img src="http://lolcats.info/haz-a-funny" alt="A humorous image of a cat"> |
|
``` |
|
|
|
`bind-attr` cannot redeclare existing DOM element attributes. The use of `src` |
|
in the following `bind-attr` example will be ignored and the hard coded value |
|
of `src="/failwhale.gif"` will take precedence: |
|
|
|
```handlebars |
|
<img src="/failwhale.gif" {{bind-attr src="imageUrl" alt="imageTitle"}}> |
|
``` |
|
|
|
### `bind-attr` and the `class` attribute |
|
|
|
`bind-attr` supports a special syntax for handling a number of cases unique |
|
to the `class` DOM element attribute. The `class` attribute combines |
|
multiple discrete values into a single attribute as a space-delimited |
|
list of strings. Each string can be: |
|
|
|
* a string return value of an object's property. |
|
* a boolean return value of an object's property |
|
* a hard-coded value |
|
|
|
A string return value works identically to other uses of `bind-attr`. The |
|
return value of the property will become the value of the attribute. For |
|
example, the following view and template: |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
someProperty: function() { |
|
return "aValue"; |
|
}.property() |
|
}) |
|
``` |
|
|
|
```handlebars |
|
<img {{bind-attr class="view.someProperty}}> |
|
``` |
|
|
|
Result in the following rendered output: |
|
|
|
```html |
|
<img class="aValue"> |
|
``` |
|
|
|
A boolean return value will insert a specified class name if the property |
|
returns `true` and remove the class name if the property returns `false`. |
|
|
|
A class name is provided via the syntax |
|
`somePropertyName:class-name-if-true`. |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
someBool: true |
|
}) |
|
``` |
|
|
|
```handlebars |
|
<img {{bind-attr class="view.someBool:class-name-if-true"}}> |
|
``` |
|
|
|
Result in the following rendered output: |
|
|
|
```html |
|
<img class="class-name-if-true"> |
|
``` |
|
|
|
An additional section of the binding can be provided if you want to |
|
replace the existing class instead of removing it when the boolean |
|
value changes: |
|
|
|
```handlebars |
|
<img {{bind-attr class="view.someBool:class-name-if-true:class-name-if-false"}}> |
|
``` |
|
|
|
A hard-coded value can be used by prepending `:` to the desired |
|
class name: `:class-name-to-always-apply`. |
|
|
|
```handlebars |
|
<img {{bind-attr class=":class-name-to-always-apply"}}> |
|
``` |
|
|
|
Results in the following rendered output: |
|
|
|
```html |
|
<img class="class-name-to-always-apply"> |
|
``` |
|
|
|
All three strategies - string return value, boolean return value, and |
|
hard-coded value – can be combined in a single declaration: |
|
|
|
```handlebars |
|
<img {{bind-attr class=":class-name-to-always-apply view.someBool:class-name-if-true view.someProperty"}}> |
|
``` |
|
|
|
@method bind-attr |
|
@for Ember.Handlebars.helpers |
|
@param {Hash} options |
|
@return {String} HTML string |
|
*/ |
|
EmberHandlebars.registerHelper('bind-attr', function bindAttrHelper(options) { |
|
|
|
var attrs = options.hash; |
|
|
|
Ember.assert("You must specify at least one hash argument to bind-attr", !!Ember.keys(attrs).length); |
|
|
|
var view = options.data.view; |
|
var ret = []; |
|
var ctx = this; |
|
|
|
// Generate a unique id for this element. This will be added as a |
|
// data attribute to the element so it can be looked up when |
|
// the bound property changes. |
|
var dataId = ++Ember.uuid; |
|
|
|
// Handle classes differently, as we can bind multiple classes |
|
var classBindings = attrs['class']; |
|
if (classBindings != null) { |
|
var classResults = EmberHandlebars.bindClasses(this, classBindings, view, dataId, options); |
|
|
|
ret.push('class="' + Handlebars.Utils.escapeExpression(classResults.join(' ')) + '"'); |
|
delete attrs['class']; |
|
} |
|
|
|
var attrKeys = Ember.keys(attrs); |
|
|
|
// For each attribute passed, create an observer and emit the |
|
// current value of the property as an attribute. |
|
forEach.call(attrKeys, function(attr) { |
|
var path = attrs[attr], |
|
normalized; |
|
|
|
Ember.assert(fmt("You must provide an expression as the value of bound attribute. You specified: %@=%@", [attr, path]), typeof path === 'string'); |
|
|
|
normalized = normalizePath(ctx, path, options.data); |
|
|
|
var value = (path === 'this') ? normalized.root : handlebarsGet(ctx, path, options), |
|
type = Ember.typeOf(value); |
|
|
|
Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [value]), value === null || value === undefined || type === 'number' || type === 'string' || type === 'boolean'); |
|
|
|
var observer, invoker; |
|
|
|
observer = function observer() { |
|
var result = handlebarsGet(ctx, path, options); |
|
|
|
Ember.assert(fmt("Attributes must be numbers, strings or booleans, not %@", [result]), |
|
result === null || result === undefined || typeof result === 'number' || |
|
typeof result === 'string' || typeof result === 'boolean'); |
|
|
|
var elem = view.$("[data-bindattr-" + dataId + "='" + dataId + "']"); |
|
|
|
// If we aren't able to find the element, it means the element |
|
// to which we were bound has been removed from the view. |
|
// In that case, we can assume the template has been re-rendered |
|
// and we need to clean up the observer. |
|
if (!elem || elem.length === 0) { |
|
Ember.removeObserver(normalized.root, normalized.path, invoker); |
|
return; |
|
} |
|
|
|
Ember.View.applyAttributeBindings(elem, attr, result); |
|
}; |
|
|
|
// Add an observer to the view for when the property changes. |
|
// When the observer fires, find the element using the |
|
// unique data id and update the attribute to the new value. |
|
// Note: don't add observer when path is 'this' or path |
|
// is whole keyword e.g. {{#each x in list}} ... {{bind-attr attr="x"}} |
|
if (path !== 'this' && !(normalized.isKeyword && normalized.path === '' )) { |
|
view.registerObserver(normalized.root, normalized.path, observer); |
|
} |
|
|
|
// if this changes, also change the logic in ember-views/lib/views/view.js |
|
if ((type === 'string' || (type === 'number' && !isNaN(value)))) { |
|
ret.push(attr + '="' + Handlebars.Utils.escapeExpression(value) + '"'); |
|
} else if (value && type === 'boolean') { |
|
// The developer controls the attr name, so it should always be safe |
|
ret.push(attr + '="' + attr + '"'); |
|
} |
|
}, this); |
|
|
|
// Add the unique identifier |
|
// NOTE: We use all lower-case since Firefox has problems with mixed case in SVG |
|
ret.push('data-bindattr-' + dataId + '="' + dataId + '"'); |
|
return new EmberHandlebars.SafeString(ret.join(' ')); |
|
}); |
|
|
|
/** |
|
See `bind-attr` |
|
|
|
@method bindAttr |
|
@for Ember.Handlebars.helpers |
|
@deprecated |
|
@param {Function} context |
|
@param {Hash} options |
|
@return {String} HTML string |
|
*/ |
|
EmberHandlebars.registerHelper('bindAttr', function bindAttrHelper() { |
|
Ember.warn("The 'bindAttr' view helper is deprecated in favor of 'bind-attr'"); |
|
return EmberHandlebars.helpers['bind-attr'].apply(this, arguments); |
|
}); |
|
|
|
/** |
|
Helper that, given a space-separated string of property paths and a context, |
|
returns an array of class names. Calling this method also has the side |
|
effect of setting up observers at those property paths, such that if they |
|
change, the correct class name will be reapplied to the DOM element. |
|
|
|
For example, if you pass the string "fooBar", it will first look up the |
|
"fooBar" value of the context. If that value is true, it will add the |
|
"foo-bar" class to the current element (i.e., the dasherized form of |
|
"fooBar"). If the value is a string, it will add that string as the class. |
|
Otherwise, it will not add any new class name. |
|
|
|
@private |
|
@method bindClasses |
|
@for Ember.Handlebars |
|
@param {Ember.Object} context The context from which to lookup properties |
|
@param {String} classBindings A string, space-separated, of class bindings |
|
to use |
|
@param {Ember.View} view The view in which observers should look for the |
|
element to update |
|
@param {Srting} bindAttrId Optional bindAttr id used to lookup elements |
|
@return {Array} An array of class names to add |
|
*/ |
|
EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId, options) { |
|
var ret = [], newClass, value, elem; |
|
|
|
// Helper method to retrieve the property from the context and |
|
// determine which class string to return, based on whether it is |
|
// a Boolean or not. |
|
var classStringForPath = function(root, parsedPath, options) { |
|
var val, |
|
path = parsedPath.path; |
|
|
|
if (path === 'this') { |
|
val = root; |
|
} else if (path === '') { |
|
val = true; |
|
} else { |
|
val = handlebarsGet(root, path, options); |
|
} |
|
|
|
return Ember.View._classStringForValue(path, val, parsedPath.className, parsedPath.falsyClassName); |
|
}; |
|
|
|
// For each property passed, loop through and setup |
|
// an observer. |
|
forEach.call(classBindings.split(' '), function(binding) { |
|
|
|
// Variable in which the old class value is saved. The observer function |
|
// closes over this variable, so it knows which string to remove when |
|
// the property changes. |
|
var oldClass; |
|
|
|
var observer, invoker; |
|
|
|
var parsedPath = Ember.View._parsePropertyPath(binding), |
|
path = parsedPath.path, |
|
pathRoot = context, |
|
normalized; |
|
|
|
if (path !== '' && path !== 'this') { |
|
normalized = normalizePath(context, path, options.data); |
|
|
|
pathRoot = normalized.root; |
|
path = normalized.path; |
|
} |
|
|
|
// Set up an observer on the context. If the property changes, toggle the |
|
// class name. |
|
observer = function() { |
|
// Get the current value of the property |
|
newClass = classStringForPath(context, parsedPath, options); |
|
elem = bindAttrId ? view.$("[data-bindattr-" + bindAttrId + "='" + bindAttrId + "']") : view.$(); |
|
|
|
// If we can't find the element anymore, a parent template has been |
|
// re-rendered and we've been nuked. Remove the observer. |
|
if (!elem || elem.length === 0) { |
|
Ember.removeObserver(pathRoot, path, invoker); |
|
} else { |
|
// If we had previously added a class to the element, remove it. |
|
if (oldClass) { |
|
elem.removeClass(oldClass); |
|
} |
|
|
|
// If necessary, add a new class. Make sure we keep track of it so |
|
// it can be removed in the future. |
|
if (newClass) { |
|
elem.addClass(newClass); |
|
oldClass = newClass; |
|
} else { |
|
oldClass = null; |
|
} |
|
} |
|
}; |
|
|
|
if (path !== '' && path !== 'this') { |
|
view.registerObserver(pathRoot, path, observer); |
|
} |
|
|
|
// We've already setup the observer; now we just need to figure out the |
|
// correct behavior right now on the first pass through. |
|
value = classStringForPath(context, parsedPath, options); |
|
|
|
if (value) { |
|
ret.push(value); |
|
|
|
// Make sure we save the current value so that it can be removed if the |
|
// observer fires. |
|
oldClass = value; |
|
} |
|
}); |
|
|
|
return ret; |
|
}; |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/*globals Handlebars */ |
|
|
|
// TODO: Don't require the entire module |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
var EmberHandlebars = Ember.Handlebars; |
|
var LOWERCASE_A_Z = /^[a-z]/; |
|
var VIEW_PREFIX = /^view\./; |
|
|
|
function makeBindings(thisContext, options) { |
|
var hash = options.hash, |
|
hashType = options.hashTypes; |
|
|
|
for (var prop in hash) { |
|
if (hashType[prop] === 'ID') { |
|
|
|
var value = hash[prop]; |
|
|
|
if (Ember.IS_BINDING.test(prop)) { |
|
Ember.warn("You're attempting to render a view by passing " + prop + "=" + value + " to a view helper, but this syntax is ambiguous. You should either surround " + value + " in quotes or remove `Binding` from " + prop + "."); |
|
} else { |
|
hash[prop + 'Binding'] = value; |
|
hashType[prop + 'Binding'] = 'STRING'; |
|
delete hash[prop]; |
|
delete hashType[prop]; |
|
} |
|
} |
|
} |
|
|
|
if (hash.hasOwnProperty('idBinding')) { |
|
// id can't be bound, so just perform one-time lookup. |
|
hash.id = EmberHandlebars.get(thisContext, hash.idBinding, options); |
|
hashType.id = 'STRING'; |
|
delete hash.idBinding; |
|
delete hashType.idBinding; |
|
} |
|
} |
|
|
|
EmberHandlebars.ViewHelper = Ember.Object.create({ |
|
|
|
propertiesFromHTMLOptions: function(options) { |
|
var hash = options.hash, data = options.data; |
|
var extensions = {}, |
|
classes = hash['class'], |
|
dup = false; |
|
|
|
if (hash.id) { |
|
extensions.elementId = hash.id; |
|
dup = true; |
|
} |
|
|
|
if (hash.tag) { |
|
extensions.tagName = hash.tag; |
|
dup = true; |
|
} |
|
|
|
if (classes) { |
|
classes = classes.split(' '); |
|
extensions.classNames = classes; |
|
dup = true; |
|
} |
|
|
|
if (hash.classBinding) { |
|
extensions.classNameBindings = hash.classBinding.split(' '); |
|
dup = true; |
|
} |
|
|
|
if (hash.classNameBindings) { |
|
if (extensions.classNameBindings === undefined) extensions.classNameBindings = []; |
|
extensions.classNameBindings = extensions.classNameBindings.concat(hash.classNameBindings.split(' ')); |
|
dup = true; |
|
} |
|
|
|
if (hash.attributeBindings) { |
|
Ember.assert("Setting 'attributeBindings' via Handlebars is not allowed. Please subclass Ember.View and set it there instead."); |
|
extensions.attributeBindings = null; |
|
dup = true; |
|
} |
|
|
|
if (dup) { |
|
hash = Ember.$.extend({}, hash); |
|
delete hash.id; |
|
delete hash.tag; |
|
delete hash['class']; |
|
delete hash.classBinding; |
|
} |
|
|
|
// Set the proper context for all bindings passed to the helper. This applies to regular attribute bindings |
|
// as well as class name bindings. If the bindings are local, make them relative to the current context |
|
// instead of the view. |
|
var path; |
|
|
|
// Evaluate the context of regular attribute bindings: |
|
for (var prop in hash) { |
|
if (!hash.hasOwnProperty(prop)) { continue; } |
|
|
|
// Test if the property ends in "Binding" |
|
if (Ember.IS_BINDING.test(prop) && typeof hash[prop] === 'string') { |
|
path = this.contextualizeBindingPath(hash[prop], data); |
|
if (path) { hash[prop] = path; } |
|
} |
|
} |
|
|
|
// Evaluate the context of class name bindings: |
|
if (extensions.classNameBindings) { |
|
for (var b in extensions.classNameBindings) { |
|
var full = extensions.classNameBindings[b]; |
|
if (typeof full === 'string') { |
|
// Contextualize the path of classNameBinding so this: |
|
// |
|
// classNameBinding="isGreen:green" |
|
// |
|
// is converted to this: |
|
// |
|
// classNameBinding="_parentView.context.isGreen:green" |
|
var parsedPath = Ember.View._parsePropertyPath(full); |
|
path = this.contextualizeBindingPath(parsedPath.path, data); |
|
if (path) { extensions.classNameBindings[b] = path + parsedPath.classNames; } |
|
} |
|
} |
|
} |
|
|
|
return Ember.$.extend(hash, extensions); |
|
}, |
|
|
|
// Transform bindings from the current context to a context that can be evaluated within the view. |
|
// Returns null if the path shouldn't be changed. |
|
// |
|
// TODO: consider the addition of a prefix that would allow this method to return `path`. |
|
contextualizeBindingPath: function(path, data) { |
|
var normalized = Ember.Handlebars.normalizePath(null, path, data); |
|
if (normalized.isKeyword) { |
|
return 'templateData.keywords.' + path; |
|
} else if (Ember.isGlobalPath(path)) { |
|
return null; |
|
} else if (path === 'this' || path === '') { |
|
return '_parentView.context'; |
|
} else { |
|
return '_parentView.context.' + path; |
|
} |
|
}, |
|
|
|
helper: function(thisContext, path, options) { |
|
var data = options.data, |
|
fn = options.fn, |
|
newView; |
|
|
|
makeBindings(thisContext, options); |
|
|
|
if ('string' === typeof path) { |
|
|
|
// TODO: this is a lame conditional, this should likely change |
|
// but something along these lines will likely need to be added |
|
// as deprecation warnings |
|
// |
|
if (options.types[0] === 'STRING' && LOWERCASE_A_Z.test(path) && !VIEW_PREFIX.test(path)) { |
|
Ember.assert("View requires a container", !!data.view.container); |
|
newView = data.view.container.lookupFactory('view:' + path); |
|
} else { |
|
newView = EmberHandlebars.get(thisContext, path, options); |
|
} |
|
|
|
Ember.assert("Unable to find view at path '" + path + "'", !!newView); |
|
} else { |
|
newView = path; |
|
} |
|
|
|
Ember.assert(Ember.String.fmt('You must pass a view to the #view helper, not %@ (%@)', [path, newView]), Ember.View.detect(newView) || Ember.View.detectInstance(newView)); |
|
|
|
var viewOptions = this.propertiesFromHTMLOptions(options, thisContext); |
|
var currentView = data.view; |
|
viewOptions.templateData = data; |
|
var newViewProto = newView.proto ? newView.proto() : newView; |
|
|
|
if (fn) { |
|
Ember.assert("You cannot provide a template block if you also specified a templateName", !get(viewOptions, 'templateName') && !get(newViewProto, 'templateName')); |
|
viewOptions.template = fn; |
|
} |
|
|
|
// We only want to override the `_context` computed property if there is |
|
// no specified controller. See View#_context for more information. |
|
if (!newViewProto.controller && !newViewProto.controllerBinding && !viewOptions.controller && !viewOptions.controllerBinding) { |
|
viewOptions._context = thisContext; |
|
} |
|
|
|
currentView.appendChild(newView, viewOptions); |
|
} |
|
}); |
|
|
|
/** |
|
`{{view}}` inserts a new instance of `Ember.View` into a template passing its |
|
options to the `Ember.View`'s `create` method and using the supplied block as |
|
the view's own template. |
|
|
|
An empty `<body>` and the following template: |
|
|
|
```handlebars |
|
A span: |
|
{{#view tagName="span"}} |
|
hello. |
|
{{/view}} |
|
``` |
|
|
|
Will result in HTML structure: |
|
|
|
```html |
|
<body> |
|
<!-- Note: the handlebars template script |
|
also results in a rendered Ember.View |
|
which is the outer <div> here --> |
|
|
|
<div class="ember-view"> |
|
A span: |
|
<span id="ember1" class="ember-view"> |
|
Hello. |
|
</span> |
|
</div> |
|
</body> |
|
``` |
|
|
|
### `parentView` setting |
|
|
|
The `parentView` property of the new `Ember.View` instance created through |
|
`{{view}}` will be set to the `Ember.View` instance of the template where |
|
`{{view}}` was called. |
|
|
|
```javascript |
|
aView = Ember.View.create({ |
|
template: Ember.Handlebars.compile("{{#view}} my parent: {{parentView.elementId}} {{/view}}") |
|
}); |
|
|
|
aView.appendTo('body'); |
|
``` |
|
|
|
Will result in HTML structure: |
|
|
|
```html |
|
<div id="ember1" class="ember-view"> |
|
<div id="ember2" class="ember-view"> |
|
my parent: ember1 |
|
</div> |
|
</div> |
|
``` |
|
|
|
### Setting CSS id and class attributes |
|
|
|
The HTML `id` attribute can be set on the `{{view}}`'s resulting element with |
|
the `id` option. This option will _not_ be passed to `Ember.View.create`. |
|
|
|
```handlebars |
|
{{#view tagName="span" id="a-custom-id"}} |
|
hello. |
|
{{/view}} |
|
``` |
|
|
|
Results in the following HTML structure: |
|
|
|
```html |
|
<div class="ember-view"> |
|
<span id="a-custom-id" class="ember-view"> |
|
hello. |
|
</span> |
|
</div> |
|
``` |
|
|
|
The HTML `class` attribute can be set on the `{{view}}`'s resulting element |
|
with the `class` or `classNameBindings` options. The `class` option will |
|
directly set the CSS `class` attribute and will not be passed to |
|
`Ember.View.create`. `classNameBindings` will be passed to `create` and use |
|
`Ember.View`'s class name binding functionality: |
|
|
|
```handlebars |
|
{{#view tagName="span" class="a-custom-class"}} |
|
hello. |
|
{{/view}} |
|
``` |
|
|
|
Results in the following HTML structure: |
|
|
|
```html |
|
<div class="ember-view"> |
|
<span id="ember2" class="ember-view a-custom-class"> |
|
hello. |
|
</span> |
|
</div> |
|
``` |
|
|
|
### Supplying a different view class |
|
|
|
`{{view}}` can take an optional first argument before its supplied options to |
|
specify a path to a custom view class. |
|
|
|
```handlebars |
|
{{#view "MyApp.CustomView"}} |
|
hello. |
|
{{/view}} |
|
``` |
|
|
|
The first argument can also be a relative path accessible from the current |
|
context. |
|
|
|
```javascript |
|
MyApp = Ember.Application.create({}); |
|
MyApp.OuterView = Ember.View.extend({ |
|
innerViewClass: Ember.View.extend({ |
|
classNames: ['a-custom-view-class-as-property'] |
|
}), |
|
template: Ember.Handlebars.compile('{{#view "view.innerViewClass"}} hi {{/view}}') |
|
}); |
|
|
|
MyApp.OuterView.create().appendTo('body'); |
|
``` |
|
|
|
Will result in the following HTML: |
|
|
|
```html |
|
<div id="ember1" class="ember-view"> |
|
<div id="ember2" class="ember-view a-custom-view-class-as-property"> |
|
hi |
|
</div> |
|
</div> |
|
``` |
|
|
|
### Blockless use |
|
|
|
If you supply a custom `Ember.View` subclass that specifies its own template |
|
or provide a `templateName` option to `{{view}}` it can be used without |
|
supplying a block. Attempts to use both a `templateName` option and supply a |
|
block will throw an error. |
|
|
|
```handlebars |
|
{{view "MyApp.ViewWithATemplateDefined"}} |
|
``` |
|
|
|
### `viewName` property |
|
|
|
You can supply a `viewName` option to `{{view}}`. The `Ember.View` instance |
|
will be referenced as a property of its parent view by this name. |
|
|
|
```javascript |
|
aView = Ember.View.create({ |
|
template: Ember.Handlebars.compile('{{#view viewName="aChildByName"}} hi {{/view}}') |
|
}); |
|
|
|
aView.appendTo('body'); |
|
aView.get('aChildByName') // the instance of Ember.View created by {{view}} helper |
|
``` |
|
|
|
@method view |
|
@for Ember.Handlebars.helpers |
|
@param {String} path |
|
@param {Hash} options |
|
@return {String} HTML string |
|
*/ |
|
EmberHandlebars.registerHelper('view', function viewHelper(path, options) { |
|
Ember.assert("The view helper only takes a single argument", arguments.length <= 2); |
|
|
|
// If no path is provided, treat path param as options. |
|
if (path && path.data && path.data.isRenderData) { |
|
options = path; |
|
path = "Ember.View"; |
|
} |
|
|
|
return EmberHandlebars.ViewHelper.helper(this, path, options); |
|
}); |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
// TODO: Don't require all of this module |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var get = Ember.get, handlebarsGet = Ember.Handlebars.get, fmt = Ember.String.fmt; |
|
|
|
/** |
|
`{{collection}}` is a `Ember.Handlebars` helper for adding instances of |
|
`Ember.CollectionView` to a template. See [Ember.CollectionView](/api/classes/Ember.CollectionView.html) |
|
for additional information on how a `CollectionView` functions. |
|
|
|
`{{collection}}`'s primary use is as a block helper with a `contentBinding` |
|
option pointing towards an `Ember.Array`-compatible object. An `Ember.View` |
|
instance will be created for each item in its `content` property. Each view |
|
will have its own `content` property set to the appropriate item in the |
|
collection. |
|
|
|
The provided block will be applied as the template for each item's view. |
|
|
|
Given an empty `<body>` the following template: |
|
|
|
```handlebars |
|
{{#collection contentBinding="App.items"}} |
|
Hi {{view.content.name}} |
|
{{/collection}} |
|
``` |
|
|
|
And the following application code |
|
|
|
```javascript |
|
App = Ember.Application.create() |
|
App.items = [ |
|
Ember.Object.create({name: 'Dave'}), |
|
Ember.Object.create({name: 'Mary'}), |
|
Ember.Object.create({name: 'Sara'}) |
|
] |
|
``` |
|
|
|
Will result in the HTML structure below |
|
|
|
```html |
|
<div class="ember-view"> |
|
<div class="ember-view">Hi Dave</div> |
|
<div class="ember-view">Hi Mary</div> |
|
<div class="ember-view">Hi Sara</div> |
|
</div> |
|
``` |
|
|
|
### Blockless use in a collection |
|
|
|
If you provide an `itemViewClass` option that has its own `template` you can |
|
omit the block. |
|
|
|
The following template: |
|
|
|
```handlebars |
|
{{collection contentBinding="App.items" itemViewClass="App.AnItemView"}} |
|
``` |
|
|
|
And application code |
|
|
|
```javascript |
|
App = Ember.Application.create(); |
|
App.items = [ |
|
Ember.Object.create({name: 'Dave'}), |
|
Ember.Object.create({name: 'Mary'}), |
|
Ember.Object.create({name: 'Sara'}) |
|
]; |
|
|
|
App.AnItemView = Ember.View.extend({ |
|
template: Ember.Handlebars.compile("Greetings {{view.content.name}}") |
|
}); |
|
``` |
|
|
|
Will result in the HTML structure below |
|
|
|
```html |
|
<div class="ember-view"> |
|
<div class="ember-view">Greetings Dave</div> |
|
<div class="ember-view">Greetings Mary</div> |
|
<div class="ember-view">Greetings Sara</div> |
|
</div> |
|
``` |
|
|
|
### Specifying a CollectionView subclass |
|
|
|
By default the `{{collection}}` helper will create an instance of |
|
`Ember.CollectionView`. You can supply a `Ember.CollectionView` subclass to |
|
the helper by passing it as the first argument: |
|
|
|
```handlebars |
|
{{#collection App.MyCustomCollectionClass contentBinding="App.items"}} |
|
Hi {{view.content.name}} |
|
{{/collection}} |
|
``` |
|
|
|
### Forwarded `item.*`-named Options |
|
|
|
As with the `{{view}}`, helper options passed to the `{{collection}}` will be |
|
set on the resulting `Ember.CollectionView` as properties. Additionally, |
|
options prefixed with `item` will be applied to the views rendered for each |
|
item (note the camelcasing): |
|
|
|
```handlebars |
|
{{#collection contentBinding="App.items" |
|
itemTagName="p" |
|
itemClassNames="greeting"}} |
|
Howdy {{view.content.name}} |
|
{{/collection}} |
|
``` |
|
|
|
Will result in the following HTML structure: |
|
|
|
```html |
|
<div class="ember-view"> |
|
<p class="ember-view greeting">Howdy Dave</p> |
|
<p class="ember-view greeting">Howdy Mary</p> |
|
<p class="ember-view greeting">Howdy Sara</p> |
|
</div> |
|
``` |
|
|
|
@method collection |
|
@for Ember.Handlebars.helpers |
|
@param {String} path |
|
@param {Hash} options |
|
@return {String} HTML string |
|
@deprecated Use `{{each}}` helper instead. |
|
*/ |
|
Ember.Handlebars.registerHelper('collection', function collectionHelper(path, options) { |
|
Ember.deprecate("Using the {{collection}} helper without specifying a class has been deprecated as the {{each}} helper now supports the same functionality.", path !== 'collection'); |
|
|
|
// If no path is provided, treat path param as options. |
|
if (path && path.data && path.data.isRenderData) { |
|
options = path; |
|
path = undefined; |
|
Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 1); |
|
} else { |
|
Ember.assert("You cannot pass more than one argument to the collection helper", arguments.length === 2); |
|
} |
|
|
|
var fn = options.fn; |
|
var data = options.data; |
|
var inverse = options.inverse; |
|
var view = options.data.view; |
|
|
|
|
|
var controller, container; |
|
// If passed a path string, convert that into an object. |
|
// Otherwise, just default to the standard class. |
|
var collectionClass; |
|
if (path) { |
|
controller = data.keywords.controller; |
|
container = controller && controller.container; |
|
collectionClass = handlebarsGet(this, path, options) || container.lookupFactory('view:' + path); |
|
Ember.assert(fmt("%@ #collection: Could not find collection class %@", [data.view, path]), !!collectionClass); |
|
} |
|
else { |
|
collectionClass = Ember.CollectionView; |
|
} |
|
|
|
var hash = options.hash, itemHash = {}, match; |
|
|
|
// Extract item view class if provided else default to the standard class |
|
var collectionPrototype = collectionClass.proto(), |
|
itemViewClass; |
|
|
|
if (hash.itemView) { |
|
controller = data.keywords.controller; |
|
Ember.assert('You specified an itemView, but the current context has no ' + |
|
'container to look the itemView up in. This probably means ' + |
|
'that you created a view manually, instead of through the ' + |
|
'container. Instead, use container.lookup("view:viewName"), ' + |
|
'which will properly instantiate your view.', |
|
controller && controller.container); |
|
container = controller.container; |
|
itemViewClass = container.lookupFactory('view:' + hash.itemView); |
|
Ember.assert('You specified the itemView ' + hash.itemView + ", but it was " + |
|
"not found at " + container.describe("view:" + hash.itemView) + |
|
" (and it was not registered in the container)", !!itemViewClass); |
|
} else if (hash.itemViewClass) { |
|
itemViewClass = handlebarsGet(collectionPrototype, hash.itemViewClass, options); |
|
} else { |
|
itemViewClass = collectionPrototype.itemViewClass; |
|
} |
|
|
|
Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [data.view, itemViewClass]), !!itemViewClass); |
|
|
|
delete hash.itemViewClass; |
|
delete hash.itemView; |
|
|
|
// Go through options passed to the {{collection}} helper and extract options |
|
// that configure item views instead of the collection itself. |
|
for (var prop in hash) { |
|
if (hash.hasOwnProperty(prop)) { |
|
match = prop.match(/^item(.)(.*)$/); |
|
|
|
if (match && prop !== 'itemController') { |
|
// Convert itemShouldFoo -> shouldFoo |
|
itemHash[match[1].toLowerCase() + match[2]] = hash[prop]; |
|
// Delete from hash as this will end up getting passed to the |
|
// {{view}} helper method. |
|
delete hash[prop]; |
|
} |
|
} |
|
} |
|
|
|
if (fn) { |
|
itemHash.template = fn; |
|
delete options.fn; |
|
} |
|
|
|
var emptyViewClass; |
|
if (inverse && inverse !== Ember.Handlebars.VM.noop) { |
|
emptyViewClass = get(collectionPrototype, 'emptyViewClass'); |
|
emptyViewClass = emptyViewClass.extend({ |
|
template: inverse, |
|
tagName: itemHash.tagName |
|
}); |
|
} else if (hash.emptyViewClass) { |
|
emptyViewClass = handlebarsGet(this, hash.emptyViewClass, options); |
|
} |
|
if (emptyViewClass) { hash.emptyView = emptyViewClass; } |
|
|
|
if (!hash.keyword) { |
|
itemHash._context = Ember.computed.alias('content'); |
|
} |
|
|
|
var viewOptions = Ember.Handlebars.ViewHelper.propertiesFromHTMLOptions({ data: data, hash: itemHash }, this); |
|
hash.itemViewClass = itemViewClass.extend(viewOptions); |
|
|
|
return Ember.Handlebars.helpers.view.call(this, collectionClass, options); |
|
}); |
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/*globals Handlebars */ |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var handlebarsGet = Ember.Handlebars.get; |
|
|
|
/** |
|
`unbound` allows you to output a property without binding. *Important:* The |
|
output will not be updated if the property changes. Use with caution. |
|
|
|
```handlebars |
|
<div>{{unbound somePropertyThatDoesntChange}}</div> |
|
``` |
|
|
|
`unbound` can also be used in conjunction with a bound helper to |
|
render it in its unbound form: |
|
|
|
```handlebars |
|
<div>{{unbound helperName somePropertyThatDoesntChange}}</div> |
|
``` |
|
|
|
@method unbound |
|
@for Ember.Handlebars.helpers |
|
@param {String} property |
|
@return {String} HTML string |
|
*/ |
|
Ember.Handlebars.registerHelper('unbound', function unboundHelper(property, fn) { |
|
var options = arguments[arguments.length - 1], helper, context, out; |
|
|
|
if (arguments.length > 2) { |
|
// Unbound helper call. |
|
options.data.isUnbound = true; |
|
helper = Ember.Handlebars.helpers[arguments[0]] || Ember.Handlebars.helpers.helperMissing; |
|
out = helper.apply(this, Array.prototype.slice.call(arguments, 1)); |
|
delete options.data.isUnbound; |
|
return out; |
|
} |
|
|
|
context = (fn.contexts && fn.contexts.length) ? fn.contexts[0] : this; |
|
return handlebarsGet(context, property, fn); |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/*jshint debug:true*/ |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var get = Ember.get; |
|
var handlebarsGet = Ember.Handlebars.get, normalizePath = Ember.Handlebars.normalizePath; |
|
var a_slice = [].slice; |
|
|
|
/** |
|
`log` allows you to output the value of variables in the current rendering |
|
context. `log` also accepts primitive types such as strings or numbers. |
|
|
|
```handlebars |
|
{{log "myVariable:" myVariable }} |
|
``` |
|
|
|
@method log |
|
@for Ember.Handlebars.helpers |
|
@param {String} property |
|
*/ |
|
Ember.Handlebars.registerHelper('log', function logHelper() { |
|
var params = a_slice.call(arguments, 0, -1), |
|
options = arguments[arguments.length - 1], |
|
logger = Ember.Logger.log, |
|
values = [], |
|
allowPrimitives = false; |
|
|
|
|
|
allowPrimitives = true; |
|
|
|
|
|
for (var i = 0; i < params.length; i++) { |
|
var type = options.types[i]; |
|
|
|
if (type === 'ID' || !allowPrimitives) { |
|
var context = (options.contexts && options.contexts[i]) || this, |
|
normalized = normalizePath(context, params[i], options.data); |
|
|
|
if (normalized.path === 'this') { |
|
values.push(normalized.root); |
|
} else { |
|
values.push(handlebarsGet(normalized.root, normalized.path, options)); |
|
} |
|
} else { |
|
values.push(params[i]); |
|
} |
|
} |
|
|
|
logger.apply(logger, values); |
|
}); |
|
|
|
/** |
|
Execute the `debugger` statement in the current context. |
|
|
|
```handlebars |
|
{{debugger}} |
|
``` |
|
|
|
Before invoking the `debugger` statement, there |
|
are a few helpful variables defined in the |
|
body of this helper that you can inspect while |
|
debugging that describe how and where this |
|
helper was invoked: |
|
|
|
- templateContext: this is most likely a controller |
|
from which this template looks up / displays properties |
|
- typeOfTemplateContext: a string description of |
|
what the templateContext is |
|
|
|
For example, if you're wondering why a value `{{foo}}` |
|
isn't rendering as expected within a template, you |
|
could place a `{{debugger}}` statement, and when |
|
the `debugger;` breakpoint is hit, you can inspect |
|
`templateContext`, determine if it's the object you |
|
expect, and/or evaluate expressions in the console |
|
to perform property lookups on the `templateContext`: |
|
|
|
``` |
|
> templateContext.get('foo') // -> "<value of {{foo}}>" |
|
``` |
|
|
|
@method debugger |
|
@for Ember.Handlebars.helpers |
|
@param {String} property |
|
*/ |
|
Ember.Handlebars.registerHelper('debugger', function debuggerHelper(options) { |
|
|
|
// These are helpful values you can inspect while debugging. |
|
var templateContext = this; |
|
var typeOfTemplateContext = Ember.inspect(templateContext); |
|
|
|
debugger; |
|
}); |
|
|
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
var fmt = Ember.String.fmt; |
|
|
|
Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, { |
|
init: function() { |
|
var itemController = get(this, 'itemController'); |
|
var binding; |
|
|
|
if (itemController) { |
|
var controller = get(this, 'controller.container').lookupFactory('controller:array').create({ |
|
_isVirtual: true, |
|
parentController: get(this, 'controller'), |
|
itemController: itemController, |
|
target: get(this, 'controller'), |
|
_eachView: this |
|
}); |
|
|
|
this.disableContentObservers(function() { |
|
set(this, 'content', controller); |
|
binding = new Ember.Binding('content', '_eachView.dataSource').oneWay(); |
|
binding.connect(controller); |
|
}); |
|
|
|
set(this, '_arrayController', controller); |
|
} else { |
|
this.disableContentObservers(function() { |
|
binding = new Ember.Binding('content', 'dataSource').oneWay(); |
|
binding.connect(this); |
|
}); |
|
} |
|
|
|
return this._super(); |
|
}, |
|
|
|
_assertArrayLike: function(content) { |
|
Ember.assert(fmt("The value that #each loops over must be an Array. You " + |
|
"passed %@, but it should have been an ArrayController", |
|
[content.constructor]), |
|
!Ember.ControllerMixin.detect(content) || |
|
(content && content.isGenerated) || |
|
content instanceof Ember.ArrayController); |
|
Ember.assert(fmt("The value that #each loops over must be an Array. You passed %@", [(Ember.ControllerMixin.detect(content) && content.get('model') !== undefined) ? fmt("'%@' (wrapped in %@)", [content.get('model'), content]) : content]), Ember.Array.detect(content)); |
|
}, |
|
|
|
disableContentObservers: function(callback) { |
|
Ember.removeBeforeObserver(this, 'content', null, '_contentWillChange'); |
|
Ember.removeObserver(this, 'content', null, '_contentDidChange'); |
|
|
|
callback.call(this); |
|
|
|
Ember.addBeforeObserver(this, 'content', null, '_contentWillChange'); |
|
Ember.addObserver(this, 'content', null, '_contentDidChange'); |
|
}, |
|
|
|
itemViewClass: Ember._MetamorphView, |
|
emptyViewClass: Ember._MetamorphView, |
|
|
|
createChildView: function(view, attrs) { |
|
view = this._super(view, attrs); |
|
|
|
// At the moment, if a container view subclass wants |
|
// to insert keywords, it is responsible for cloning |
|
// the keywords hash. This will be fixed momentarily. |
|
var keyword = get(this, 'keyword'); |
|
var content = get(view, 'content'); |
|
|
|
if (keyword) { |
|
var data = get(view, 'templateData'); |
|
|
|
data = Ember.copy(data); |
|
data.keywords = view.cloneKeywords(); |
|
set(view, 'templateData', data); |
|
|
|
// In this case, we do not bind, because the `content` of |
|
// a #each item cannot change. |
|
data.keywords[keyword] = content; |
|
} |
|
|
|
// If {{#each}} is looping over an array of controllers, |
|
// point each child view at their respective controller. |
|
if (content && content.isController) { |
|
set(view, 'controller', content); |
|
} |
|
|
|
return view; |
|
}, |
|
|
|
destroy: function() { |
|
if (!this._super()) { return; } |
|
|
|
var arrayController = get(this, '_arrayController'); |
|
|
|
if (arrayController) { |
|
arrayController.destroy(); |
|
} |
|
|
|
return this; |
|
} |
|
}); |
|
|
|
// Defeatureify doesn't seem to like nested functions that need to be removed |
|
function _addMetamorphCheck() { |
|
Ember.Handlebars.EachView.reopen({ |
|
_checkMetamorph: Ember.on('didInsertElement', function() { |
|
Ember.assert("The metamorph tags, " + |
|
this.morph.start + " and " + this.morph.end + |
|
", have different parents.\nThe browser has fixed your template to output valid HTML (for example, check that you have properly closed all tags and have used a TBODY tag when creating a table with '{{#each}}')", |
|
document.getElementById( this.morph.start ).parentNode === |
|
document.getElementById( this.morph.end ).parentNode |
|
); |
|
}) |
|
}); |
|
} |
|
|
|
Ember.runInDebug( function() { |
|
_addMetamorphCheck(); |
|
}); |
|
|
|
var GroupedEach = Ember.Handlebars.GroupedEach = function(context, path, options) { |
|
var self = this, |
|
normalized = Ember.Handlebars.normalizePath(context, path, options.data); |
|
|
|
this.context = context; |
|
this.path = path; |
|
this.options = options; |
|
this.template = options.fn; |
|
this.containingView = options.data.view; |
|
this.normalizedRoot = normalized.root; |
|
this.normalizedPath = normalized.path; |
|
this.content = this.lookupContent(); |
|
|
|
this.addContentObservers(); |
|
this.addArrayObservers(); |
|
|
|
this.containingView.on('willClearRender', function() { |
|
self.destroy(); |
|
}); |
|
}; |
|
|
|
GroupedEach.prototype = { |
|
contentWillChange: function() { |
|
this.removeArrayObservers(); |
|
}, |
|
|
|
contentDidChange: function() { |
|
this.content = this.lookupContent(); |
|
this.addArrayObservers(); |
|
this.rerenderContainingView(); |
|
}, |
|
|
|
contentArrayWillChange: Ember.K, |
|
|
|
contentArrayDidChange: function() { |
|
this.rerenderContainingView(); |
|
}, |
|
|
|
lookupContent: function() { |
|
return Ember.Handlebars.get(this.normalizedRoot, this.normalizedPath, this.options); |
|
}, |
|
|
|
addArrayObservers: function() { |
|
if (!this.content) { return; } |
|
|
|
this.content.addArrayObserver(this, { |
|
willChange: 'contentArrayWillChange', |
|
didChange: 'contentArrayDidChange' |
|
}); |
|
}, |
|
|
|
removeArrayObservers: function() { |
|
if (!this.content) { return; } |
|
|
|
this.content.removeArrayObserver(this, { |
|
willChange: 'contentArrayWillChange', |
|
didChange: 'contentArrayDidChange' |
|
}); |
|
}, |
|
|
|
addContentObservers: function() { |
|
Ember.addBeforeObserver(this.normalizedRoot, this.normalizedPath, this, this.contentWillChange); |
|
Ember.addObserver(this.normalizedRoot, this.normalizedPath, this, this.contentDidChange); |
|
}, |
|
|
|
removeContentObservers: function() { |
|
Ember.removeBeforeObserver(this.normalizedRoot, this.normalizedPath, this.contentWillChange); |
|
Ember.removeObserver(this.normalizedRoot, this.normalizedPath, this.contentDidChange); |
|
}, |
|
|
|
render: function() { |
|
if (!this.content) { return; } |
|
|
|
var content = this.content, |
|
contentLength = get(content, 'length'), |
|
data = this.options.data, |
|
template = this.template; |
|
|
|
data.insideEach = true; |
|
for (var i = 0; i < contentLength; i++) { |
|
template(content.objectAt(i), { data: data }); |
|
} |
|
}, |
|
|
|
rerenderContainingView: function() { |
|
var self = this; |
|
Ember.run.scheduleOnce('render', this, function() { |
|
// It's possible it's been destroyed after we enqueued a re-render call. |
|
if (!self.destroyed) { |
|
self.containingView.rerender(); |
|
} |
|
}); |
|
}, |
|
|
|
destroy: function() { |
|
this.removeContentObservers(); |
|
if (this.content) { |
|
this.removeArrayObservers(); |
|
} |
|
this.destroyed = true; |
|
} |
|
}; |
|
|
|
/** |
|
The `{{#each}}` helper loops over elements in a collection, rendering its |
|
block once for each item. It is an extension of the base Handlebars `{{#each}}` |
|
helper: |
|
|
|
```javascript |
|
Developers = [{name: 'Yehuda'},{name: 'Tom'}, {name: 'Paul'}]; |
|
``` |
|
|
|
```handlebars |
|
{{#each Developers}} |
|
{{name}} |
|
{{/each}} |
|
``` |
|
|
|
`{{each}}` supports an alternative syntax with element naming: |
|
|
|
```handlebars |
|
{{#each person in Developers}} |
|
{{person.name}} |
|
{{/each}} |
|
``` |
|
|
|
When looping over objects that do not have properties, `{{this}}` can be used |
|
to render the object: |
|
|
|
```javascript |
|
DeveloperNames = ['Yehuda', 'Tom', 'Paul'] |
|
``` |
|
|
|
```handlebars |
|
{{#each DeveloperNames}} |
|
{{this}} |
|
{{/each}} |
|
``` |
|
### {{else}} condition |
|
`{{#each}}` can have a matching `{{else}}`. The contents of this block will render |
|
if the collection is empty. |
|
|
|
``` |
|
{{#each person in Developers}} |
|
{{person.name}} |
|
{{else}} |
|
<p>Sorry, nobody is available for this task.</p> |
|
{{/each}} |
|
``` |
|
### Specifying a View class for items |
|
If you provide an `itemViewClass` option that references a view class |
|
with its own `template` you can omit the block. |
|
|
|
The following template: |
|
|
|
```handlebars |
|
{{#view App.MyView }} |
|
{{each view.items itemViewClass="App.AnItemView"}} |
|
{{/view}} |
|
``` |
|
|
|
And application code |
|
|
|
```javascript |
|
App = Ember.Application.create({ |
|
MyView: Ember.View.extend({ |
|
items: [ |
|
Ember.Object.create({name: 'Dave'}), |
|
Ember.Object.create({name: 'Mary'}), |
|
Ember.Object.create({name: 'Sara'}) |
|
] |
|
}) |
|
}); |
|
|
|
App.AnItemView = Ember.View.extend({ |
|
template: Ember.Handlebars.compile("Greetings {{name}}") |
|
}); |
|
``` |
|
|
|
Will result in the HTML structure below |
|
|
|
```html |
|
<div class="ember-view"> |
|
<div class="ember-view">Greetings Dave</div> |
|
<div class="ember-view">Greetings Mary</div> |
|
<div class="ember-view">Greetings Sara</div> |
|
</div> |
|
``` |
|
|
|
If an `itemViewClass` is defined on the helper, and therefore the helper is not |
|
being used as a block, an `emptyViewClass` can also be provided optionally. |
|
The `emptyViewClass` will match the behavior of the `{{else}}` condition |
|
described above. That is, the `emptyViewClass` will render if the collection |
|
is empty. |
|
|
|
### Representing each item with a Controller. |
|
By default the controller lookup within an `{{#each}}` block will be |
|
the controller of the template where the `{{#each}}` was used. If each |
|
item needs to be presented by a custom controller you can provide a |
|
`itemController` option which references a controller by lookup name. |
|
Each item in the loop will be wrapped in an instance of this controller |
|
and the item itself will be set to the `content` property of that controller. |
|
|
|
This is useful in cases where properties of model objects need transformation |
|
or synthesis for display: |
|
|
|
```javascript |
|
App.DeveloperController = Ember.ObjectController.extend({ |
|
isAvailableForHire: function() { |
|
return !this.get('content.isEmployed') && this.get('content.isSeekingWork'); |
|
}.property('isEmployed', 'isSeekingWork') |
|
}) |
|
``` |
|
|
|
```handlebars |
|
{{#each person in developers itemController="developer"}} |
|
{{person.name}} {{#if person.isAvailableForHire}}Hire me!{{/if}} |
|
{{/each}} |
|
``` |
|
|
|
Each itemController will receive a reference to the current controller as |
|
a `parentController` property. |
|
|
|
### (Experimental) Grouped Each |
|
|
|
When used in conjunction with the experimental [group helper](https://github.com/emberjs/group-helper), |
|
you can inform Handlebars to re-render an entire group of items instead of |
|
re-rendering them one at a time (in the event that they are changed en masse |
|
or an item is added/removed). |
|
|
|
```handlebars |
|
{{#group}} |
|
{{#each people}} |
|
{{firstName}} {{lastName}} |
|
{{/each}} |
|
{{/group}} |
|
``` |
|
|
|
This can be faster than the normal way that Handlebars re-renders items |
|
in some cases. |
|
|
|
If for some reason you have a group with more than one `#each`, you can make |
|
one of the collections be updated in normal (non-grouped) fashion by setting |
|
the option `groupedRows=true` (counter-intuitive, I know). |
|
|
|
For example, |
|
|
|
```handlebars |
|
{{dealershipName}} |
|
|
|
{{#group}} |
|
{{#each dealers}} |
|
{{firstName}} {{lastName}} |
|
{{/each}} |
|
|
|
{{#each car in cars groupedRows=true}} |
|
{{car.make}} {{car.model}} {{car.color}} |
|
{{/each}} |
|
{{/group}} |
|
``` |
|
Any change to `dealershipName` or the `dealers` collection will cause the |
|
entire group to be re-rendered. However, changes to the `cars` collection |
|
will be re-rendered individually (as normal). |
|
|
|
Note that `group` behavior is also disabled by specifying an `itemViewClass`. |
|
|
|
@method each |
|
@for Ember.Handlebars.helpers |
|
@param [name] {String} name for item (used with `in`) |
|
@param [path] {String} path |
|
@param [options] {Object} Handlebars key/value pairs of options |
|
@param [options.itemViewClass] {String} a path to a view class used for each item |
|
@param [options.itemController] {String} name of a controller to be created for each item |
|
@param [options.groupedRows] {boolean} enable normal item-by-item rendering when inside a `#group` helper |
|
*/ |
|
Ember.Handlebars.registerHelper('each', function eachHelper(path, options) { |
|
if (arguments.length === 4) { |
|
Ember.assert("If you pass more than one argument to the each helper, it must be in the form #each foo in bar", arguments[1] === "in"); |
|
|
|
var keywordName = arguments[0]; |
|
|
|
options = arguments[3]; |
|
path = arguments[2]; |
|
if (path === '') { path = "this"; } |
|
|
|
options.hash.keyword = keywordName; |
|
} |
|
|
|
if (arguments.length === 1) { |
|
options = path; |
|
path = 'this'; |
|
} |
|
|
|
options.hash.dataSourceBinding = path; |
|
// Set up emptyView as a metamorph with no tag |
|
//options.hash.emptyViewClass = Ember._MetamorphView; |
|
|
|
if (options.data.insideGroup && !options.hash.groupedRows && !options.hash.itemViewClass) { |
|
new Ember.Handlebars.GroupedEach(this, path, options).render(); |
|
} else { |
|
return Ember.Handlebars.helpers.collection.call(this, 'Ember.Handlebars.EachView', options); |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
/** |
|
`template` allows you to render a template from inside another template. |
|
This allows you to re-use the same template in multiple places. For example: |
|
|
|
```html |
|
<script type="text/x-handlebars" data-template-name="logged_in_user"> |
|
{{#with loggedInUser}} |
|
Last Login: {{lastLogin}} |
|
User Info: {{template "user_info"}} |
|
{{/with}} |
|
</script> |
|
``` |
|
|
|
```html |
|
<script type="text/x-handlebars" data-template-name="user_info"> |
|
Name: <em>{{name}}</em> |
|
Karma: <em>{{karma}}</em> |
|
</script> |
|
``` |
|
|
|
```handlebars |
|
{{#if isUser}} |
|
{{template "user_info"}} |
|
{{else}} |
|
{{template "unlogged_user_info"}} |
|
{{/if}} |
|
``` |
|
|
|
This helper looks for templates in the global `Ember.TEMPLATES` hash. If you |
|
add `<script>` tags to your page with the `data-template-name` attribute set, |
|
they will be compiled and placed in this hash automatically. |
|
|
|
You can also manually register templates by adding them to the hash: |
|
|
|
```javascript |
|
Ember.TEMPLATES["my_cool_template"] = Ember.Handlebars.compile('<b>{{user}}</b>'); |
|
``` |
|
|
|
@deprecated |
|
@method template |
|
@for Ember.Handlebars.helpers |
|
@param {String} templateName the template to render |
|
*/ |
|
|
|
Ember.Handlebars.registerHelper('template', function(name, options) { |
|
Ember.deprecate("The `template` helper has been deprecated in favor of the `partial` helper. Please use `partial` instead, which will work the same way."); |
|
return Ember.Handlebars.helpers.partial.apply(this, arguments); |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
/** |
|
The `partial` helper renders another template without |
|
changing the template context: |
|
|
|
```handlebars |
|
{{foo}} |
|
{{partial "nav"}} |
|
``` |
|
|
|
The above example template will render a template named |
|
"_nav", which has the same context as the parent template |
|
it's rendered into, so if the "_nav" template also referenced |
|
`{{foo}}`, it would print the same thing as the `{{foo}}` |
|
in the above example. |
|
|
|
If a "_nav" template isn't found, the `partial` helper will |
|
fall back to a template named "nav". |
|
|
|
## Bound template names |
|
|
|
The parameter supplied to `partial` can also be a path |
|
to a property containing a template name, e.g.: |
|
|
|
```handlebars |
|
{{partial someTemplateName}} |
|
``` |
|
|
|
The above example will look up the value of `someTemplateName` |
|
on the template context (e.g. a controller) and use that |
|
value as the name of the template to render. If the resolved |
|
value is falsy, nothing will be rendered. If `someTemplateName` |
|
changes, the partial will be re-rendered using the new template |
|
name. |
|
|
|
## Setting the partial's context with `with` |
|
|
|
The `partial` helper can be used in conjunction with the `with` |
|
helper to set a context that will be used by the partial: |
|
|
|
```handlebars |
|
{{#with currentUser}} |
|
{{partial "user_info"}} |
|
{{/with}} |
|
``` |
|
|
|
@method partial |
|
@for Ember.Handlebars.helpers |
|
@param {String} partialName the name of the template to render minus the leading underscore |
|
*/ |
|
|
|
Ember.Handlebars.registerHelper('partial', function partialHelper(name, options) { |
|
|
|
var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this; |
|
|
|
if (options.types[0] === "ID") { |
|
// Helper was passed a property path; we need to |
|
// create a binding that will re-render whenever |
|
// this property changes. |
|
options.fn = function(context, fnOptions) { |
|
var partialName = Ember.Handlebars.get(context, name, fnOptions); |
|
renderPartial(context, partialName, fnOptions); |
|
}; |
|
|
|
return Ember.Handlebars.bind.call(context, name, options, true, exists); |
|
} else { |
|
// Render the partial right into parent template. |
|
renderPartial(context, name, options); |
|
} |
|
}); |
|
|
|
function exists(value) { |
|
return !Ember.isNone(value); |
|
} |
|
|
|
function renderPartial(context, name, options) { |
|
var nameParts = name.split("/"), |
|
lastPart = nameParts[nameParts.length - 1]; |
|
|
|
nameParts[nameParts.length - 1] = "_" + lastPart; |
|
|
|
var view = options.data.view, |
|
underscoredName = nameParts.join("/"), |
|
template = view.templateForName(underscoredName), |
|
deprecatedTemplate = !template && view.templateForName(name); |
|
|
|
Ember.assert("Unable to find partial with name '"+name+"'.", template || deprecatedTemplate); |
|
|
|
template = template || deprecatedTemplate; |
|
|
|
template(context, { data: options.data }); |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
/** |
|
`{{yield}}` denotes an area of a template that will be rendered inside |
|
of another template. It has two main uses: |
|
|
|
### Use with `layout` |
|
When used in a Handlebars template that is assigned to an `Ember.View` |
|
instance's `layout` property Ember will render the layout template first, |
|
inserting the view's own rendered output at the `{{yield}}` location. |
|
|
|
An empty `<body>` and the following application code: |
|
|
|
```javascript |
|
AView = Ember.View.extend({ |
|
classNames: ['a-view-with-layout'], |
|
layout: Ember.Handlebars.compile('<div class="wrapper">{{yield}}</div>'), |
|
template: Ember.Handlebars.compile('<span>I am wrapped</span>') |
|
}); |
|
|
|
aView = AView.create(); |
|
aView.appendTo('body'); |
|
``` |
|
|
|
Will result in the following HTML output: |
|
|
|
```html |
|
<body> |
|
<div class='ember-view a-view-with-layout'> |
|
<div class="wrapper"> |
|
<span>I am wrapped</span> |
|
</div> |
|
</div> |
|
</body> |
|
``` |
|
|
|
The `yield` helper cannot be used outside of a template assigned to an |
|
`Ember.View`'s `layout` property and will throw an error if attempted. |
|
|
|
```javascript |
|
BView = Ember.View.extend({ |
|
classNames: ['a-view-with-layout'], |
|
template: Ember.Handlebars.compile('{{yield}}') |
|
}); |
|
|
|
bView = BView.create(); |
|
bView.appendTo('body'); |
|
|
|
// throws |
|
// Uncaught Error: assertion failed: |
|
// You called yield in a template that was not a layout |
|
``` |
|
|
|
### Use with Ember.Component |
|
When designing components `{{yield}}` is used to denote where, inside the component's |
|
template, an optional block passed to the component should render: |
|
|
|
```handlebars |
|
<!-- application.hbs --> |
|
{{#labeled-textfield value=someProperty}} |
|
First name: |
|
{{/labeled-textfield}} |
|
``` |
|
|
|
```handlebars |
|
<!-- components/labeled-textfield.hbs --> |
|
<label> |
|
{{yield}} {{input value=value}} |
|
</label> |
|
``` |
|
|
|
Result: |
|
|
|
```html |
|
<label> |
|
First name: <input type="text" /> |
|
<label> |
|
``` |
|
|
|
@method yield |
|
@for Ember.Handlebars.helpers |
|
@param {Hash} options |
|
@return {String} HTML string |
|
*/ |
|
Ember.Handlebars.registerHelper('yield', function yieldHelper(options) { |
|
var view = options.data.view; |
|
|
|
while (view && !get(view, 'layout')) { |
|
if (view._contextView) { |
|
view = view._contextView; |
|
} else { |
|
view = get(view, 'parentView'); |
|
} |
|
} |
|
|
|
Ember.assert("You called yield in a template that was not a layout", !!view); |
|
|
|
view._yield(this, options); |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
/** |
|
`loc` looks up the string in the localized strings hash. |
|
This is a convenient way to localize text. For example: |
|
|
|
```html |
|
<script type="text/x-handlebars" data-template-name="home"> |
|
{{loc "welcome"}} |
|
</script> |
|
``` |
|
|
|
Take note that `"welcome"` is a string and not an object |
|
reference. |
|
|
|
@method loc |
|
@for Ember.Handlebars.helpers |
|
@param {String} str The string to format |
|
*/ |
|
|
|
Ember.Handlebars.registerHelper('loc', function locHelper(str) { |
|
return Ember.String.loc(str); |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var set = Ember.set, get = Ember.get; |
|
|
|
/** |
|
The internal class used to create text inputs when the `{{input}}` |
|
helper is used with `type` of `checkbox`. |
|
|
|
See [handlebars.helpers.input](/api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. |
|
|
|
## Direct manipulation of `checked` |
|
|
|
The `checked` attribute of an `Ember.Checkbox` object should always be set |
|
through the Ember object or by interacting with its rendered element |
|
representation via the mouse, keyboard, or touch. Updating the value of the |
|
checkbox via jQuery will result in the checked value of the object and its |
|
element losing synchronization. |
|
|
|
## Layout and LayoutName properties |
|
|
|
Because HTML `input` elements are self closing `layout` and `layoutName` |
|
properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s |
|
layout section for more information. |
|
|
|
@class Checkbox |
|
@namespace Ember |
|
@extends Ember.View |
|
*/ |
|
Ember.Checkbox = Ember.View.extend({ |
|
classNames: ['ember-checkbox'], |
|
|
|
tagName: 'input', |
|
|
|
attributeBindings: ['type', 'checked', 'indeterminate', 'disabled', 'tabindex', 'name', |
|
'autofocus', 'form'], |
|
|
|
type: "checkbox", |
|
checked: false, |
|
disabled: false, |
|
indeterminate: false, |
|
|
|
init: function() { |
|
this._super(); |
|
this.on("change", this, this._updateElementValue); |
|
}, |
|
|
|
didInsertElement: function() { |
|
this._super(); |
|
this.get('element').indeterminate = !!this.get('indeterminate'); |
|
}, |
|
|
|
_updateElementValue: function() { |
|
set(this, 'checked', this.$().prop('checked')); |
|
} |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
/** |
|
Shared mixin used by `Ember.TextField` and `Ember.TextArea`. |
|
|
|
@class TextSupport |
|
@namespace Ember |
|
@uses Ember.TargetActionSupport |
|
@extends Ember.Mixin |
|
@private |
|
*/ |
|
Ember.TextSupport = Ember.Mixin.create(Ember.TargetActionSupport, { |
|
value: "", |
|
|
|
attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex', 'readonly', |
|
'autofocus', 'form', 'selectionDirection', 'spellcheck', 'required'], |
|
placeholder: null, |
|
disabled: false, |
|
maxlength: null, |
|
|
|
init: function() { |
|
this._super(); |
|
this.on("focusOut", this, this._elementValueDidChange); |
|
this.on("change", this, this._elementValueDidChange); |
|
this.on("paste", this, this._elementValueDidChange); |
|
this.on("cut", this, this._elementValueDidChange); |
|
this.on("input", this, this._elementValueDidChange); |
|
this.on("keyUp", this, this.interpretKeyEvents); |
|
}, |
|
|
|
/** |
|
The action to be sent when the user presses the return key. |
|
|
|
This is similar to the `{{action}}` helper, but is fired when |
|
the user presses the return key when editing a text field, and sends |
|
the value of the field as the context. |
|
|
|
@property action |
|
@type String |
|
@default null |
|
*/ |
|
action: null, |
|
|
|
/** |
|
The event that should send the action. |
|
|
|
Options are: |
|
|
|
* `enter`: the user pressed enter |
|
* `keyPress`: the user pressed a key |
|
|
|
@property onEvent |
|
@type String |
|
@default enter |
|
*/ |
|
onEvent: 'enter', |
|
|
|
/** |
|
Whether they `keyUp` event that triggers an `action` to be sent continues |
|
propagating to other views. |
|
|
|
By default, when the user presses the return key on their keyboard and |
|
the text field has an `action` set, the action will be sent to the view's |
|
controller and the key event will stop propagating. |
|
|
|
If you would like parent views to receive the `keyUp` event even after an |
|
action has been dispatched, set `bubbles` to true. |
|
|
|
@property bubbles |
|
@type Boolean |
|
@default false |
|
*/ |
|
bubbles: false, |
|
|
|
interpretKeyEvents: function(event) { |
|
var map = Ember.TextSupport.KEY_EVENTS; |
|
var method = map[event.keyCode]; |
|
|
|
this._elementValueDidChange(); |
|
if (method) { return this[method](event); } |
|
}, |
|
|
|
_elementValueDidChange: function() { |
|
set(this, 'value', this.$().val()); |
|
}, |
|
|
|
/** |
|
The action to be sent when the user inserts a new line. |
|
|
|
Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 13. |
|
Uses sendAction to send the `enter` action to the controller. |
|
|
|
@method insertNewline |
|
@param {Event} event |
|
*/ |
|
insertNewline: function(event) { |
|
sendAction('enter', this, event); |
|
sendAction('insert-newline', this, event); |
|
}, |
|
|
|
/** |
|
Called when the user hits escape. |
|
|
|
Called by the `Ember.TextSupport` mixin on keyUp if keycode matches 27. |
|
Uses sendAction to send the `escape-press` action to the controller. |
|
|
|
@method cancel |
|
@param {Event} event |
|
*/ |
|
cancel: function(event) { |
|
sendAction('escape-press', this, event); |
|
}, |
|
|
|
/** |
|
Called when the text area is focused. |
|
|
|
@method focusIn |
|
@param {Event} event |
|
*/ |
|
focusIn: function(event) { |
|
sendAction('focus-in', this, event); |
|
}, |
|
|
|
/** |
|
Called when the text area is blurred. |
|
|
|
@method focusOut |
|
@param {Event} event |
|
*/ |
|
focusOut: function(event) { |
|
sendAction('focus-out', this, event); |
|
}, |
|
|
|
/** |
|
The action to be sent when the user presses a key. Enabled by setting |
|
the `onEvent` property to `keyPress`. |
|
|
|
Uses sendAction to send the `keyPress` action to the controller. |
|
|
|
@method keyPress |
|
@param {Event} event |
|
*/ |
|
keyPress: function(event) { |
|
sendAction('key-press', this, event); |
|
} |
|
|
|
}); |
|
|
|
Ember.TextSupport.KEY_EVENTS = { |
|
13: 'insertNewline', |
|
27: 'cancel' |
|
}; |
|
|
|
// In principle, this shouldn't be necessary, but the legacy |
|
// sectionAction semantics for TextField are different from |
|
// the component semantics so this method normalizes them. |
|
function sendAction(eventName, view, event) { |
|
var action = get(view, eventName), |
|
on = get(view, 'onEvent'), |
|
value = get(view, 'value'); |
|
|
|
// back-compat support for keyPress as an event name even though |
|
// it's also a method name that consumes the event (and therefore |
|
// incompatible with sendAction semantics). |
|
if (on === eventName || (on === 'keyPress' && eventName === 'key-press')) { |
|
view.sendAction('action', value); |
|
} |
|
|
|
view.sendAction(eventName, value); |
|
|
|
if (action || on === eventName) { |
|
if(!get(view, 'bubbles')) { |
|
event.stopPropagation(); |
|
} |
|
} |
|
} |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
/** |
|
|
|
The internal class used to create text inputs when the `{{input}}` |
|
helper is used with `type` of `text`. |
|
|
|
See [Handlebars.helpers.input](/api/classes/Ember.Handlebars.helpers.html#method_input) for usage details. |
|
|
|
## Layout and LayoutName properties |
|
|
|
Because HTML `input` elements are self closing `layout` and `layoutName` |
|
properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s |
|
layout section for more information. |
|
|
|
@class TextField |
|
@namespace Ember |
|
@extends Ember.Component |
|
@uses Ember.TextSupport |
|
*/ |
|
Ember.TextField = Ember.Component.extend(Ember.TextSupport, { |
|
|
|
classNames: ['ember-text-field'], |
|
tagName: "input", |
|
attributeBindings: ['type', 'value', 'size', 'pattern', 'name', 'min', 'max', |
|
'accept', 'autocomplete', 'autosave', 'formaction', |
|
'formenctype', 'formmethod', 'formnovalidate', 'formtarget', |
|
'height', 'inputmode', 'list', 'multiple', 'pattern', 'step', |
|
'width'], |
|
|
|
/** |
|
The `value` attribute of the input element. As the user inputs text, this |
|
property is updated live. |
|
|
|
@property value |
|
@type String |
|
@default "" |
|
*/ |
|
value: "", |
|
|
|
/** |
|
The `type` attribute of the input element. |
|
|
|
@property type |
|
@type String |
|
@default "text" |
|
*/ |
|
type: "text", |
|
|
|
/** |
|
The `size` of the text field in characters. |
|
|
|
@property size |
|
@type String |
|
@default null |
|
*/ |
|
size: null, |
|
|
|
/** |
|
The `pattern` attribute of input element. |
|
|
|
@property pattern |
|
@type String |
|
@default null |
|
*/ |
|
pattern: null, |
|
|
|
/** |
|
The `min` attribute of input element used with `type="number"` or `type="range"`. |
|
|
|
@property min |
|
@type String |
|
@default null |
|
*/ |
|
min: null, |
|
|
|
/** |
|
The `max` attribute of input element used with `type="number"` or `type="range"`. |
|
|
|
@property max |
|
@type String |
|
@default null |
|
*/ |
|
max: null |
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var get = Ember.get, set = Ember.set; |
|
|
|
/** |
|
The internal class used to create textarea element when the `{{textarea}}` |
|
helper is used. |
|
|
|
See [handlebars.helpers.textarea](/api/classes/Ember.Handlebars.helpers.html#method_textarea) for usage details. |
|
|
|
## Layout and LayoutName properties |
|
|
|
Because HTML `textarea` elements do not contain inner HTML the `layout` and |
|
`layoutName` properties will not be applied. See [Ember.View](/api/classes/Ember.View.html)'s |
|
layout section for more information. |
|
|
|
@class TextArea |
|
@namespace Ember |
|
@extends Ember.Component |
|
@uses Ember.TextSupport |
|
*/ |
|
Ember.TextArea = Ember.Component.extend(Ember.TextSupport, { |
|
classNames: ['ember-text-area'], |
|
|
|
tagName: "textarea", |
|
attributeBindings: ['rows', 'cols', 'name', 'selectionEnd', 'selectionStart', 'wrap'], |
|
rows: null, |
|
cols: null, |
|
|
|
_updateElementValue: Ember.observer('value', function() { |
|
// We do this check so cursor position doesn't get affected in IE |
|
var value = get(this, 'value'), |
|
$el = this.$(); |
|
if ($el && value !== $el.val()) { |
|
$el.val(value); |
|
} |
|
}), |
|
|
|
init: function() { |
|
this._super(); |
|
this.on("didInsertElement", this, this._updateElementValue); |
|
} |
|
|
|
}); |
|
|
|
})(); |
|
|
|
|
|
|
|
(function() { |
|
/*jshint eqeqeq:false */ |
|
|
|
/** |
|
@module ember |
|
@submodule ember-handlebars |
|
*/ |
|
|
|
var set = Ember.set, |
|
get = Ember.get, |
|
indexOf = Ember.EnumerableUtils.indexOf, |
|
indexesOf = Ember.EnumerableUtils.indexesOf, |
|
forEach = Ember.EnumerableUtils.forEach, |
|
replace = Ember.EnumerableUtils.replace, |
|
isArray = Ember.isArray, |
|
precompileTemplate = Ember.Handlebars.compile; |
|
|
|
Ember.SelectOption = Ember.View.extend({ |
|
tagName: 'option', |
|
attributeBindings: ['value', 'selected'], |
|
|
|
defaultTemplate: function(context, options) { |
|
options = { data: options.data, hash: {} }; |
|
Ember.Handlebars.helpers.bind.call(context, "view.label", options); |
|
}, |
|
|
|
init: function() { |
|
this.labelPathDidChange(); |
|
this.valuePathDidChange(); |
|
|
|
this._super(); |
|
}, |
|
|
|
selected: Ember.computed(function() { |
|
var content = get(this, 'content'), |
|
selection = get(this, 'parentView.selection'); |
|
if (get(this, 'parentView.multiple')) { |
|
return selection && indexOf(selection, content.valueOf()) > -1; |
|
} else { |
|
// Primitives get passed through bindings as objects... since |
|
// `new Number(4) !== 4`, we use `==` below |
|
return content == selection; |
|
} |
|
}).property('content', 'parentView.selection'), |
|
|
|
labelPathDidChange: Ember.observer('parentView.optionLabelPath', function() { |
|
var labelPath = get(this, 'parentView.optionLabelPath'); |
|
|
|
if (!labelPath) { return; } |
|
|
|
Ember.defineProperty(this, 'label', Ember.computed(function() { |
|
return get(this, labelPath); |
|
}).property(labelPath)); |
|
}), |
|
|
|
valuePathDidChange: Ember.observer('parentView.optionValuePath', function() { |
|
var valuePath = get(this, 'parentView.optionValuePath'); |
|
|
|
if (!valuePath) { return; } |
|
|
|
Ember.defineProperty(this, 'value', Ember.computed(function() { |
|
return get(this, valuePath); |
|
}).property(valuePath)); |
|
}) |
|
}); |
|
|
|
Ember.SelectOptgroup = Ember.CollectionView.extend({ |
|
tagName: 'optgroup', |
|
attributeBindings: ['label'], |
|
|
|
selectionBinding: 'parentView.selection', |
|
multipleBinding: 'parentView.multiple', |
|
optionLabelPathBinding: 'parentView.optionLabelPath', |
|
optionValuePathBinding: 'parentView.optionValuePath', |
|
|
|
itemViewClassBinding: 'parentView.optionView' |
|
}); |
|
|
|
/** |
|
The `Ember.Select` view class renders a |
|
[select](https://developer.mozilla.org/en/HTML/Element/select) HTML element, |
|
allowing the user to choose from a list of options. |
|
|
|
The text and `value` property of each `<option>` element within the |
|
`<select>` element are populated from the objects in the `Element.Select`'s |
|
`content` property. The underlying data object of the selected `<option>` is |
|
stored in the `Element.Select`'s `value` property. |
|
|
|
## The Content Property (array of strings) |
|
|
|
The simplest version of an `Ember.Select` takes an array of strings as its |
|
`content` property. The string will be used as both the `value` property and |
|
the inner text of each `<option>` element inside the rendered `<select>`. |
|
|
|
Example: |
|
|
|
```javascript |
|
App.ApplicationController = Ember.Controller.extend({ |
|
names: ["Yehuda", "Tom"] |
|
}); |
|
``` |
|
|
|
```handlebars |
|
{{view Ember.Select content=names}} |
|
``` |
|
|
|
Would result in the following HTML: |
|
|
|
```html |
|
<select class="ember-select"> |
|
<option value="Yehuda">Yehuda</option> |
|
<option value="Tom">Tom</option> |
|
</select> |
|
``` |
|
|
|
You can control which `<option>` is selected through the `Ember.Select`'s |
|
`value` property: |
|
|
|
```javascript |
|
App.ApplicationController = Ember.Controller.extend({ |
|
selectedName: 'Tom', |
|
names: ["Yehuda", "Tom"] |
|
}); |
|
``` |
|
|
|
```handlebars |
|
{{view Ember.Select |
|
content=names |
|
value=selectedName |
|
}} |
|
``` |
|
|
|
Would result in the following HTML with the `<option>` for 'Tom' selected: |
|
|
|
```html |
|
<select class="ember-select"> |
|
<option value="Yehuda">Yehuda</option> |
|
<option value="Tom" selected="selected">Tom</option> |
|
</select> |
|
``` |
|
|
|
A user interacting with the rendered `<select>` to choose "Yehuda" would |
|
update the value of `selectedName` to "Yehuda". |
|
|
|
## The Content Property (array of Objects) |
|
|
|
An `Ember.Select` can also take an array of JavaScript or Ember objects as |
|
its `content` property. |
|
|
|
When using objects you need to tell the `Ember.Select` which property should |
|
be accessed on each object to supply the `value` attribute of the `<option>` |
|
and which property should be used to supply the element text. |
|
|
|
The `optionValuePath` option is used to specify the path on each object to |
|
the desired property for the `value` attribute. The `optionLabelPath` |
|
specifies the path on each object to the desired property for the |
|
element's text. Both paths must reference each object itself as `content`: |
|
|
|
```javascript |
|
App.ApplicationController = Ember.Controller.extend({ |
|
programmers: [ |
|
{firstName: "Yehuda", id: 1}, |
|
{firstName: "Tom", id: 2} |
|
] |
|
}); |
|
``` |
|
|
|
```handlebars |
|
{{view Ember.Select |
|
content=programmers |
|
optionValuePath="content.id" |
|
optionLabelPath="content.firstName"}} |
|
``` |
|
|
|
Would result in the following HTML: |
|
|
|
```html |
|
<select class="ember-select"> |
|
<option value="1">Yehuda</option> |
|
<option value="2">Tom</option> |
|
</select> |
|
``` |
|
|
|
The `value` attribute of the selected `<option>` within an `Ember.Select` |
|
can be bound to a property on another object: |
|
|
|
```javascript |
|
App.ApplicationController = Ember.Controller.extend({ |
|
programmers: [ |
|
{firstName: "Yehuda", id: 1}, |
|
{firstName: "Tom", id: 2} |
|
], |
|
currentProgrammer: { |
|
id: 2 |
|
} |
|
}); |
|
``` |
|
|
|
```handlebars |
|
{{view Ember.Select |
|
content=programmers |
|
optionValuePath="content.id" |
|
optionLabelPath="content.firstName" |
|
value=currentProgrammer.id}} |
|
``` |
|
|
|
Would result in the following HTML with a selected option: |
|
|
|
```html |
|
<select class="ember-select"> |
|
<option value="1">Yehuda</option> |
|
<option value="2" selected="selected">Tom</option> |
|
</select> |
|
``` |
|
|
|
Interacting with the rendered element by selecting the first option |
|
('Yehuda') will update the `id` of `currentProgrammer` |
|
to match the `value` property of the newly selected `<option>`. |
|
|
|
Alternatively, you can control selection through the underlying objects |
|
used to render each object by binding the `selection` option. When the selected |
|
`<option>` is changed, the property path provided to `selection` |
|
will be updated to match the content object of the rendered `<option>` |
|
element: |
|
|
|
```javascript |
|
App.ApplicationController = Ember.Controller.extend({ |
|
selectedPerson: null, |
|
programmers: [ |
|
{firstName: "Yehuda", id: 1}, |
|
{firstName: "Tom", id: 2} |
|
] |
|
}); |
|
``` |
|
|
|
```handlebars |
|
{{view Ember.Select |
|
content=programmers |
|
optionValuePath="content.id" |
|
optionLabelPath="content.firstName" |
|
selection=selectedPerson}} |
|
``` |
|
|
|
Would result in the following HTML with a selected option: |
|
|
|
```html |
|
<select class="ember-select"> |
|
<option value="1">Yehuda</option> |
|
<option value="2" selected="selected">Tom</option> |
|
</select> |
|
``` |
|
|
|
Interacting with the rendered element by selecting the first option |
|
('Yehuda') will update the `selectedPerson` to match the object of |
|
the newly selected `<option>`. In this case it is the first object |
|
in the `programmers` |
|
|
|
## Supplying a Prompt |
|
|
|
A `null` value for the `Ember.Select`'s `value` or `selection` property |
|
results in there being no `<option>` with a `selected` attribute: |
|
|
|
```javascript |
|
App.ApplicationController = Ember.Controller.extend({ |
|
selectedProgrammer: null, |
|
programmers: [ |
|
"Yehuda", |
|
"Tom" |
|
] |
|
}); |
|
``` |
|
|
|
``` handlebars |
|
{{view Ember.Select |
|
content=programmers |
|
value=selectedProgrammer |
|
}} |
|
``` |
|
|
|
Would result in the following HTML: |
|
|
|
```html |
|
<select class="ember-select"> |
|
<option value="Yehuda">Yehuda</option> |
|
<option value="Tom">Tom</option> |
|
</select> |
|
``` |
|
|
|
Although `selectedProgrammer` is `null` and no `<option>` |
|
has a `selected` attribute the rendered HTML will display the |
|
first item as though it were selected. You can supply a string |
|
value for the `Ember.Select` to display when there is no selection |
|
with the `prompt` option: |
|
|
|
```javascript |
|
App.ApplicationController = Ember.Controller.extend({ |
|
selectedProgrammer: null, |
|
programmers: [ |
|
"Yehuda", |
|
"Tom" |
|
] |
|
}); |
|
``` |
|
|
|
```handlebars |
|
{{view Ember.Select |
|
content=programmers |
|
value=selectedProgrammer |
|
prompt="Please select a name" |
|
}} |
|
``` |
|
|
|
Would result in the following HTML: |
|
|
|
```html |
|
<select class="ember-select"> |
|
<option>Please select a name</option> |
|
<option value="Yehuda">Yehuda</option> |
|
<option value="Tom">Tom</option> |
|
</select> |
|
``` |
|
|
|
@class Select |
|
@namespace Ember |
|
@extends Ember.View |
|
*/ |
|
Ember.Select = Ember.View.extend({ |
|
tagName: 'select', |
|
classNames: ['ember-select'], |
|
defaultTemplate: Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) { |
|
this.compilerInfo = [4,'>= 1.0.0']; |
|
helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {}; |
|
var buffer = '', stack1, escapeExpression=this.escapeExpression, self=this; |
|
|
|
function program1(depth0,data) { |
|
|
|
var buffer = '', stack1; |
|
data.buffer.push("<option value=\"\">"); |
|
stack1 = helpers._triageMustache.call(depth0, "view.prompt", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data}); |
|
if(stack1 || stack1 === 0) { data.buffer.push(stack1); } |
|
data.buffer.push("</option>"); |
|
return buffer; |
|
} |
|
|
|
function program3(depth0,data) { |
|
|
|
var stack1; |
|
stack1 = helpers.each.call(depth0, "view.groupedContent", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(4, program4, data),contexts:[depth0],types:["ID"],data:data}); |
|
if(stack1 || stack1 === 0) { data.buffer.push(stack1); } |
|
else { data.buffer.push(''); } |
|
} |
|
function program4(depth0,data) { |
|
|
|
|
|
data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.groupView", {hash:{ |
|
'content': ("content"), |
|
'label': ("label") |
|
},hashTypes:{'content': "ID",'label': "ID"},hashContexts:{'content': depth0,'label': depth0},contexts:[depth0],types:["ID"],data:data}))); |
|
} |
|
|
|
function program6(depth0,data) { |
|
|
|
var stack1; |
|
stack1 = helpers.each.call(depth0, "view.content", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(7, program7, data),contexts:[depth0],types:["ID"],data:data}); |
|
if(stack1 || stack1 === 0) { data.buffer.push(stack1); } |
|
else { data.buffer.push(''); } |
|
} |
|
function program7(depth0,data) { |
|
|
|
|
|
data.buffer.push(escapeExpression(helpers.view.call(depth0, "view.optionView", {hash:{ |
|
'content': ("") |
|
},hashTypes:{'content': "ID"},hashContexts:{'content': depth0},contexts:[depth0],types:["ID"],data:data}))); |
|
} |
|
|
|
stack1 = helpers['if'].call(depth0, "view.prompt", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],data:data}); |
|
if(stack1 || stack1 === 0) { data.buffer.push(stack1); } |
|
stack1 = helpers['if'].call(depth0, "view.optionGroupPath", {hash:{},hashTypes:{},hashContexts:{},inverse:self.program(6, program6, data),fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],data:data}); |
|
if(stack1 || stack1 === 0) { data.buffer.push(stack1); } |
|
return buffer; |
|
|
|
}), |
|
attributeBindings: ['multiple', 'disabled', 'tabindex', 'name', 'required', 'autofocus', |
|
'form', 'size'], |
|
|
|
/** |
|
The `multiple` attribute of the select element. Indicates whether multiple |
|
options can be selected. |
|
|
|
@property multiple |
|
@type Boolean |
|
@default false |
|
*/ |
|
multiple: false, |
|
|
|
/** |
|
The `disabled` attribute of the select element. Indicates whether |
|
the element is disabled from interactions. |
|
|
|
@property disabled |
|
@type Boolean |
|
@default false |
|
*/ |
|
disabled: false, |
|
|
|
/** |
|
The `required` attribute of the select element. Indicates whether |
|
a selected option is required for form validation. |
|
|
|
@property required |
|
@type Boolean |
|
@default false |
|
*/ |
|
required: false, |
|
|
|
/** |
|
The list of options. |
|
|
|
If `optionLabelPath` and `optionValuePath` are not overridden, this should |
|
be a list of strings, which will serve simultaneously as labels and values. |
|
|
|
Otherwise, this should be a list of objects. For instance: |
|
|
|
```javascript |
|
Ember.Select.create({ |
|
content: Ember.A([ |
|
{ id: 1, firstName: 'Yehuda' }, |
|
{ id: 2, firstName: 'Tom' } |
|
]), |
|
optionLabelPath: 'content.firstName', |
|
optionValuePath: 'content.id' |
|
}); |
|
``` |
|
|
|
@property content |
|
@type Array |
|
@default null |
|
*/ |
|
content: null, |
|
|
|
/** |
|
When `multiple` is `false`, the element of `content` that is currently |
|
selected, if any. |
|
|
|
When `multiple` is `true`, an array of such elements. |
|
|
|
@property selection |
|
@type Object or Array |
|
@default null |
|
*/ |
|
selection: null, |
|
|
|
/** |
|
In single selection mode (when `multiple` is `false`), value can be used to |
|
get the current selection's value or set the selection by it's value. |
|
|
|
It is not currently supported in multiple selection mode. |
|
|
|
@property value |
|
@type String |
|
@default null |
|
*/ |
|
value: Ember.computed(function(key, value) { |
|
if (arguments.length === 2) { return value; } |
|
var valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''); |
|
return valuePath ? get(this, 'selection.' + valuePath) : get(this, 'selection'); |
|
}).property('selection'), |
|
|
|
/** |
|
If given, a top-most dummy option will be rendered to serve as a user |
|
prompt. |
|
|
|
@property prompt |
|
@type String |
|
@default null |
|
*/ |
|
prompt: null, |
|
|
|
/** |
|
The path of the option labels. See [content](/api/classes/Ember.Select.html#property_content). |
|
|
|
@property optionLabelPath |
|
@type String |
|
@default 'content' |
|
*/ |
|
optionLabelPath: 'content', |
|
|
|
/** |
|
The path of the option values. See [content](/api/classes/Ember.Select.html#property_content). |
|
|
|
@property optionValuePath |
|
@type String |
|
@default 'content' |
|
*/ |
|
optionValuePath: 'content', |
|
|
|
/** |
|
The path of the option group. |
|
When this property is used, `content` should be sorted by `optionGroupPath`. |
|
|
|
@property optionGroupPath |
|
@type String |
|
@default null |
|
*/ |
|
optionGroupPath: null, |
|
|
|
/** |
|
The view class for optgroup. |
|
|
|
@property groupView |
|
@type Ember.View |
|
@default Ember.SelectOptgroup |
|
*/ |
|
groupView: Ember.Se |