vendor/assets/javascripts/ember-dev.js in ember-rails-0.2.3 vs vendor/assets/javascripts/ember-dev.js in ember-rails-0.2.4

- old
+ new

@@ -1606,11 +1606,11 @@ if ('undefined' === typeof Ember) { /** @namespace @name Ember - @version 0.9.3 + @version 0.9.4 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. @@ -1638,14 +1638,14 @@ } /** @static @type String - @default '0.9.3' + @default '0.9.4' @constant */ -Ember.VERSION = '0.9.3'; +Ember.VERSION = '0.9.4'; /** @static @type Hash @constant @@ -2385,23 +2385,23 @@ } return target ; } var TUPLE_RET = []; -var IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]/; -var IS_GLOBAL_SET = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]?/; +var IS_GLOBAL = /^([A-Z$]|([0-9][A-Z$]))/; +var IS_GLOBAL_PATH = /^([A-Z$]|([0-9][A-Z$])).*[\.\*]/; var HAS_THIS = /^this[\.\*]/; var FIRST_KEY = /^([^\.\*]+)/; function firstKey(path) { return path.match(FIRST_KEY)[0]; } // assumes path is already normalized function normalizeTuple(target, path) { var hasThis = HAS_THIS.test(path), - isGlobal = !hasThis && IS_GLOBAL.test(path), + isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), key; if (!target || isGlobal) target = window; if (hasThis) path = path.slice(5); @@ -2462,13 +2462,18 @@ return normalizeTuple(target, normalizePath(path)); }; Ember.normalizeTuple.primitive = normalizeTuple; -Ember.getPath = function(root, path) { - var hasThis, hasStar, isGlobal; +Ember.getPath = function(root, path, _checkGlobal) { + var hasThis, hasStar, isGlobal, ret; + // Helpers that operate with 'this' within an #each + if (path === '') { + return root; + } + if (!path && 'string'===typeof root) { path = root; root = null; } @@ -2480,19 +2485,30 @@ if (root === null && !hasStar && path.indexOf('.') < 0) { return get(window, path); } // detect complicated paths and normalize them path = normalizePath(path); hasThis = HAS_THIS.test(path); - isGlobal = !hasThis && IS_GLOBAL.test(path); - if (!root || hasThis || isGlobal || hasStar) { + + if (!root || hasThis || hasStar) { + if (root && root !== window && IS_GLOBAL.test(path)) { + console.warn("Fetching globals with Ember.getPath is deprecated", root, path); + } + var tuple = normalizeTuple(root, path); root = tuple[0]; path = tuple[1]; tuple.length = 0; } - - return getPath(root, path); + + ret = getPath(root, path); + + if (ret === undefined && root !== window && !hasThis && IS_GLOBAL.test(path) && _checkGlobal !== false) { + console.warn("Fetching globals with Ember.getPath is deprecated", root, path); + return Ember.getPath(window, path); + } else { + return ret; + } }; Ember.setPath = function(root, path, value, tolerant) { var keyName; @@ -2502,27 +2518,34 @@ root = null; } path = normalizePath(path); if (path.indexOf('*')>0) { + if (root && root !== window && IS_GLOBAL.test(path)) { + console.warn("Setting globals with Ember.setPath is deprecated", path); + }; + var tuple = normalizeTuple(root, path); root = tuple[0]; path = tuple[1]; tuple.length = 0; } if (path.indexOf('.') > 0) { keyName = path.slice(path.lastIndexOf('.')+1); path = path.slice(0, path.length-(keyName.length+1)); - if (!HAS_THIS.test(path) && IS_GLOBAL_SET.test(path) && path.indexOf('.')<0) { - root = window[path]; // special case only works during set... - } else if (path !== 'this') { - root = Ember.getPath(root, path); + if (path !== 'this') { + // Remove the `false` when we're done with this deprecation + root = Ember.getPath(root, path, false); + if (!root && IS_GLOBAL.test(path)) { + console.warn("Setting globals with Ember.setPath is deprecated", path); + root = Ember.getPath(window, path); + } } } else { - if (IS_GLOBAL_SET.test(path)) throw new Error('Invalid Path'); + if (IS_GLOBAL.test(path)) throw new Error('Invalid Path'); keyName = path; } if (!keyName || keyName.length===0 || keyName==='*') { throw new Error('Invalid Path'); @@ -2750,11 +2773,13 @@ function xformForArgs(args) { return function (target, method, params) { var obj = params[0], keyName = changeKey(params[1]), val; var copy_args = args.slice(); - if (method.length>2) val = Ember.getPath(obj, keyName); + if (method.length>2) { + val = Ember.getPath(Ember.isGlobalPath(keyName) ? window : obj, keyName); + } copy_args.unshift(obj, keyName, val); method.apply(target, copy_args); }; } @@ -3153,13 +3178,14 @@ Ember.defineProperty(contact, 'fullName', Ember.computed(function() { return this.firstName+' '+this.lastName; }).property('firstName', 'lastName').cacheable()); */ Ember.defineProperty = function(obj, keyName, desc, val) { - var m = meta(obj, false), descs = m.descs, watching = m.watching[keyName]>0; + var m = meta(obj, false), descs = m.descs, watching = m.watching[keyName]>0, override = true; if (val === undefined) { + override = false; val = hasDesc(descs, keyName) ? descs[keyName].teardown(obj, keyName) : obj[keyName]; } else if (hasDesc(descs, keyName)) { descs[keyName].teardown(obj, keyName); } @@ -3176,11 +3202,15 @@ // compatibility with ES5 } else { if (descs[keyName]) meta(obj).descs[keyName] = null; o_defineProperty(obj, keyName, desc); } - + + // if key is being watched, override chains that + // were initialized with the prototype + if (override && watching) Ember.overrideChains(obj, keyName, m); + return this; }; /** Creates a new object using the passed object as its prototype. On browsers @@ -3233,24 +3263,11 @@ meta(ret, true).proto = ret; if (GUID_KEY in ret) Ember.generateGuid(ret, 'ember'); if (META_KEY in ret) Ember.rewatch(ret); // setup watch chains if needed. return ret; }; - -/** - Tears down the meta on an object so that it can be garbage collected. - Multiple calls will have no effect. - - @param {Object} obj the object to destroy - @returns {void} -*/ -Ember.destroy = function(obj) { - if (obj[META_KEY]) obj[META_KEY] = null; -}; - - })({}); (function(exports) { // ========================================================================== @@ -3264,10 +3281,11 @@ var get = Ember.get, set = Ember.set; var normalizeTuple = Ember.normalizeTuple.primitive; var normalizePath = Ember.normalizePath; var SIMPLE_PROPERTY = Ember.SIMPLE_PROPERTY; var GUID_KEY = Ember.GUID_KEY; +var META_KEY = Ember.META_KEY; var notifyObservers = Ember.notifyObservers; var FIRST_KEY = /^([^\.\*]+)/; var IS_PATH = /[\.\*]/; @@ -3563,11 +3581,11 @@ path = 'this.'+path; if (this._paths[path]>0) Ember.propertyDidChange(this.value(), path); } }; -Wp.didChange = function() { +Wp.didChange = function(suppressEvent) { // invalidate my own value first. if (this._watching) { var obj = this._parent.value(); if (obj !== this._object) { removeChainWatcher(this._object, this._key, this); @@ -3585,14 +3603,16 @@ // then notify chains... var chains = this._chains; if (chains) { for(var key in chains) { if (!chains.hasOwnProperty(key)) continue; - chains[key].didChange(); + chains[key].didChange(suppressEvent); } } + if (suppressEvent) return; + // and finally tell parent about my path changing... if (this._parent) this._parent.chainDidChange(this, this._key, 1); }; // get the chains for the current object. If the current object has @@ -3608,32 +3628,36 @@ return ret ; } -function notifyChains(obj, keyName, methodName) { - var m = meta(obj, false); +function notifyChains(obj, m, keyName, methodName, arg) { var nodes = m.chainWatchers; + if (!nodes || nodes.__emberproto__ !== obj) return; // nothing to do nodes = nodes[keyName]; if (!nodes) return; - + for(var key in nodes) { if (!nodes.hasOwnProperty(key)) continue; - nodes[key][methodName](obj, keyName); + nodes[key][methodName](arg); } } -function chainsWillChange(obj, keyName) { - notifyChains(obj, keyName, 'willChange'); +Ember.overrideChains = function(obj, keyName, m) { + notifyChains(obj, m, keyName, 'didChange', true); } -function chainsDidChange(obj, keyName) { - notifyChains(obj, keyName, 'didChange'); +function chainsWillChange(obj, keyName, m) { + notifyChains(obj, m, keyName, 'willChange'); } +function chainsDidChange(obj, keyName, m) { + notifyChains(obj, m, keyName, 'didChange'); +} + // .......................................................... // WATCH // var WATCHED_PROPERTY = Ember.SIMPLE_PROPERTY.watched; @@ -3755,11 +3779,11 @@ var propertyWillChange = Ember.propertyWillChange = function(obj, keyName) { var m = meta(obj, false), proto = m.proto, desc = m.descs[keyName]; if (proto === obj) return ; if (desc && desc.willChange) desc.willChange(obj, keyName); dependentKeysWillChange(obj, keyName, m); - chainsWillChange(obj, keyName); + chainsWillChange(obj, keyName, m); Ember.notifyBeforeObservers(obj, keyName); }; /** This function is called just after an object property has changed. @@ -3781,14 +3805,55 @@ var propertyDidChange = Ember.propertyDidChange = function(obj, keyName) { var m = meta(obj, false), proto = m.proto, desc = m.descs[keyName]; if (proto === obj) return ; if (desc && desc.didChange) desc.didChange(obj, keyName); dependentKeysDidChange(obj, keyName, m); - chainsDidChange(obj, keyName); + chainsDidChange(obj, keyName, m); Ember.notifyObservers(obj, keyName); }; +var NODE_STACK = [] + +/** + Tears down the meta on an object so that it can be garbage collected. + Multiple calls will have no effect. + + @param {Object} obj the object to destroy + @returns {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(exports) { // ========================================================================== @@ -3820,12 +3885,24 @@ if ('string'===typeof method) method = target[method]; if (args && ignore>0) { args = args.length>ignore ? slice.call(args, ignore) : null; } - // IE8's Function.prototype.apply doesn't accept undefined/null arguments. - return method.apply(target || this, args || []); + + // Unfortunately in some browsers we lose the backtrace if we rethrow the existing error, + // so in the event that we don't have an `onerror` handler we don't wrap in a try/catch + if ('function' === typeof Ember.onerror) { + try { + // IE8's Function.prototype.apply doesn't accept undefined/null arguments. + return method.apply(target || this, args || []); + } catch (error) { + Ember.onerror(error); + } + } else { + // IE8's Function.prototype.apply doesn't accept undefined/null arguments. + return method.apply(target || this, args || []); + } } // .......................................................... // RUNLOOP @@ -3966,12 +4043,15 @@ */ Ember.run = run = function(target, method) { var ret, loop; run.begin(); - if (target || method) ret = invoke(target, method, arguments, 2); - run.end(); + try { + if (target || method) ret = invoke(target, method, arguments, 2); + } finally { + run.end(); + } return ret; }; /** Begins a new RunLoop. Any deferred actions invoked after the begin will @@ -4418,11 +4498,12 @@ }; var get = Ember.get, getPath = Ember.getPath, setPath = Ember.setPath, - guidFor = Ember.guidFor; + guidFor = Ember.guidFor, + isGlobalPath = Ember.isGlobalPath; // Applies a binding's transformations against a value. function getTransformedValue(binding, val, obj, dir) { // First run a type transform, if it exists, that changes the fundamental @@ -4446,27 +4527,36 @@ function empty(val) { return val===undefined || val===null || val==='' || (Ember.isArray(val) && get(val, 'length')===0) ; } +function getPathWithGlobals(obj, path) { + return getPath(isGlobalPath(path) ? window : obj, path); +} + function getTransformedFromValue(obj, binding) { - var operation = binding._operation; - var fromValue = operation ? operation(obj, binding._from, binding._operand) : getPath(obj, binding._from); + var operation = binding._operation, + fromValue; + if (operation) { + fromValue = operation(obj, binding._from, binding._operand); + } else { + fromValue = getPathWithGlobals(obj, binding._from); + } return getTransformedValue(binding, fromValue, obj, 'to'); } function getTransformedToValue(obj, binding) { var toValue = getPath(obj, binding._to); return getTransformedValue(binding, toValue, obj, 'from'); } var AND_OPERATION = function(obj, left, right) { - return getPath(obj, left) && getPath(obj, right); + return getPathWithGlobals(obj, left) && getPathWithGlobals(obj, right); }; var OR_OPERATION = function(obj, left, right) { - return getPath(obj, left) || getPath(obj, right); + return getPathWithGlobals(obj, left) || getPathWithGlobals(obj, right); }; // .......................................................... // BINDING // @@ -4815,11 +4905,11 @@ // 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 guid = guidFor(obj), direction = this[guid], val, transformedValue; + var guid = guidFor(obj), direction = this[guid]; var fromPath = this._from, toPath = this._to; delete this[guid]; @@ -4829,17 +4919,17 @@ if (toValue === fromValue) { return; } // if we're synchronizing from the remote object... if (direction === 'fwd') { - if (log) { Ember.Logger.log(' ', this.toString(), val, '->', fromValue, obj); } - Ember.trySetPath(obj, toPath, fromValue); + if (log) { Ember.Logger.log(' ', this.toString(), toValue, '->', fromValue, obj); } + Ember.trySetPath(Ember.isGlobalPath(toPath) ? window : obj, toPath, fromValue); // if we're synchronizing *to* the remote object } else if (direction === 'back') {// && !this._oneWay) { - if (log) { Ember.Logger.log(' ', this.toString(), val, '<-', fromValue, obj); } - Ember.trySetPath(obj, fromPath, toValue); + if (log) { Ember.Logger.log(' ', this.toString(), toValue, '<-', fromValue, obj); } + Ember.trySetPath(Ember.isGlobalPath(fromPath) ? window : obj, fromPath, toValue); } } }; @@ -5069,10 +5159,19 @@ with any other helpers. The example below will create a one way binding that does not allow empty values or values less than 10: valueBinding: Ember.Binding.oneWay("MyApp.someController.value").notEmpty().notLessThan(10) + Finally, it's also possible to specify bi-directional transforms. To do this, + you can pass a hash to `transform` with `to` and `from`. In the following + example, we are expecting a lowercase string that we want to transform to + uppercase. + + valueBinding: Ember.Binding.transform({ + to: function(value, binding) { return value.toUpperCase(); }, + from: function(value, binding) { return value.toLowerCase(); } + ## How to Manually Adding Binding 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. The binding will actually become active only when you @@ -6066,10 +6165,13 @@ var Namespace = Ember.Namespace, obj; if (Namespace.PROCESSED) { return; } for (var prop in window) { + // 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" && window.StorageList && window.globalStorage instanceof window.StorageList) { continue; } // Unfortunately, some versions of IE don't support window.hasOwnProperty if (window.hasOwnProperty && !window.hasOwnProperty(prop)) { continue; } obj = window[prop]; @@ -6228,13 +6330,15 @@ contexts.push(ctx); return null; } function iter(key, value) { + var valueProvided = arguments.length === 2; + function i(item) { var cur = get(item, key); - return value===undefined ? !!cur : value===cur; + return valueProvided ? value===cur : !!cur; } return i ; } function xform(target, method, params) { @@ -6325,10 +6429,17 @@ 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. @returns {Object} the object or undefined + + @example + var arr = ["a", "b", "c"]; + arr.firstObject(); => "a" + + var arr = []; + arr.firstObject(); => undefined */ firstObject: Ember.computed(function() { if (get(this, 'length')===0) return undefined ; if (Ember.Array && Ember.Array.detect(this)) return this.objectAt(0); @@ -6338,13 +6449,22 @@ pushCtx(context); return ret ; }).property(), /** - Helper method returns the last object from a collection. + 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. - @returns {Object} the object or undefined + @returns {Object} the last object or undefined + + @example + var arr = ["a", "b", "c"]; + arr.lastObject(); => "c" + + var arr = []; + arr.lastObject(); => undefined */ lastObject: Ember.computed(function() { var len = get(this, 'length'); if (len===0) return undefined ; if (Ember.Array && Ember.Array.detect(this)) { @@ -6356,11 +6476,10 @@ 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 @@ -6523,11 +6642,11 @@ @params key {String} the property to test @param value {String} optional value to test against. @returns {Array} filtered array */ filterProperty: function(key, value) { - return this.filter(iter(key, value)); + return this.filter(iter.apply(this, arguments)); }, /** Returns the first item in the array for which the callback returns YES. This method works similar to the filter() method defined in JavaScript 1.6 @@ -6578,11 +6697,11 @@ @params key {String} the property to test @param value {String} optional value to test against. @returns {Object} found item or null */ findProperty: function(key, value) { - return this.find(iter(key, value)); + return this.find(iter.apply(this, arguments)); }, /** Returns YES if the passed function returns YES for every item in the enumeration. This corresponds with the every() method in JavaScript 1.6. @@ -6623,11 +6742,11 @@ @params key {String} the property to test @param value {String} optional value to test against. @returns {Array} filtered array */ everyProperty: function(key, value) { - return this.every(iter(key, value)); + return this.every(iter.apply(this, arguments)); }, /** Returns YES if the passed function returns true for any item in the @@ -6669,11 +6788,11 @@ @params key {String} the property to test @param value {String} optional value to test against. @returns {Boolean} true */ someProperty: function(key, value) { - return this.some(iter(key, value)); + return this.some(iter.apply(this, arguments)); }, /** 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 @@ -6739,11 +6858,11 @@ return ret; }, /** Simply converts the enumerable into a genuine array. The order is not - gauranteed. Corresponds to the method implemented by Prototype. + guaranteed. Corresponds to the method implemented by Prototype. @returns {Array} the enumerable as an array. */ toArray: function() { var ret = []; @@ -7066,15 +7185,27 @@ } return ret ; }, /** - Returns the index for a particular object in the index. + 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. @param {Object} object the item to search for - @param {NUmber} startAt optional starting location to search, default 0 - @returns {Number} index of -1 if not found + @param {Number} startAt optional starting location to search, default 0 + @returns {Number} index or -1 if not found + + @example + 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 */ indexOf: function(object, startAt) { var idx, len = get(this, 'length'); if (startAt === undefined) startAt = 0; @@ -7085,20 +7216,32 @@ } return -1; }, /** - Returns the last index for a particular object in the index. + 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. @param {Object} object the item to search for - @param {NUmber} startAt optional starting location to search, default 0 - @returns {Number} index of -1 if not found + @param {Number} startAt optional starting location to search, default 0 + @returns {Number} index or -1 if not found + + @example + 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 */ lastIndexOf: function(object, startAt) { var idx, len = get(this, 'length'); - if (startAt === undefined) startAt = len-1; + 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 ; } @@ -7739,15 +7882,16 @@ @param {Hash} hash the hash of keys and values to set @returns {Ember.Observable} */ setProperties: function(hash) { - Ember.beginPropertyChanges(this); - for(var prop in hash) { - if (hash.hasOwnProperty(prop)) set(this, prop, hash[prop]); - } - Ember.endPropertyChanges(this); + var self = this; + Ember.changeProperties(function(){ + for(var prop in hash) { + if (hash.hasOwnProperty(prop)) set(self, prop, hash[prop]); + } + }); return this; }, /** Begins a grouping of property changes. @@ -8055,11 +8199,11 @@ scheduled for execution by the `destroy` method. @private */ _scheduledDestroy: function() { - this[Ember.META_KEY] = null; + Ember.destroy(this); }, bind: function(to, from) { if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); } from.to(to).connect(this); @@ -9723,39 +9867,49 @@ })({}); (function(exports) { -var get = Ember.get, set = Ember.set; +var get = Ember.get, set = Ember.set, getPath = Ember.getPath; Ember.TargetActionSupport = Ember.Mixin.create({ target: null, action: null, targetObject: Ember.computed(function() { var target = get(this, 'target'); if (Ember.typeOf(target) === "string") { - return Ember.getPath(this, target); + // TODO: Remove the false when deprecation is done + var value = getPath(this, target, false); + if (value === undefined) { value = getPath(window, target); } + return value; } else { return target; } }).property('target').cacheable(), triggerAction: function() { var action = get(this, 'action'), target = get(this, 'targetObject'); if (target && action) { + var ret; + if (typeof target.send === 'function') { - target.send(action, this); + ret = target.send(action, this); } else { if (typeof action === 'string') { action = target[action]; } - action.call(target, this); + ret = action.call(target, this); } + if (ret !== false) ret = true; + + return ret; + } else { + return false; } } }); })({}); @@ -10318,22 +10472,22 @@ this._super(); set(this ,'elementClasses', Ember.A()); set(this, 'elementAttributes', {}); set(this, 'elementStyle', {}); - set(this, 'childBuffers', Ember.A()); + set(this, 'childBuffers', []); set(this, 'elements', {}); }, /** Adds a string of HTML to the RenderBuffer. @param {String} string HTML to push into the buffer @returns {Ember.RenderBuffer} this */ push: function(string) { - get(this, 'childBuffers').pushObject(String(string)); + get(this, 'childBuffers').push(String(string)); return this; }, /** Adds a class to the buffer, which will be rendered to the class attribute. @@ -10464,22 +10618,22 @@ @param {String} tagName Tag name to use for the child buffer's element @returns {Ember.RenderBuffer} A new RenderBuffer object */ begin: function(tagName) { return this.newBuffer(tagName, this, function(buffer) { - get(this, 'childBuffers').pushObject(buffer); + get(this, 'childBuffers').push(buffer); }); }, /** Prepend a new child buffer to the current render buffer. @param {String} tagName Tag name to use for the child buffer's element */ prepend: function(tagName) { return this.newBuffer(tagName, this, function(buffer) { - get(this, 'childBuffers').insertAt(0, buffer); + get(this, 'childBuffers').splice(0, 0, buffer); }); }, /** Replace the current buffer with a new render buffer. @@ -10503,11 +10657,11 @@ var parentBuffer = get(this, 'parentBuffer'); return this.newBuffer(tagName, parentBuffer, function(buffer) { var siblings = get(parentBuffer, 'childBuffers'); var index = siblings.indexOf(this); - siblings.insertAt(index + 1, buffer); + siblings.splice(index + 1, 0, buffer); }); }, /** Closes the current buffer and adds its content to the parentBuffer. @@ -10613,14 +10767,19 @@ @private 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. + @type DOMElement - @default document + @default 'body' */ - rootElement: document, + rootElement: 'body', /** @private Sets up event listeners for standard browser events. @@ -10667,10 +10826,12 @@ 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 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]); } } @@ -10747,21 +10908,13 @@ return result; }, /** @private */ _bubbleEvent: function(view, evt, eventName) { - var result = true, handler, - self = this; - - Ember.run(function() { - handler = view[eventName]; - if (Ember.typeOf(handler) === 'function') { - result = handler.call(view, evt); - } - }); - - return result; + return Ember.run(function() { + return view.handleEvent(eventName, evt); + }); }, /** @private */ destroy: function() { var rootElement = get(this, 'rootElement'); @@ -10811,14 +10964,18 @@ */ Ember.Application = Ember.Namespace.extend( /** @scope Ember.Application.prototype */{ /** + The root DOM element of the Application. + + Can be specified as DOMElement or a selector string. + @type DOMElement - @default document + @default 'body' */ - rootElement: document, + rootElement: 'body', /** @type Ember.EventDispatcher @default null */ @@ -10840,25 +10997,39 @@ rootElement: rootElement }); set(this, 'eventDispatcher', eventDispatcher); - var self = this; - Ember.$(document).ready(function() { - self.ready(); - }); + // jQuery 1.7 doesn't call the ready callback if already ready + if (Ember.$.isReady) { + this.didBecomeReady(); + } else { + var self = this; + Ember.$(document).ready(function() { + self.didBecomeReady(); + }); + } this._super(); }, - ready: function() { + /** @private */ + didBecomeReady: function() { var eventDispatcher = get(this, 'eventDispatcher'), customEvents = get(this, 'customEvents'); eventDispatcher.setup(customEvents); + + this.ready(); }, + /** + Called when the Application has become ready. + The call will be delayed until the DOM has become ready. + */ + ready: Ember.K, + /** @private */ destroy: function() { get(this, 'eventDispatcher').destroy(); return this._super(); } @@ -11042,17 +11213,23 @@ } else { return parent; } }).property('_parentView'), + // return the current view, not including virtual views + concreteView: Ember.computed(function() { + if (!this.isVirtual) { return this; } + else { return get(this, 'parentView'); } + }).property('_parentView'), + /** If false, the view will appear hidden in DOM. @type Boolean - @default true + @default null */ - isVisible: true, + isVisible: null, /** Array of child views. You should never edit this array directly. Instead, use appendChild and removeFromParent. @@ -11463,10 +11640,13 @@ */ appendTo: function(target) { // Schedule the DOM element to be created and appended to the given // element after bindings have synchronized. this._insertElementLater(function() { + if (get(this, 'isVisible') === null) { + set(this, 'isVisible', true); + } this.$().appendTo(target); }); return this; }, @@ -11583,11 +11763,11 @@ @returns {Ember.RenderBuffer} */ renderBuffer: function(tagName) { tagName = tagName || get(this, 'tagName'); - if (tagName == null) { tagName = tagName || 'div'; } + if (tagName == null) { tagName = 'div'; } return Ember.RenderBuffer(tagName); }, /** @@ -11772,11 +11952,11 @@ // 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"). if (parentBuffer) { var tagName = get(this, 'tagName'); - tagName = tagName == null ? 'div' : tagName; + if (tagName == null) { tagName = 'div'; } buffer = parentBuffer[bufferOperation](tagName); } else { buffer = this.renderBuffer(); } @@ -11821,11 +12001,11 @@ var role = get(this, 'ariaRole'); if (role) { buffer.attr('role', role); } - if (!get(this, 'isVisible')) { + if (get(this, 'isVisible') === false) { buffer.style('display', 'none'); } }, // .......................................................... @@ -11961,10 +12141,18 @@ this.classNames = Ember.A(get(this, 'classNames').slice()); set(this, 'domManager', this.domManagerClass.create({ view: this })); meta(this)["Ember.View"] = {}; + + var viewController = get(this, 'viewController'); + if (viewController) { + viewController = Ember.getPath(viewController); + if (viewController) { + set(viewController, 'view', this); + } + } }, appendChild: function(view, options) { return this.invokeForState('appendChild', view, options); }, @@ -12075,28 +12263,79 @@ createChildView: function(view, attrs) { if (Ember.View.detect(view)) { view = view.create(attrs || {}, { _parentView: this }); var viewName = attrs && attrs.viewName || view.viewName; - if (viewName) { set(this, viewName, view); } + + // don't set the property on a virtual view, as they are invisible to + // consumers of the view API + if (viewName) { set(get(this, 'concreteView'), viewName, view); } } else { ember_assert('must pass instance of View', view instanceof Ember.View); set(view, '_parentView', this); } return view; }, + becameVisible: Ember.K, + becameHidden: Ember.K, + /** @private When the view's `isVisible` property changes, toggle the visibility element of the actual DOM element. */ _isVisibleDidChange: Ember.observer(function() { - this.$().toggle(get(this, 'isVisible')); + var isVisible = get(this, 'isVisible'); + + this.$().toggle(isVisible); + + if (this._isAncestorHidden()) { return; } + + if (isVisible) { + this._notifyBecameVisible(); + } else { + this._notifyBecameHidden(); + } }, 'isVisible'), + _notifyBecameVisible: function() { + this.becameVisible(); + + this.forEachChildView(function(view) { + var isVisible = get(view, 'isVisible'); + + if (isVisible || isVisible === null) { + view._notifyBecameVisible(); + } + }); + }, + + _notifyBecameHidden: function() { + this.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(function(view) { meta(view)['Ember.View'].buffer = null; }); }, @@ -12107,10 +12346,23 @@ if (children !== false) { this.forEachChildView(function(view) { view.transitionTo(state); }); } + }, + + // ....................................................... + // EVENT HANDLING + // + + /** + @private + + Handle events from `Ember.EventDispatcher` + */ + handleEvent: function(eventName, evt) { + return this.invokeForState('handleEvent', eventName, evt); } }); /** @@ -12230,10 +12482,15 @@ return Ember.$(); }, getElement: function() { return null; + }, + + // Handle events from `Ember.EventDispatcher` + handleEvent: function() { + return true; // continue event propagation } } }; Ember.View.reopen({ @@ -12426,10 +12683,20 @@ this.willDestroyElement(); }); get(view, 'domManager').remove(); return view; + }, + + // Handle events from `Ember.EventDispatcher` + handleEvent: function(view, eventName, evt) { + var handler = view[eventName]; + if (Ember.typeOf(handler) === 'function') { + return handler.call(view, evt); + } else { + return true; // continue event propagation + } } }; Ember.View.states.inDOM = { parentState: Ember.View.states.hasElement, @@ -12559,11 +12826,11 @@ view.renderToBuffer(buffer); }); }, /** - When the container view is destroyer, tear down the child views + When the container view is destroyed, tear down the child views array observer. @private */ destroy: function() { @@ -12586,10 +12853,15 @@ @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) { + if (removed === 0) { return; } + + var changedViews = views.slice(start, removed); + this.setParentView(changedViews, null); + this.invokeForState('childViewsWillChange', views, start, removed); }, /** When a child view is added, make sure the DOM gets updated appropriately. @@ -12610,14 +12882,23 @@ var len = get(views, 'length'); // No new child views were added; bail out. if (added === 0) return; + var changedViews = views.slice(start, added); + this.setParentView(changedViews, this); + // Let the current state handle the changes this.invokeForState('childViewsDidChange', views, start, added); }, + setParentView: function(views, parentView) { + views.forEach(function(view) { + set(view, '_parentView', parentView); + }); + }, + /** Schedules a child view to be inserted into the DOM after bindings have finished syncing for this run loop. @param {Ember.View} view the child view to insert @@ -12823,10 +13104,14 @@ arrayDidChange: function(content, start, removed, added) { var itemViewClass = get(this, 'itemViewClass'), childViews = get(this, 'childViews'), addedViews = [], view, item, idx, len, itemTagName; + if ('string' === typeof itemViewClass) { + itemViewClass = Ember.getPath(itemViewClass); + } + ember_assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", [itemViewClass]), Ember.View.detect(itemViewClass)); len = content ? get(content, 'length') : 0; if (len) { for (idx = start; idx < start+added; idx++) { @@ -12902,11 +13187,12 @@ // Project: Ember - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== -Ember.$ = jQuery; +ember_assert("Ember requires jQuery 1.6 or 1.7", window.jQuery && jQuery().jquery.match(/^1\.[67](.\d+)?$/)); +Ember.$ = window.jQuery; })({}); (function(exports) { var get = Ember.get, set = Ember.set; @@ -12914,20 +13200,56 @@ isState: true, parentState: null, start: null, init: function() { - Ember.keys(this).forEach(function(name) { - var value = this[name]; + var states = get(this, 'states'), foundStates; - if (value && value.isState) { - set(value, 'parentState', this); - set(value, 'name', (get(this, 'name') || '') + '.' + name); + // As a convenience, loop over the properties + // of this state and look for any that are other + // Ember.State instances or classes, and move them + // to the `states` hash. This avoids having to + // create an explicit separate hash. + + if (!states) { + states = {}; + for (var name in this) { + if (name === "constructor") { continue; } + value = this.setupChild(name, this[name]); + + if (value) { + foundStates = true; + states[name] = value; + } } - }, this); + + if (foundStates) { set(this, 'states', states); } + } else { + for (var name in states) { + this.setupChild(name, states[name]); + } + } + + set(this, 'routes', {}); }, + setupChild: function(name, value) { + if (!value) { return false; } + + if (Ember.State.detect(value)) { + value = value.create(); + } + + if (value.isState) { + set(value, 'parentState', this); + set(value, 'name', (get(this, 'name') || '') + '.' + name); + return value; + } + + return false; + }, + enter: Ember.K, exit: Ember.K }); })({}); @@ -12949,24 +13271,10 @@ `initialState` property. */ init: function() { this._super(); - var states = get(this, 'states'); - if (!states) { - states = {}; - Ember.keys(this).forEach(function(name) { - var value = get(this, name); - - if (value && value.isState) { - states[name] = value; - } - }, this); - - set(this, 'states', states); - } - var initialState = get(this, 'initialState'); if (!initialState && get(this, 'start')) { initialState = 'start'; } @@ -12983,11 +13291,11 @@ If the current state is a view state or the descendent of a view state, this property will be the view associated with it. If there is no view state active in this state manager, this value will be null. */ - currentView: SC.computed(function() { + currentView: Ember.computed(function() { var currentState = get(this, 'currentState'), view; while (currentState) { if (get(currentState, 'isViewState')) { @@ -13017,36 +13325,76 @@ var parentState = get(currentState, 'parentState'); if (parentState) { this.sendRecursively(event, parentState, context); } } }, + findStatesByRoute: function(state, route) { + if (!route || route === "") { return undefined; } + var r = route.split('.'), ret = []; + + for (var i=0, len = r.length; i < len; i += 1) { + var states = get(state, 'states') ; + + if (!states) { return undefined; } + + var s = get(states, r[i]); + if (s) { state = s; ret.push(s); } + else { return undefined; } + } + + return ret; + }, + goToState: function(name) { if (Ember.empty(name)) { return; } var currentState = get(this, 'currentState') || this, state, newState; - var exitStates = Ember.A(); + var exitStates = [], enterStates; - newState = getPath(currentState, name); state = currentState; - if (!newState) { + if (state.routes[name]) { + // cache hit + exitStates = state.routes[name].exitStates; + enterStates = state.routes[name].enterStates; + state = state.routes[name].futureState; + } else { + // cache miss + + newState = this.findStatesByRoute(currentState, name); + while (state && !newState) { - exitStates[Ember.guidFor(state)] = state; - exitStates.push(state); + exitStates.unshift(state); state = get(state, 'parentState'); if (!state) { - state = get(this, 'states'); - newState = getPath(state, name); + newState = this.findStatesByRoute(this, name); if (!newState) { return; } } - newState = getPath(state, name); + newState = this.findStatesByRoute(state, name); } + + enterStates = newState.slice(0), exitStates = exitStates.slice(0); + + if (enterStates.length > 0) { + state = enterStates[enterStates.length - 1]; + + while (enterStates.length > 0 && enterStates[0] === exitStates[0]) { + enterStates.shift(); + exitStates.shift(); + } + } + + currentState.routes[name] = { + exitStates: exitStates, + enterStates: enterStates, + futureState: state + }; } - this.enterState(state, name, exitStates); + this.enterState(exitStates, enterStates, state); }, getState: function(name) { var state = get(this, name), parentState = get(this, 'parentState'); @@ -13079,28 +13427,13 @@ callback.call(this, head, transition); if (!async) { transition.resume(); } }, - enterState: function(parent, name, exitStates) { + enterState: function(exitStates, enterStates, state) { var log = Ember.LOG_STATE_TRANSITIONS; - var parts = name.split("."), state = parent, enterStates = Ember.A(); - - parts.forEach(function(name) { - state = state[name]; - - var guid = Ember.guidFor(state); - - if (guid in exitStates) { - exitStates.removeObject(state); - delete exitStates[guid]; - } else { - enterStates.push(state); - } - }); - var stateManager = this; this.asyncEach(exitStates, function(state, transition) { state.exit(stateManager, transition); }, function() { @@ -13136,27 +13469,51 @@ Ember.ViewState = Ember.State.extend({ isViewState: true, enter: function(stateManager) { - var view = get(this, 'view'); + var view = get(this, 'view'), root, childViews; if (view) { - view.appendTo(stateManager.get('rootElement') || 'body'); + if (Ember.View.detect(view)) { + view = view.create(); + set(this, 'view', view); + } + + ember_assert('view must be an Ember.View', view instanceof Ember.View); + + root = stateManager.get('rootView'); + + if (root) { + childViews = get(root, 'childViews'); + childViews.pushObject(view); + } else { + root = stateManager.get('rootElement') || 'body'; + view.appendTo(root); + } } }, exit: function(stateManager) { var view = get(this, 'view'); if (view) { - view.remove(); + // If the view has a parent view, then it is + // part of a view hierarchy and should be removed + // from its parent. + if (get(view, 'parentView')) { + view.removeFromParent(); + } else { + + // Otherwise, the view is a "root view" and + // was appended directly to the DOM. + view.remove(); + } } } }); - })({}); (function(exports) { // ========================================================================== @@ -13663,11 +14020,11 @@ */ Ember.Handlebars.Compiler.prototype.mustache = function(mustache) { if (mustache.params.length || mustache.hash) { return Handlebars.Compiler.prototype.mustache.call(this, mustache); } else { - var id = new Handlebars.AST.IdNode(['bind']); + var id = new Handlebars.AST.IdNode(['_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) { @@ -13678,10 +14035,23 @@ return Handlebars.Compiler.prototype.mustache.call(this, mustache); } }; /** + Used for precompilation of Ember Handlebars templates. This will not be used during normal + app execution. + + @param {String} string The template to precompile +*/ +Ember.Handlebars.precompile = function(string) { + var ast = Handlebars.parse(string); + var options = { data: true, stringParams: true }; + var environment = new Ember.Handlebars.Compiler().compile(ast, options); + return new Ember.Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); +}; + +/** The entry point for Ember Handlebars. This replaces the default Handlebars.compile and turns on template-local data and String parameters. @param {String} string The template to compile */ @@ -13693,10 +14063,25 @@ return Handlebars.template(templateSpec); }; /** + Lookup both on root and on window + + @param {Object} root The object to look up the property on + @param {String} path The path to be lookedup +*/ +Ember.Handlebars.getPath = function(root, path) { + // TODO: Remove this `false` when the `getPath` globals support is removed + var value = Ember.getPath(root, path, false); + if (value === undefined && root !== window && Ember.isGlobalPath(path)) { + value = Ember.getPath(window, path); + } + return value; +}; + +/** 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 @@ -13778,21 +14163,18 @@ insertNewline: Ember.K, cancel: Ember.K, focusOut: function(event) { this._elementValueDidChange(); - return false; }, change: function(event) { this._elementValueDidChange(); - return false; }, keyUp: function(event) { this.interpretKeyEvents(event); - return false; }, /** @private */ @@ -13803,11 +14185,11 @@ this._elementValueDidChange(); if (method) { return this[method](event); } }, _elementValueDidChange: function() { - set(this, 'value', this.$().val() || ''); + set(this, 'value', this.$().val()); } }); Ember.TextSupport.KEY_EVENTS = { @@ -13835,19 +14217,12 @@ classNames: ['ember-text-field'], tagName: "input", attributeBindings: ['type', 'value'], - type: "text", + type: "text" - /** - @private - */ - _updateElementValue: function() { - this.$().val(get(this, 'value')); - } - }); })({}); @@ -13862,23 +14237,30 @@ Ember.Button = Ember.View.extend(Ember.TargetActionSupport, { classNames: ['ember-button'], classNameBindings: ['isActive'], tagName: 'button', - attributeBindings: ['type', 'disabled'], - type: 'button', - disabled: false, + propagateEvents: false, - click: function() { - // Actually invoke the button's target and action. - // This method comes from the Ember.TargetActionSupport mixin. - this.triggerAction(); + attributeBindings: ['type', 'disabled', 'href'], - return get(this, 'propagateEvents'); - }, + // Defaults to 'button' if tagName is 'input' or 'button' + type: Ember.computed(function(key, value) { + var tagName = this.get('tagName'); + if (value !== undefined) { this._type = value; } + if (this._type !== undefined) { return this._type; } + if (tagName === 'input' || tagName === 'button') { return 'button'; } + }).property('tagName').cacheable(), + disabled: false, + + // Allow 'a' tags to act like buttons + href: Ember.computed(function() { + return this.get('tagName') === 'a' ? '#' : null; + }).property('tagName').cacheable(), + mouseDown: function() { if (!get(this, 'disabled')) { set(this, 'isActive', true); this._mouseDown = true; this._mouseEntered = true; @@ -13900,28 +14282,45 @@ } }, mouseUp: function(event) { if (get(this, 'isActive')) { + // Actually invoke the button's target and action. + // This method comes from the Ember.TargetActionSupport mixin. + this.triggerAction(); set(this, 'isActive', false); } this._mouseDown = false; this._mouseEntered = false; return get(this, 'propagateEvents'); }, + keyDown: function(event) { + // Handle space or enter + if (event.keyCode === 13 || event.keyCode === 32) { + this.mouseDown(); + } + }, + + keyUp: function(event) { + // Handle space or enter + if (event.keyCode === 13 || event.keyCode === 32) { + this.mouseUp(); + } + }, + // TODO: Handle proper touch behavior. Including should make inactive when // finger moves more than 20x outside of the edge of the button (vs mouse // which goes inactive as soon as mouse goes out of edges.) touchStart: function(touch) { - this.mouseDown(touch); + return this.mouseDown(touch); }, touchEnd: function(touch) { - this.mouseUp(touch); + return this.mouseUp(touch); } }); })({}); @@ -13969,15 +14368,15 @@ (function(exports) { var get = Ember.get, getPath = Ember.getPath; Ember.TabPaneView = Ember.View.extend({ - tabsContainer: SC.computed(function() { + tabsContainer: Ember.computed(function() { return this.nearestInstanceOf(Ember.TabContainerView); }).property(), - isVisible: SC.computed(function() { + isVisible: Ember.computed(function() { return get(this, 'viewName') === getPath(this, 'tabsContainer.currentView'); }).property('tabsContainer.currentView') }); })({}); @@ -13985,11 +14384,11 @@ (function(exports) { var get = Ember.get, setPath = Ember.setPath; Ember.TabView = Ember.View.extend({ - tabsContainer: SC.computed(function() { + tabsContainer: Ember.computed(function() { return this.nearestInstanceOf(Ember.TabContainerView); }).property(), mouseUp: function() { setPath(this, 'tabsContainer.currentView', get(this, 'value')); @@ -14002,10 +14401,97 @@ (function(exports) { })({}); (function(exports) { +var set = Ember.set, get = Ember.get, getPath = Ember.getPath; + +Ember.Select = Ember.View.extend({ + tagName: 'select', + template: Ember.Handlebars.compile( + '{{#if prompt}}<option>{{prompt}}</option>{{/if}}' + + '{{#each content}}{{view Ember.SelectOption contentBinding="this"}}{{/each}}' + ), + + content: null, + selection: null, + prompt: null, + + optionLabelPath: 'content', + optionValuePath: 'content', + + + didInsertElement: function() { + var selection = get(this, 'selection'); + + if (selection) { this.selectionDidChange(); } + + this.change(); + }, + + change: function() { + var selectedIndex = this.$()[0].selectedIndex, + content = get(this, 'content'), + prompt = get(this, 'prompt'); + + if (!content) { return; } + if (prompt && selectedIndex === 0) { set(this, 'selection', null); return; } + + if (prompt) { selectedIndex -= 1; } + set(this, 'selection', content.objectAt(selectedIndex)); + }, + + selectionDidChange: Ember.observer(function() { + var el = this.$()[0], + content = get(this, 'content'), + selection = get(this, 'selection'), + selectionIndex = content.indexOf(selection), + prompt = get(this, 'prompt'); + + if (prompt) { selectionIndex += 1; } + if (el) { el.selectedIndex = selectionIndex; } + }, 'selection') +}); + +Ember.SelectOption = Ember.View.extend({ + tagName: 'option', + template: Ember.Handlebars.compile("{{label}}"), + attributeBindings: ['value'], + + init: function() { + this.labelPathDidChange(); + this.valuePathDidChange(); + + this._super(); + }, + + labelPathDidChange: Ember.observer(function() { + var labelPath = getPath(this, 'parentView.optionLabelPath'); + + if (!labelPath) { return; } + + Ember.defineProperty(this, 'label', Ember.computed(function() { + return getPath(this, labelPath); + }).property(labelPath).cacheable()); + }, 'parentView.optionLabelPath'), + + valuePathDidChange: Ember.observer(function() { + var valuePath = getPath(this, 'parentView.optionValuePath'); + + if (!valuePath) { return; } + + Ember.defineProperty(this, 'value', Ember.computed(function() { + return getPath(this, valuePath); + }).property(valuePath).cacheable()); + }, 'parentView.optionValuePath') +}); + + +})({}); + + +(function(exports) { // ========================================================================== // Project: Ember Handlebar Views // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== @@ -14096,11 +14582,11 @@ // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== /*globals Handlebars */ -var get = Ember.get, set = Ember.set, getPath = Ember.getPath; +var get = Ember.get, set = Ember.set, getPath = Ember.Handlebars.getPath; /** @ignore @private @class @@ -14167,10 +14653,33 @@ @type String @default null */ property: null, + normalizedValue: Ember.computed(function() { + var property = get(this, 'property'), + context = get(this, 'previousContext'), + valueNormalizer = get(this, 'valueNormalizerFunc'), + result; + + // Use the current context as the result if no + // property is provided. + if (property === '') { + result = context; + } else { + result = getPath(context, property); + } + + return valueNormalizer ? valueNormalizer(result) : result; + }).property('property', 'previousContext', 'valueNormalizerFunc'), + + rerenderIfNeeded: function() { + if (!get(this, 'isDestroyed') && get(this, 'normalizedValue') !== this._lastNormalizedValue) { + this.rerender(); + } + }, + /** 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 `property` key on `previousContext`, @@ -14189,28 +14698,19 @@ // If not invoked via a triple-mustache ({{{foo}}}), escape // the content of the template. var escape = get(this, 'isEscaped'); var shouldDisplay = get(this, 'shouldDisplayFunc'), - property = get(this, 'property'), preserveContext = get(this, 'preserveContext'), context = get(this, 'previousContext'); var inverseTemplate = get(this, 'inverseTemplate'), displayTemplate = get(this, 'displayTemplate'); - var result; + var result = get(this, 'normalizedValue'); + this._lastNormalizedValue = result; - - // Use the current context as the result if no - // property is provided. - if (property === '') { - result = context; - } else { - result = getPath(context, property); - } - // First, test the conditional to see if we should // render the template or not. if (shouldDisplay(result)) { set(this, 'template', displayTemplate); @@ -14256,16 +14756,19 @@ // Project: Ember Handlebar Views // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== /*globals Handlebars */ -var get = Ember.get, getPath = Ember.getPath, set = Ember.set, fmt = Ember.String.fmt; +var get = Ember.get, getPath = Ember.Handlebars.getPath, set = Ember.set, fmt = Ember.String.fmt; +var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers; +var helpers = EmberHandlebars.helpers; + (function() { // Binds a property into the DOM. This will create a hook in DOM that the - // KVO system will look for and upate if the property changes. - var bind = function(property, options, preserveContext, shouldDisplay) { + // KVO system will look for and update if the property changes. + var bind = function(property, options, preserveContext, shouldDisplay, valueNormalizer) { var data = options.data, fn = options.fn, inverse = options.inverse, view = data.view, ctx = this; @@ -14276,47 +14779,64 @@ // and add it to the nearest view's childViews array. // See the documentation of Ember._BindableSpanView for more. var bindView = view.createChildView(Ember._BindableSpanView, { preserveContext: preserveContext, shouldDisplayFunc: shouldDisplay, + valueNormalizerFunc: valueNormalizer, displayTemplate: fn, inverseTemplate: inverse, property: property, previousContext: ctx, isEscaped: options.hash.escaped }); view.appendChild(bindView); - var observer, invoker; - /** @private */ - observer = function(){ - // Double check since sometimes the view gets destroyed after this observer is already queued - if (!get(bindView, 'isDestroyed')) { bindView.rerender(); } + var observer = function() { + Ember.run.once(bindView, 'rerenderIfNeeded'); }; - /** @private */ - invoker = function() { - Ember.run.once(observer); - }; - // Observes the given property on the context and // tells the Ember._BindableSpan 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 (property !== '') { - Ember.addObserver(ctx, property, invoker); + Ember.addObserver(ctx, property, observer); } } else { // The object is not observable, so just render it out and // be done with it. data.buffer.push(getPath(this, property)); } }; /** + '_triageMustache' is used internally select between a binding and helper 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 + @name Handlebars.helpers._triageMustache + @param {String} property Property/helperID to triage + @param {Function} fn Context to provide for rendering + @returns {String} HTML string + */ + EmberHandlebars.registerHelper('_triageMustache', function(property, fn) { + ember_assert("You cannot pass more than one argument to the _triageMustache helper", arguments.length <= 2); + if (helpers[property]) { + return helpers[property].call(this, fn); + } + else { + return helpers.bind.apply(this, arguments); + } + }); + + /** `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`: {{bind "content.title"}} @@ -14332,11 +14852,11 @@ @name Handlebars.helpers.bind @param {String} property Property to bind @param {Function} fn Context to provide for rendering @returns {String} HTML string */ - Ember.Handlebars.registerHelper('bind', function(property, fn) { + EmberHandlebars.registerHelper('bind', function(property, fn) { ember_assert("You cannot pass more than one argument to the bind helper", arguments.length <= 2); var context = (fn.contexts && fn.contexts[0]) || this; return bind.call(context, property, fn, false, function(result) { @@ -14356,66 +14876,67 @@ @name Handlebars.helpers.boundIf @param {String} property Property to bind @param {Function} fn Context to provide for rendering @returns {String} HTML string */ - Ember.Handlebars.registerHelper('boundIf', function(property, fn) { + EmberHandlebars.registerHelper('boundIf', function(property, fn) { var context = (fn.contexts && fn.contexts[0]) || this; - - return bind.call(context, property, fn, true, function(result) { + var func = function(result) { if (Ember.typeOf(result) === 'array') { return get(result, 'length') !== 0; } else { return !!result; } - } ); + }; + + return bind.call(context, property, fn, true, func, func); }); })(); /** @name Handlebars.helpers.with @param {Function} context @param {Hash} options @returns {String} HTML string */ -Ember.Handlebars.registerHelper('with', function(context, options) { +EmberHandlebars.registerHelper('with', function(context, options) { 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 Ember.Handlebars.helpers.bind.call(options.contexts[0], context, options); + return helpers.bind.call(options.contexts[0], context, options); }); /** @name Handlebars.helpers.if @param {Function} context @param {Hash} options @returns {String} HTML string */ -Ember.Handlebars.registerHelper('if', function(context, options) { +EmberHandlebars.registerHelper('if', function(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); - return Ember.Handlebars.helpers.boundIf.call(options.contexts[0], context, options); + return helpers.boundIf.call(options.contexts[0], context, options); }); /** @name Handlebars.helpers.unless @param {Function} context @param {Hash} options @returns {String} HTML string */ -Ember.Handlebars.registerHelper('unless', function(context, options) { +EmberHandlebars.registerHelper('unless', function(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; - return Ember.Handlebars.helpers.boundIf.call(options.contexts[0], context, options); + return helpers.boundIf.call(options.contexts[0], context, options); }); /** `bindAttr` allows you to create a binding between DOM element attributes and Ember objects. For example: @@ -14424,11 +14945,11 @@ @name Handlebars.helpers.bindAttr @param {Hash} options @returns {String} HTML string */ -Ember.Handlebars.registerHelper('bindAttr', function(options) { +EmberHandlebars.registerHelper('bindAttr', function(options) { var attrs = options.hash; ember_assert("You must specify at least one hash argument to bindAttr", !!Ember.keys(attrs).length); @@ -14442,11 +14963,11 @@ var dataId = ++jQuery.uuid; // Handle classes differently, as we can bind multiple classes var classBindings = attrs['class']; if (classBindings !== null && classBindings !== undefined) { - var classResults = Ember.Handlebars.bindClasses(this, classBindings, view, dataId); + var classResults = EmberHandlebars.bindClasses(this, classBindings, view, dataId); ret.push('class="' + classResults.join(' ') + '"'); delete attrs['class']; } var attrKeys = Ember.keys(attrs); @@ -14504,11 +15025,11 @@ } }, this); // Add the unique identifier ret.push('data-bindAttr-' + dataId + '="' + dataId + '"'); - return new Ember.Handlebars.SafeString(ret.join(' ')); + return new EmberHandlebars.SafeString(ret.join(' ')); }); /** 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 @@ -14533,21 +15054,22 @@ @param {Srting} bindAttrId Optional bindAttr id used to lookup elements @returns {Array} An array of class names to add */ -Ember.Handlebars.bindClasses = function(context, classBindings, view, bindAttrId) { +EmberHandlebars.bindClasses = function(context, classBindings, view, bindAttrId) { 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 classStringForProperty = function(property) { var split = property.split(':'), - property = split[0], className = split[1]; + property = split[0]; + var val = getPath(context, property); // If value is a Boolean and true, return the dasherized property // name. if (val === YES) { @@ -14717,11 +15239,11 @@ fn = options.fn, hash = options.hash, newView; if ('string' === typeof path) { - newView = Ember.getPath(thisContext, path); + newView = Ember.Handlebars.getPath(thisContext, path); ember_assert("Unable to find view at path '" + path + "'", !!newView); } else { newView = path; } @@ -14769,11 +15291,11 @@ // License: Licensed under MIT license (see license.js) // ========================================================================== /*globals Handlebars ember_assert */ // TODO: Don't require all of this module -var get = Ember.get, fmt = Ember.String.fmt; +var get = Ember.get, getPath = Ember.Handlebars.getPath, fmt = Ember.String.fmt; /** @name Handlebars.helpers.collection @param {String} path @param {Hash} options @@ -14794,20 +15316,20 @@ var inverse = options.inverse; // If passed a path string, convert that into an object. // Otherwise, just default to the standard class. var collectionClass; - collectionClass = path ? Ember.getPath(this, path) : Ember.CollectionView; + collectionClass = path ? getPath(this, path) : Ember.CollectionView; ember_assert(fmt("%@ #collection: Could not find %@", data.view, path), !!collectionClass); var hash = options.hash, itemHash = {}, match; // Extract item view class if provided else default to the standard class var itemViewClass, itemViewPath = hash.itemViewClass; var collectionPrototype = get(collectionClass, 'proto'); delete hash.itemViewClass; - itemViewClass = itemViewPath ? Ember.getPath(collectionPrototype, itemViewPath) : collectionPrototype.itemViewClass; + itemViewClass = itemViewPath ? getPath(collectionPrototype, itemViewPath) : collectionPrototype.itemViewClass; ember_assert(fmt("%@ #collection: Could not find %@", data.view, itemViewPath), !!itemViewClass); // 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) { @@ -14861,11 +15383,11 @@ // Project: Ember Handlebar Views // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== /*globals Handlebars */ -var getPath = Ember.getPath; +var getPath = Ember.Handlebars.getPath; /** `unbound` allows you to output a property without binding. *Important:* The output will not be updated if the property changes. Use with caution. @@ -14975,10 +15497,68 @@ })({}); (function(exports) { +var EmberHandlebars = Ember.Handlebars, getPath = Ember.Handlebars.getPath; + +var ActionHelper = EmberHandlebars.ActionHelper = {}; + +ActionHelper.registerAction = function(actionName, eventName, target, view) { + var actionId = (++jQuery.uuid).toString(), + existingHandler = view[eventName], + handler; + + if (existingHandler) { + var handler = function(event) { + var ret; + if ($(event.target).closest('[data-ember-action]').attr('data-ember-action') === actionId) { + ret = target[actionName](event); + } + return ret !== false ? existingHandler.call(view, event) : ret; + }; + } else { + var handler = function(event) { + if ($(event.target).closest('[data-ember-action]').attr('data-ember-action') === actionId) { + return target[actionName](event); + } + }; + } + + view[eventName] = handler; + + view.reopen({ + rerender: function() { + if (existingHandler) { + view[eventName] = existingHandler; + } else { + view[eventName] = null; + } + return this._super(); + } + }); + + return actionId; +}; + +EmberHandlebars.registerHelper('action', function(actionName, options) { + var hash = options.hash || {}, + eventName = options.hash.on || "click", + view = options.data.view, + target; + + if (view.isVirtual) { view = view.get('parentView'); } + target = options.hash.target ? getPath(this, options.hash.target) : view; + + var actionId = ActionHelper.registerAction(actionName, eventName, target, view); + return new EmberHandlebars.SafeString('data-ember-action="' + actionId + '"'); +}); + +})({}); + + +(function(exports) { // ========================================================================== // Project: Ember Handlebar Views // Copyright: ©2011 Strobe Inc. and contributors. // License: Licensed under MIT license (see license.js) // ========================================================================== @@ -14998,12 +15578,12 @@ // // Script tags with type="text/html" or "text/x-handlebars" will be compiled // with Ember's Handlebars and are suitable for use as a view's template. // Those with type="text/x-raw-handlebars" will be compiled with regular // Handlebars and are suitable for use in views' computed properties. -Ember.Handlebars.bootstrap = function() { - Ember.$('script[type="text/html"], script[type="text/x-handlebars"], script[type="text/x-raw-handlebars"]') +Ember.Handlebars.bootstrap = function(ctx) { + Ember.$('script[type="text/html"], script[type="text/x-handlebars"], script[type="text/x-raw-handlebars"]', ctx) .each(function() { // Get a reference to the script tag var script = Ember.$(this), compile = (script.attr('type') === 'text/x-raw-handlebars') ? Ember.$.proxy(Handlebars.compile, Handlebars) : @@ -15011,11 +15591,11 @@ // Get the name of the script, used by Ember.View's templateName property. // First look for data-template-name attribute, then fall back to its // id if no name is found. templateName = script.attr('data-template-name') || script.attr('id'), template = compile(script.html()), - view, viewPath; + view, viewPath, tagName; if (templateName) { // For templates which have a name, we save them and then remove them from the DOM Ember.TEMPLATES[templateName] = template; @@ -15036,12 +15616,17 @@ // Users can optionally specify a custom view subclass to use by setting the // data-view attribute of the script tag. viewPath = script.attr('data-view'); view = viewPath ? Ember.getPath(viewPath) : Ember.View; + // Users can optionally specify a custom tag name to use by setting the + // data-tag-name attribute on the script tag. + tagName = script.attr('data-tag-name'); + view = view.create({ - template: template + template: template, + tagName: (tagName) ? tagName : undefined }); view._insertElementLater(function() { script.replaceWith(this.$()); @@ -15050,10 +15635,14 @@ }); } }); }; -Ember.$(document).ready(Ember.Handlebars.bootstrap); +Ember.$(document).ready( + function(){ + Ember.Handlebars.bootstrap( Ember.$(document) ); + } +); })({}); (function(exports) {