Ember.View.create({
attributeBindings: ['type'],
type: 'button'
});
If the value of the property is a Boolean, the name of that property is
added as an attribute.
// Renders something like
Ember.View.create({
attributeBindings: ['enabled'],
enabled: true
});
*/
attributeBindings: [],
state: 'preRender',
// .......................................................
// CORE DISPLAY METHODS
//
/**
@private
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
*/
init: function() {
this._super();
// Register the view for event handling. This hash is used by
// Ember.EventDispatcher to dispatch incoming events.
if (!this.isVirtual) Ember.View.views[get(this, 'elementId')] = this;
// 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());
var viewController = get(this, 'viewController');
if (viewController) {
viewController = get(viewController);
if (viewController) {
set(viewController, 'view', this);
}
}
},
appendChild: function(view, options) {
return this.invokeForState('appendChild', view, options);
},
/**
Removes the child view from the parent view.
@param {Ember.View} view
@returns {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.
@returns {Ember.View} receiver
*/
removeAllChildren: function() {
return this.mutateChildViews(function(view) {
this.removeChild(view);
});
},
destroyAllChildren: function() {
return this.mutateChildViews(function(view) {
view.destroy();
});
},
/**
Removes the view from its parentView, if one is found. Otherwise
does nothing.
@returns {Ember.View} receiver
*/
removeFromParent: function() {
var parent = get(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.
*/
willDestroy: function() {
// calling this._super() will nuke computed properties and observers,
// so collect any information we need before calling super.
var childViews = this._childViews,
parent = get(this, '_parentView'),
childLen;
// destroy the element -- this will avoid each child view destroying
// the element over and over again...
if (!this.removedFromDOM) { this.destroyElement(); }
// remove from non-virtual parent view if viewName was specified
if (this.viewName) {
var nonVirtualParentView = get(this, 'parentView');
if (nonVirtualParentView) {
set(nonVirtualParentView, this.viewName, null);
}
}
// 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.state = 'destroyed';
childLen = childViews.length;
for (var i=childLen-1; i>=0; i--) {
childViews[i].removedFromDOM = true;
childViews[i].destroy();
}
// next remove view from global hash
if (!this.isVirtual) delete Ember.View.views[get(this, 'elementId')];
},
/**
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.
@param {Class} viewClass
@param {Hash} [attrs] Attributes to add
@returns {Ember.View} new instance
@test in createChildViews
*/
createChildView: function(view, attrs) {
if (Ember.View.detect(view)) {
attrs = attrs || {};
attrs._parentView = this;
attrs.templateData = attrs.templateData || get(this, 'templateData');
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 {
Ember.assert('You must pass instance or subclass of View', view instanceof Ember.View);
Ember.assert("You can only pass attributes when a class is provided", !attrs);
if (!get(view, 'templateData')) {
set(view, 'templateData', get(this, 'templateData'));
}
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() {
var $el = this.$();
if (!$el) { return; }
var isVisible = get(this, 'isVisible');
$el.toggle(isVisible);
if (this._isAncestorHidden()) { return; }
if (isVisible) {
this._notifyBecameVisible();
} else {
this._notifyBecameHidden();
}
}, 'isVisible'),
_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(function(view) {
this.buffer = null;
});
},
transitionTo: function(state, children) {
this.state = state;
if (children !== false) {
this.forEachChildView(function(view) {
view.transitionTo(state);
});
}
},
/**
@private
Override the default event firing from Ember.Evented to
also call methods with the given name.
*/
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);
}
},
has: function(name) {
return Ember.typeOf(this[name]) === 'function' || this._super(name);
},
// .......................................................
// EVENT HANDLING
//
/**
@private
Handle events from `Ember.EventDispatcher`
*/
handleEvent: function(eventName, evt) {
return this.invokeForState('handleEvent', eventName, evt);
}
});
/**
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
* 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.
/** @private */
var DOMManager = {
prepend: function(view, childView) {
childView._insertElementLater(function() {
var element = view.$();
element.prepend(childView.$());
});
},
after: function(view, nextView) {
nextView._insertElementLater(function() {
var element = view.$();
element.after(nextView.$());
});
},
replace: function(view) {
var element = get(view, 'element');
set(view, 'element', null);
view._insertElementLater(function() {
Ember.$(element).replaceWith(get(view, 'element'));
});
},
remove: function(view) {
var elem = get(view, 'element');
set(view, 'element', null);
view._lastInsert = null;
Ember.$(elem).remove();
},
empty: function(view) {
view.$().empty();
}
};
Ember.View.reopen({
states: Ember.View.states,
domManager: DOMManager
});
Ember.View.reopenClass({
/**
@private
Parse a path and return an object which holds the parsed properties.
For example a path like "content.isEnabled:enabled:disabled" wil return the
following object:
{
path: "content.isEnabled",
className: "enabled",
falsyClassName: "disabled",
classNames: ":enabled:disabled"
}
*/
_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
};
},
/**
@private
Get the class name for a given value, based on the path, optional className
and optional falsyClassName.
- if the value is truthy and a className is defined, the className is returned
- if the value is true, the dasherized last part of the supplied path is returned
- if the value is false and a falsyClassName is supplied, the falsyClassName is returned
- if the value is truthy, the value is returned
- if none of the above rules apply, null is returned
*/
_classStringForValue: function(path, val, className, falsyClassName) {
// If the value is truthy and we're using the colon syntax,
// we should return the className directly
if (!!val && className) {
return className;
// If value is a Boolean and true, return the dasherized property
// name.
} else if (val === true) {
// catch syntax like isEnabled::not-enabled
if (val === true && !className && falsyClassName) { return null; }
// 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 false and a falsyClassName is specified, return it
} else if (val === false && falsyClassName) {
return falsyClassName;
// If the value is not false, undefined, or null, return the current
// value of the property.
} else if (val !== false && val !== undefined && 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;
}
}
});
// Create a global view 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);
var currentValue = elem.attr(name);
// if this changes, also change the logic in ember-handlebars/lib/helpers/binding.js
if ((type === 'string' || (type === 'number' && !isNaN(value))) && value !== currentValue) {
elem.attr(name, value);
} else if (value && type === 'boolean') {
elem.attr(name, name);
} else if (!value) {
elem.removeAttr(name);
}
};
})();
(function() {
// ==========================================================================
// 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)
// ==========================================================================
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);
view._lastInsert = null;
return view;
}
}
};
Ember.View.reopen({
states: Ember.View.states
});
})();
(function() {
// ==========================================================================
// 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.View.states.preRender = {
parentState: Ember.View.states._default,
// a view leaves the preRender state once its element has been
// created (createElement).
insertElement: function(view, fn) {
if (view._lastInsert !== Ember.guidFor(fn)){
return;
}
view.createElement();
view._notifyWillInsertElement();
// after createElement, the view will be in the hasElement state.
fn.call(view);
view.transitionTo('inDOM');
view._notifyDidInsertElement();
},
empty: Ember.K,
setElement: function(view, value) {
if (value !== null) {
view.transitionTo('hasElement');
}
return value;
}
};
})();
(function() {
// ==========================================================================
// 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)
// ==========================================================================
var get = Ember.get, set = Ember.set, meta = Ember.meta;
Ember.View.states.inBuffer = {
parentState: Ember.View.states._default,
$: 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) {
// Ember.deprecate("Something you did caused a view to re-render after it rendered but before it was inserted into the DOM. Because this is avoidable and the cause of significant performance issues in applications, this behavior is deprecated. If you want to use the debugger to find out what caused this, you can set ENV.RAISE_ON_DEPRECATION to true.");
view._notifyWillRerender();
view.clearRenderedChildren();
view.renderToBuffer(view.buffer, 'replaceWith');
},
// 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;
childView = this.createChildView(childView, options);
view._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();
view._notifyWillDestroyElement();
view.transitionTo('preRender');
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.");
},
// 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;
}
};
})();
(function() {
// ==========================================================================
// 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)
// ==========================================================================
var get = Ember.get, set = Ember.set, meta = Ember.meta;
Ember.View.states.hasElement = {
parentState: Ember.View.states._default,
$: 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._notifyWillRerender();
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);
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
}
}
};
Ember.View.states.inDOM = {
parentState: Ember.View.states.hasElement,
insertElement: function(view, fn) {
if (view._lastInsert !== Ember.guidFor(fn)){
return;
}
throw "You can't insert an element into the DOM that has already been inserted";
}
};
})();
(function() {
// ==========================================================================
// 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)
// ==========================================================================
var destroyedError = "You can't call %@ on a destroyed view", fmt = Ember.String.fmt;
Ember.View.states.destroyed = {
parentState: Ember.View.states._default,
appendChild: function() {
throw fmt(destroyedError, ['appendChild']);
},
rerender: function() {
throw fmt(destroyedError, ['rerender']);
},
destroyElement: function() {
throw fmt(destroyedError, ['destroyElement']);
},
empty: function() {
throw fmt(destroyedError, ['empty']);
},
setElement: function() {
throw fmt(destroyedError, ["set('element', ...)"]);
},
// Since element insertion is scheduled, don't do anything if
// the view has been destroyed between scheduling and execution
insertElement: Ember.K
};
})();
(function() {
// ==========================================================================
// 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)
// ==========================================================================
})();
(function() {
// ==========================================================================
// 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)
// ==========================================================================
var get = Ember.get, set = Ember.set, meta = Ember.meta;
var forEach = Ember.EnumerableUtils.forEach;
var childViewsProperty = Ember.computed(function() {
return get(this, '_childViews');
}).property('_childViews').cacheable();
/**
@class
A `ContainerView` is an `Ember.View` subclass that allows for manual or programatic
management of a view's `childViews` array that will correctly update the `ContainerView`
instance's rendered DOM representation.
## 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`:
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`:
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:
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 views in a container's `childViews` array should be added and removed by manipulating
the `childViews` property directly.
To remove a view pass that view into a `removeObject` call on the container's `childViews` property.
Given an empty `` the following code
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
Removing a view
aContainer.get('childViews') // [aContainer.aView, aContainer.bView]
aContainer.get('childViews').removeObject(aContainer.get('bView'))
aContainer.get('childViews') // [aContainer.aView]
Will result in the following HTML
Similarly, adding a child view is accomplished by adding `Ember.View` instances to the
container's `childViews` property.
Given an empty `` the following code
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
Adding a view
AnotherViewClass = Ember.View.extend({
template: Ember.Handlebars.compile("Another view")
})
aContainer.get('childViews') // [aContainer.aView, aContainer.bView]
aContainer.get('childViews').pushObject(AnotherViewClass.create())
aContainer.get('childViews') // [aContainer.aView, aContainer.bView,
]
Will result in the following HTML
Direct manipulation of childViews presence or absence in the DOM via calls to
`remove` or `removeFromParent` or calls to a container's `removeChild` may not behave
correctly.
Calling `remove()` on a child view will remove the view's HTML, but it will remain as part of its
container's `childView`s property.
Calling `removeChild()` on the container will remove the passed view instance from the container's
`childView`s but keep its HTML within the container's rendered view.
Calling `removeFromParent()` behaves as expected but should be avoided in favor of direct
manipulation of a container's `childViews` property.
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
Calling `aContainer.get('aView').removeFromParent()` will result in the following HTML
And the `Ember.View` instance stored in `aContainer.aView` will be removed from `aContainer`'s
`childViews` array.
## 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.
## Binding a View to Display
If you would like to display a single view in your ContainerView, you can set its `currentView`
property. When the `currentView` property is set to a view instance, it will be added to the
ContainerView's `childViews` array. If the `currentView` property is later changed to a
different view, the new view will replace the old view. If `currentView` is set to `null`, the
last `currentView` will be removed.
This functionality is useful for cases where you want to bind the display of a ContainerView to
a controller or state manager. For example, you can bind the `currentView` of a container to
a controller like this:
// Controller
App.appController = Ember.Object.create({
view: Ember.View.create({
templateName: 'person_template'
})
});
// Handlebars template
{{view Ember.ContainerView currentViewBinding="App.appController.view"}}
@extends Ember.View
*/
Ember.ContainerView = Ember.View.extend({
init: function() {
this._super();
var childViews = get(this, 'childViews');
Ember.defineProperty(this, 'childViews', 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) _childViews.push(this.createChildView(currentView));
// Make the _childViews array observable
Ember.A(_childViews);
// Sets up an array observer on the child views array. This
// observer will detect when child views are added or removed
// and update the DOM to reflect the mutation.
get(this, 'childViews').addArrayObserver(this, {
willChange: 'childViewsWillChange',
didChange: 'childViewsDidChange'
});
},
/**
Instructs each child view to render to the passed render buffer.
@param {Ember.RenderBuffer} buffer the buffer to render to
@private
*/
render: function(buffer) {
this.forEachChildView(function(view) {
view.renderToBuffer(buffer);
});
},
/**
When the container view is destroyed, tear down the child views
array observer.
@private
*/
willDestroy: function() {
get(this, 'childViews').removeArrayObserver(this, {
willChange: 'childViewsWillChange',
didChange: 'childViewsDidChange'
});
this._super();
},
/**
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
@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, start+removed);
this.initializeViews(changedViews, null, null);
this.invokeForState('childViewsWillChange', views, start, removed);
},
/**
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
@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) {
var len = get(views, 'length');
// No new child views were added; bail out.
if (added === 0) return;
var changedViews = views.slice(start, start+added);
this.initializeViews(changedViews, this, get(this, 'templateData'));
// Let the current state handle the changes
this.invokeForState('childViewsDidChange', views, start, added);
},
initializeViews: function(views, parentView, templateData) {
forEach(views, function(view) {
set(view, '_parentView', parentView);
if (!get(view, 'templateData')) {
set(view, 'templateData', templateData);
}
});
},
/**
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
@param {Ember.View} prev the child view after which the specified view should
be inserted
@private
*/
_scheduleInsertion: function(view, prev) {
if (prev) {
prev.domManager.after(prev, view);
} else {
this.domManager.prepend(this, view);
}
},
currentView: null,
_currentViewWillChange: Ember.beforeObserver(function() {
var childViews = get(this, 'childViews'),
currentView = get(this, 'currentView');
if (currentView) {
childViews.removeObject(currentView);
currentView.destroy();
}
}, 'currentView'),
_currentViewDidChange: Ember.observer(function() {
var childViews = get(this, 'childViews'),
currentView = get(this, 'currentView');
if (currentView) {
childViews.pushObject(currentView);
}
}, 'currentView')
});
// Ember.ContainerView extends the default view states to provide different
// behavior for childViewsWillChange and childViewsDidChange.
Ember.ContainerView.states = {
parent: Ember.View.states,
inBuffer: {
childViewsDidChange: function(parentView, views, start, added) {
var buffer = parentView.buffer,
startWith, prev, prevBuffer, view;
// Determine where to begin inserting the child view(s) in the
// render buffer.
if (start === 0) {
// If views were inserted at the beginning, prepend the first
// view to the render buffer, then begin inserting any
// additional views at the beginning.
view = views[start];
startWith = start + 1;
view.renderToBuffer(buffer, 'prepend');
} else {
// Otherwise, just insert them at the same place as the child
// views mutation.
view = views[start - 1];
startWith = start;
}
for (var i=startWith; i` and the following code:
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
the letter: A
the letter: B
the letter: C
## 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 `` and the following code:
anUndorderedListView = Ember.CollectionView.create({
tagName: 'ul',
content: ['A','B','C'],
itemViewClass: Ember.View.extend({
template: Ember.Handlebars.compile("the letter: {{view.content}}")
})
})
anUndorderedListView.appendTo('body')
Will result in the following HTML structure
the letter: A
the letter: B
the letter: C
Additional tagName pairs can be provided by adding to `Ember.CollectionView.CONTAINER_MAP `
Ember.CollectionView.CONTAINER_MAP['article'] = 'section'
## 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.
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
## 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.
## Use in templates via the `{{collection}}` Ember.Handlebars helper
Ember.Handlebars provides a helper specifically for adding `CollectionView`s to templates.
See `Ember.Handlebars.collection` for more details
@since Ember 0.9
@extends Ember.ContainerView
*/
Ember.CollectionView = Ember.ContainerView.extend(
/** @scope Ember.CollectionView.prototype */ {
/**
A list of items to be displayed by the Ember.CollectionView.
@type Ember.Array
@default null
*/
content: null,
/**
@private
This provides metadata about what kind of empty view class this
collection would like if it is being instantiated from another
system (like Handlebars)
*/
emptyViewClass: Ember.View,
/**
An optional view to display if content is set to an empty array.
@type Ember.View
@default null
*/
emptyView: null,
/**
@type Ember.View
@default Ember.View
*/
itemViewClass: Ember.View,
/** @private */
init: function() {
var ret = this._super();
this._contentDidChange();
return ret;
},
_contentWillChange: Ember.beforeObserver(function() {
var content = this.get('content');
if (content) { content.removeArrayObserver(this); }
var len = content ? get(content, 'length') : 0;
this.arrayWillChange(content, 0, len);
}, 'content'),
/**
@private
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.
*/
_contentDidChange: Ember.observer(function() {
var content = get(this, 'content');
if (content) {
Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content));
content.addArrayObserver(this);
}
var len = content ? get(content, 'length') : 0;
this.arrayDidChange(content, 0, null, len);
}, 'content'),
willDestroy: function() {
var content = get(this, 'content');
if (content) { content.removeArrayObserver(this); }
this._super();
},
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 = get(this, 'childViews'), childView, idx, len;
len = get(childViews, 'length');
var removingAll = removedCount === len;
if (removingAll) {
this.invokeForState('empty');
}
for (idx = start + removedCount - 1; idx >= start; idx--) {
childView = childViews[idx];
if (removingAll) { childView.removedFromDOM = true; }
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.
@param {Array} addedObjects
the objects that were added to the content
@param {Array} removedObjects
the objects that were removed from the content
@param {Number} changeIndex
the index at which the changes occurred
*/
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 = get(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++) {
item = content.objectAt(idx);
view = this.createChildView(itemViewClass, {
content: item,
contentIndex: idx
});
addedViews.push(view);
}
} else {
var emptyView = get(this, 'emptyView');
if (!emptyView) { return; }
emptyView = this.createChildView(emptyView);
addedViews.push(emptyView);
set(this, 'emptyView', emptyView);
}
childViews.replace(start, 0, addedViews);
},
createChildView: function(view, attrs) {
view = this._super(view, attrs);
var itemTagName = get(view, 'tagName');
var tagName = (itemTagName === null || itemTagName === undefined) ? Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')] : itemTagName;
set(view, 'tagName', tagName);
return view;
}
});
/**
@static
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.
@type Hash
@constant
*/
Ember.CollectionView.CONTAINER_MAP = {
ul: 'li',
ol: 'li',
table: 'tr',
thead: 'tr',
tbody: 'tr',
tfoot: 'tr',
tr: 'td',
select: 'option'
};
})();
(function() {
// ==========================================================================
// 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)
// ==========================================================================
})();
(function() {
// ==========================================================================
// 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)
// ==========================================================================
/*globals jQuery*/
})();
(function() {
var get = Ember.get, set = Ember.set;
/**
@class
@extends Ember.Object
*/
Ember.State = Ember.Object.extend(Ember.Evented,
/** @scope Ember.State.prototype */{
isState: true,
/**
A reference to the parent state.
@type Ember.State
*/
parentState: null,
start: null,
/**
The name of this state.
@type String
*/
name: null,
/**
The full path to this state.
@type String
@readOnly
*/
path: Ember.computed(function() {
var parentPath = get(this, 'parentState.path'),
path = get(this, 'name');
if (parentPath) {
path = parentPath + '.' + path;
}
return path;
}).property().cacheable(),
/**
@private
Override the default event firing from Ember.Evented to
also call methods with the given name.
*/
trigger: function(name) {
if (this[name]) {
this[name].apply(this, [].slice.call(arguments, 1));
}
this._super.apply(this, arguments);
},
/** @private */
init: function() {
var states = get(this, 'states'), foundStates;
set(this, 'childStates', Ember.A());
set(this, 'eventTransitions', get(this, 'eventTransitions') || {});
var name, value, transitionTarget;
// 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 (name in this) {
if (name === "constructor") { continue; }
if (value = this[name]) {
if (transitionTarget = value.transitionTarget) {
this.eventTransitions[name] = transitionTarget;
}
this.setupChild(states, name, value);
}
}
set(this, 'states', states);
} else {
for (name in states) {
this.setupChild(states, name, states[name]);
}
}
set(this, 'pathsCache', {});
set(this, 'pathsCacheNoContext', {});
},
/** @private */
setupChild: function(states, name, value) {
if (!value) { return false; }
if (value.isState) {
set(value, 'name', name);
} else if (Ember.State.detect(value)) {
value = value.create({
name: name
});
}
if (value.isState) {
set(value, 'parentState', this);
get(this, 'childStates').pushObject(value);
states[name] = value;
}
},
lookupEventTransition: function(name) {
var path, state = this;
while(state && !path) {
path = state.eventTransitions[name];
state = state.get('parentState');
}
return path;
},
/**
A Boolean value indicating whether the state is a leaf state
in the state hierarchy. This is false if the state has child
states; otherwise it is true.
@type Boolean
*/
isLeaf: Ember.computed(function() {
return !get(this, 'childStates').length;
}).cacheable(),
/**
A boolean value indicating whether the state takes a context.
By default we assume all states take contexts.
*/
hasContext: true,
/**
This is the default transition event.
@event
@param {Ember.StateManager} manager
@param context
@see Ember.StateManager#transitionEvent
*/
setup: Ember.K,
/**
This event fires when the state is entered.
@event
@param {Ember.StateManager} manager
*/
enter: Ember.K,
/**
This event fires when the state is exited.
@event
@param {Ember.StateManager} manager
*/
exit: Ember.K
});
var Event = Ember.$ && Ember.$.Event;
Ember.State.reopenClass(
/** @scope Ember.State */{
/**
@static
Creates an action function for transitioning to the named state while preserving context.
The following example StateManagers are equivalent:
aManager = Ember.StateManager.create({
stateOne: Ember.State.create({
changeToStateTwo: Ember.State.transitionTo('stateTwo')
}),
stateTwo: Ember.State.create({})
})
bManager = Ember.StateManager.create({
stateOne: Ember.State.create({
changeToStateTwo: function(manager, context){
manager.transitionTo('stateTwo', context)
}
}),
stateTwo: Ember.State.create({})
})
@param {String} target
*/
transitionTo: function(target) {
var event = function(stateManager, context) {
if (Event && context instanceof Event) {
if (context.hasOwnProperty('context')) {
context = context.context;
} else {
// If we received an event and it doesn't contain
// a context, don't pass along a superfluous
// context to the target of the event.
return stateManager.transitionTo(target);
}
}
stateManager.transitionTo(target, context);
};
event.transitionTarget = target;
return event;
}
});
})();
(function() {
var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
var arrayForEach = Ember.ArrayPolyfills.forEach;
/**
@private
A Transition takes the enter, exit and resolve states and normalizes
them:
* takes any passed in contexts into consideration
* adds in `initialState`s
*/
var Transition = function(raw) {
this.enterStates = raw.enterStates.slice();
this.exitStates = raw.exitStates.slice();
this.resolveState = raw.resolveState;
this.finalState = raw.enterStates[raw.enterStates.length - 1] || raw.resolveState;
};
Transition.prototype = {
/**
@private
Normalize the passed in enter, exit and resolve states.
This process also adds `finalState` and `contexts` to the Transition object.
@param {Ember.StateManager} manager the state manager running the transition
@param {Array} contexts a list of contexts passed into `transitionTo`
*/
normalize: function(manager, contexts) {
this.matchContextsToStates(contexts);
this.addInitialStates();
this.removeUnchangedContexts(manager);
return this;
},
/**
@private
Match each of the contexts passed to `transitionTo` to a state.
This process may also require adding additional enter and exit
states if there are more contexts than enter states.
@param {Array} contexts a list of contexts passed into `transitionTo`
*/
matchContextsToStates: function(contexts) {
var stateIdx = this.enterStates.length - 1,
matchedContexts = [],
state,
context;
// Next, we will match the passed in contexts to the states they
// represent.
//
// First, assign a context to each enter state in reverse order. If
// any contexts are left, add a parent state to the list of states
// to enter and exit, and assign a context to the parent state.
//
// If there are still contexts left when the state manager is
// reached, raise an exception.
//
// This allows the following:
//
// |- root
// | |- post
// | | |- comments
// | |- about (* current state)
//
// For `transitionTo('post.comments', post, post.get('comments')`,
// the first context (`post`) will be assigned to `root.post`, and
// the second context (`post.get('comments')`) will be assigned
// to `root.post.comments`.
//
// For the following:
//
// |- root
// | |- post
// | | |- index (* current state)
// | | |- comments
//
// For `transitionTo('post.comments', otherPost, otherPost.get('comments')`,
// the `` state will be added to the list of enter and exit
// states because its context has changed.
while (contexts.length > 0) {
if (stateIdx >= 0) {
state = this.enterStates[stateIdx--];
} else {
if (this.enterStates.length) {
state = get(this.enterStates[0], 'parentState');
if (!state) { throw "Cannot match all contexts to states"; }
} else {
// If re-entering the current state with a context, the resolve
// state will be the current state.
state = this.resolveState;
}
this.enterStates.unshift(state);
this.exitStates.unshift(state);
}
// in routers, only states with dynamic segments have a context
if (get(state, 'hasContext')) {
context = contexts.pop();
} else {
context = null;
}
matchedContexts.unshift(context);
}
this.contexts = matchedContexts;
},
/**
@private
Add any `initialState`s to the list of enter states.
*/
addInitialStates: function() {
var finalState = this.finalState, initialState;
while(true) {
initialState = get(finalState, 'initialState') || 'start';
finalState = get(finalState, 'states.' + initialState);
if (!finalState) { break; }
this.finalState = finalState;
this.enterStates.push(finalState);
this.contexts.push(undefined);
}
},
/**
@private
Remove any states that were added because the number of contexts
exceeded the number of explicit enter states, but the context has
not changed since the last time the state was entered.
@param {Ember.StateManager} manager passed in to look up the last
context for a states
*/
removeUnchangedContexts: function(manager) {
// Start from the beginning of the enter states. If the state was added
// to the list during the context matching phase, make sure the context
// has actually changed since the last time the state was entered.
while (this.enterStates.length > 0) {
if (this.enterStates[0] !== this.exitStates[0]) { break; }
if (this.enterStates.length === this.contexts.length) {
if (manager.getStateMeta(this.enterStates[0], 'context') !== this.contexts[0]) { break; }
this.contexts.shift();
}
this.resolveState = this.enterStates.shift();
this.exitStates.shift();
}
}
};
/**
@class
StateManager is part of Ember's implementation of a finite state machine. A StateManager
instance manages a number of properties that are instances of `Ember.State`,
tracks the current active state, and triggers callbacks when states have changed.
## Defining States
The states of StateManager can be declared in one of two ways. First, you can define
a `states` property that contains all the states:
managerA = Ember.StateManager.create({
states: {
stateOne: Ember.State.create(),
stateTwo: Ember.State.create()
}
})
managerA.get('states')
// {
// stateOne: Ember.State.create(),
// stateTwo: Ember.State.create()
// }
You can also add instances of `Ember.State` (or an `Ember.State` subclass) directly as properties
of a StateManager. These states will be collected into the `states` property for you.
managerA = Ember.StateManager.create({
stateOne: Ember.State.create(),
stateTwo: Ember.State.create()
})
managerA.get('states')
// {
// stateOne: Ember.State.create(),
// stateTwo: Ember.State.create()
// }
## The Initial State
When created a StateManager instance will immediately enter into the state
defined as its `start` property or the state referenced by name in its
`initialState` property:
managerA = Ember.StateManager.create({
start: Ember.State.create({})
})
managerA.get('currentState.name') // 'start'
managerB = Ember.StateManager.create({
initialState: 'beginHere',
beginHere: Ember.State.create({})
})
managerB.get('currentState.name') // 'beginHere'
Because it is a property you may also provide a computed function if you wish to derive
an `initialState` programmatically:
managerC = Ember.StateManager.create({
initialState: function(){
if (someLogic) {
return 'active';
} else {
return 'passive';
}
}.property(),
active: Ember.State.create({}),
passive: Ember.State.create({})
})
## Moving Between States
A StateManager can have any number of Ember.State objects as properties
and can have a single one of these states as its current state.
Calling `transitionTo` transitions between states:
robotManager = Ember.StateManager.create({
initialState: 'poweredDown',
poweredDown: Ember.State.create({}),
poweredUp: Ember.State.create({})
})
robotManager.get('currentState.name') // 'poweredDown'
robotManager.transitionTo('poweredUp')
robotManager.get('currentState.name') // 'poweredUp'
Before transitioning into a new state the existing `currentState` will have its
`exit` method called with the StateManager instance as its first argument and
an object representing the transition as its second argument.
After transitioning into a new state the new `currentState` will have its
`enter` method called with the StateManager instance as its first argument and
an object representing the transition as its second argument.
robotManager = Ember.StateManager.create({
initialState: 'poweredDown',
poweredDown: Ember.State.create({
exit: function(stateManager){
console.log("exiting the poweredDown state")
}
}),
poweredUp: Ember.State.create({
enter: function(stateManager){
console.log("entering the poweredUp state. Destroy all humans.")
}
})
})
robotManager.get('currentState.name') // 'poweredDown'
robotManager.transitionTo('poweredUp')
// will log
// 'exiting the poweredDown state'
// 'entering the poweredUp state. Destroy all humans.'
Once a StateManager is already in a state, subsequent attempts to enter that state will
not trigger enter or exit method calls. Attempts to transition into a state that the
manager does not have will result in no changes in the StateManager's current state:
robotManager = Ember.StateManager.create({
initialState: 'poweredDown',
poweredDown: Ember.State.create({
exit: function(stateManager){
console.log("exiting the poweredDown state")
}
}),
poweredUp: Ember.State.create({
enter: function(stateManager){
console.log("entering the poweredUp state. Destroy all humans.")
}
})
})
robotManager.get('currentState.name') // 'poweredDown'
robotManager.transitionTo('poweredUp')
// will log
// 'exiting the poweredDown state'
// 'entering the poweredUp state. Destroy all humans.'
robotManager.transitionTo('poweredUp') // no logging, no state change
robotManager.transitionTo('someUnknownState') // silently fails
robotManager.get('currentState.name') // 'poweredUp'
Each state property may itself contain properties that are instances of Ember.State.
The StateManager can transition to specific sub-states in a series of transitionTo method calls or
via a single transitionTo with the full path to the specific state. The StateManager will also
keep track of the full path to its currentState
robotManager = Ember.StateManager.create({
initialState: 'poweredDown',
poweredDown: Ember.State.create({
charging: Ember.State.create(),
charged: Ember.State.create()
}),
poweredUp: Ember.State.create({
mobile: Ember.State.create(),
stationary: Ember.State.create()
})
})
robotManager.get('currentState.name') // 'poweredDown'
robotManager.transitionTo('poweredUp')
robotManager.get('currentState.name') // 'poweredUp'
robotManager.transitionTo('mobile')
robotManager.get('currentState.name') // 'mobile'
// transition via a state path
robotManager.transitionTo('poweredDown.charging')
robotManager.get('currentState.name') // 'charging'
robotManager.get('currentState.path') // 'poweredDown.charging'
Enter transition methods will be called for each state and nested child state in their
hierarchical order. Exit methods will be called for each state and its nested states in
reverse hierarchical order.
Exit transitions for a parent state are not called when entering into one of its child states,
only when transitioning to a new section of possible states in the hierarchy.
robotManager = Ember.StateManager.create({
initialState: 'poweredDown',
poweredDown: Ember.State.create({
enter: function(){},
exit: function(){
console.log("exited poweredDown state")
},
charging: Ember.State.create({
enter: function(){},
exit: function(){}
}),
charged: Ember.State.create({
enter: function(){
console.log("entered charged state")
},
exit: function(){
console.log("exited charged state")
}
})
}),
poweredUp: Ember.State.create({
enter: function(){
console.log("entered poweredUp state")
},
exit: function(){},
mobile: Ember.State.create({
enter: function(){
console.log("entered mobile state")
},
exit: function(){}
}),
stationary: Ember.State.create({
enter: function(){},
exit: function(){}
})
})
})
robotManager.get('currentState.path') // 'poweredDown'
robotManager.transitionTo('charged')
// logs 'entered charged state'
// but does *not* log 'exited poweredDown state'
robotManager.get('currentState.name') // 'charged
robotManager.transitionTo('poweredUp.mobile')
// logs
// 'exited charged state'
// 'exited poweredDown state'
// 'entered poweredUp state'
// 'entered mobile state'
During development you can set a StateManager's `enableLogging` property to `true` to
receive console messages of state transitions.
robotManager = Ember.StateManager.create({
enableLogging: true
})
## Managing currentState with Actions
To control which transitions between states are possible for a given state, StateManager
can receive and route action messages to its states via the `send` method. Calling to `send` with
an action name will begin searching for a method with the same name starting at the current state
and moving up through the parent states in a state hierarchy until an appropriate method is found
or the StateManager instance itself is reached.
If an appropriately named method is found it will be called with the state manager as the first
argument and an optional `context` object as the second argument.
managerA = Ember.StateManager.create({
initialState: 'stateOne.substateOne.subsubstateOne',
stateOne: Ember.State.create({
substateOne: Ember.State.create({
anAction: function(manager, context){
console.log("an action was called")
},
subsubstateOne: Ember.State.create({})
})
})
})
managerA.get('currentState.name') // 'subsubstateOne'
managerA.send('anAction')
// 'stateOne.substateOne.subsubstateOne' has no anAction method
// so the 'anAction' method of 'stateOne.substateOne' is called
// and logs "an action was called"
// with managerA as the first argument
// and no second argument
someObject = {}
managerA.send('anAction', someObject)
// the 'anAction' method of 'stateOne.substateOne' is called again
// with managerA as the first argument and
// someObject as the second argument.
If the StateManager attempts to send an action but does not find an appropriately named
method in the current state or while moving upwards through the state hierarchy
it will throw a new Ember.Error. Action detection only moves upwards through the state hierarchy
from the current state. It does not search in other portions of the hierarchy.
managerB = Ember.StateManager.create({
initialState: 'stateOne.substateOne.subsubstateOne',
stateOne: Ember.State.create({
substateOne: Ember.State.create({
subsubstateOne: Ember.State.create({})
})
}),
stateTwo: Ember.State.create({
anAction: function(manager, context){
// will not be called below because it is
// not a parent of the current state
}
})
})
managerB.get('currentState.name') // 'subsubstateOne'
managerB.send('anAction')
// Error: could not
// respond to event anAction in state stateOne.substateOne.subsubstateOne.
Inside of an action method the given state should delegate `transitionTo` calls on its
StateManager.
robotManager = Ember.StateManager.create({
initialState: 'poweredDown.charging',
poweredDown: Ember.State.create({
charging: Ember.State.create({
chargeComplete: function(manager, context){
manager.transitionTo('charged')
}
}),
charged: Ember.State.create({
boot: function(manager, context){
manager.transitionTo('poweredUp')
}
})
}),
poweredUp: Ember.State.create({
beginExtermination: function(manager, context){
manager.transitionTo('rampaging')
},
rampaging: Ember.State.create()
})
})
robotManager.get('currentState.name') // 'charging'
robotManager.send('boot') // throws error, no boot action
// in current hierarchy
robotManager.get('currentState.name') // remains 'charging'
robotManager.send('beginExtermination') // throws error, no beginExtermination
// action in current hierarchy
robotManager.get('currentState.name') // remains 'charging'
robotManager.send('chargeComplete')
robotManager.get('currentState.name') // 'charged'
robotManager.send('boot')
robotManager.get('currentState.name') // 'poweredUp'
robotManager.send('beginExtermination', allHumans)
robotManager.get('currentState.name') // 'rampaging'
Transition actions can also be created using the `transitionTo` method of the Ember.State class. The
following example StateManagers are equivalent:
aManager = Ember.StateManager.create({
stateOne: Ember.State.create({
changeToStateTwo: Ember.State.transitionTo('stateTwo')
}),
stateTwo: Ember.State.create({})
})
bManager = Ember.StateManager.create({
stateOne: Ember.State.create({
changeToStateTwo: function(manager, context){
manager.transitionTo('stateTwo', context)
}
}),
stateTwo: Ember.State.create({})
})
**/
Ember.StateManager = Ember.State.extend(
/** @scope Ember.StateManager.prototype */ {
/**
When creating a new statemanager, look for a default state to transition
into. This state can either be named `start`, or can be specified using the
`initialState` property.
*/
init: function() {
this._super();
set(this, 'stateMeta', Ember.Map.create());
var initialState = get(this, 'initialState');
if (!initialState && get(this, 'states.start')) {
initialState = 'start';
}
if (initialState) {
this.transitionTo(initialState);
Ember.assert('Failed to transition to initial state "' + initialState + '"', !!get(this, 'currentState'));
}
},
stateMetaFor: function(state) {
var meta = get(this, 'stateMeta'),
stateMeta = meta.get(state);
if (!stateMeta) {
stateMeta = {};
meta.set(state, stateMeta);
}
return stateMeta;
},
setStateMeta: function(state, key, value) {
return set(this.stateMetaFor(state), key, value);
},
getStateMeta: function(state, key) {
return get(this.stateMetaFor(state), key);
},
/**
The current state from among the manager's possible states. This property should
not be set directly. Use `transitionTo` to move between states by name.
@type Ember.State
@readOnly
*/
currentState: null,
/**
The name of transitionEvent that this stateManager will dispatch
@property {String}
@default 'setup'
*/
transitionEvent: 'setup',
/**
If set to true, `errorOnUnhandledEvents` will cause an exception to be
raised if you attempt to send an event to a state manager that is not
handled by the current state or any of its parent states.
@type Boolean
@default true
*/
errorOnUnhandledEvent: true,
send: function(event, context) {
Ember.assert('Cannot send event "' + event + '" while currentState is ' + get(this, 'currentState'), get(this, 'currentState'));
return this.sendRecursively(event, get(this, 'currentState'), context);
},
sendRecursively: function(event, currentState, context) {
var log = this.enableLogging,
action = currentState[event];
// Test to see if the action is a method that
// can be invoked. Don't blindly check just for
// existence, because it is possible the state
// manager has a child state of the given name,
// and we should still raise an exception in that
// case.
if (typeof action === 'function') {
if (log) { Ember.Logger.log(fmt("STATEMANAGER: Sending event '%@' to state %@.", [event, get(currentState, 'path')])); }
return action.call(currentState, this, context);
} else {
var parentState = get(currentState, 'parentState');
if (parentState) {
return this.sendRecursively(event, parentState, context);
} else if (get(this, 'errorOnUnhandledEvent')) {
throw new Ember.Error(this.toString() + " could not respond to event " + event + " in state " + get(this, 'currentState.path') + ".");
}
}
},
/**
Finds a state by its state path.
Example:
manager = Ember.StateManager.create({
root: Ember.State.create({
dashboard: Ember.State.create()
})
});
manager.getStateByPath(manager, "root.dashboard")
// returns the dashboard state
@param {Ember.State} root the state to start searching from
@param {String} path the state path to follow
@returns {Ember.State} the state at the end of the path
*/
getStateByPath: function(root, path) {
var parts = path.split('.'),
state = root;
for (var i=0, l=parts.length; i`, an attempt to
// transition to `comments.show` will match ``.
//
// First, this code will look for root.posts.show.comments.show.
// Next, it will look for root.posts.comments.show. Finally,
// it will look for `root.comments.show`, and find the state.
//
// After this process, the following variables will exist:
//
// * resolveState: a common parent state between the current
// and target state. In the above example, `` is the
// `resolveState`.
// * enterStates: a list of all of the states represented
// by the path from the `resolveState`. For example, for
// the path `root.comments.show`, `enterStates` would have
// `[, ]`
// * exitStates: a list of all of the states from the
// `resolveState` to the `currentState`. In the above
// example, `exitStates` would have
// `[`, `]`.
while (resolveState && !enterStates) {
exitStates.unshift(resolveState);
resolveState = get(resolveState, 'parentState');
if (!resolveState) {
enterStates = this.findStatesByPath(this, path);
if (!enterStates) {
Ember.assert('Could not find state for path: "'+path+'"');
return;
}
}
enterStates = this.findStatesByPath(resolveState, path);
}
// If the path contains some states that are parents of both the
// current state and the target state, remove them.
//
// For example, in the following hierarchy:
//
// |- root
// | |- post
// | | |- index (* current)
// | | |- show
//
// If the `path` is `root.post.show`, the three variables will
// be:
//
// * resolveState: ``
// * enterStates: `[, , ]`
// * exitStates: `[, , ]`
//
// The goal of this code is to remove the common states, so we
// have:
//
// * resolveState: ``
// * enterStates: `[]`
// * exitStates: `[]`
//
// This avoid unnecessary calls to the enter and exit transitions.
while (enterStates.length > 0 && enterStates[0] === exitStates[0]) {
resolveState = enterStates.shift();
exitStates.shift();
}
// Cache the enterStates, exitStates, and resolveState for the
// current state and the `path`.
var transitions = currentState.pathsCache[path] = {
exitStates: exitStates,
enterStates: enterStates,
resolveState: resolveState
};
return transitions;
},
triggerSetupContext: function(transitions) {
var contexts = transitions.contexts,
offset = transitions.enterStates.length - contexts.length,
enterStates = transitions.enterStates,
transitionEvent = get(this, 'transitionEvent');
Ember.assert("More contexts provided than states", offset >= 0);
arrayForEach.call(enterStates, function(state, idx) {
state.trigger(transitionEvent, this, contexts[idx-offset]);
}, this);
},
getState: function(name) {
var state = get(this, name),
parentState = get(this, 'parentState');
if (state) {
return state;
} else if (parentState) {
return parentState.getState(name);
}
},
enterState: function(transition) {
var log = this.enableLogging;
var exitStates = transition.exitStates.slice(0).reverse();
arrayForEach.call(exitStates, function(state) {
state.trigger('exit', this);
}, this);
arrayForEach.call(transition.enterStates, function(state) {
if (log) { Ember.Logger.log("STATEMANAGER: Entering " + get(state, 'path')); }
state.trigger('enter', this);
}, this);
set(this, 'currentState', transition.finalState);
}
});
})();
(function() {
// ==========================================================================
// Project: Ember Statecharts
// Copyright: ©2011 Living Social Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
})();
(function() {
var get = Ember.get;
Ember._ResolvedState = Ember.Object.extend({
manager: null,
state: null,
match: null,
object: Ember.computed(function(key, value) {
if (arguments.length === 2) {
this._object = value;
return value;
} else {
if (this._object) {
return this._object;
} else {
var state = get(this, 'state'),
match = get(this, 'match'),
manager = get(this, 'manager');
return state.deserialize(manager, match.hash);
}
}
}).property(),
hasPromise: Ember.computed(function() {
return Ember.canInvoke(get(this, 'object'), 'then');
}).property('object'),
promise: Ember.computed(function() {
var object = get(this, 'object');
if (Ember.canInvoke(object, 'then')) {
return object;
} else {
return {
then: function(success) { success(object); }
};
}
}).property('object'),
transition: function() {
var manager = get(this, 'manager'),
path = get(this, 'state.path'),
object = get(this, 'object');
manager.transitionTo(path, object);
}
});
})();
(function() {
var get = Ember.get;
// The Ember Routable mixin assumes the existance of a simple
// routing shim that supports the following three behaviors:
//
// * .getURL() - this is called when the page loads
// * .setURL(newURL) - this is called from within the state
// manager when the state changes to a routable state
// * .onURLChange(callback) - this happens when the user presses
// the back or forward button
var paramForClass = function(classObject) {
var className = classObject.toString(),
parts = className.split("."),
last = parts[parts.length - 1];
return Ember.String.underscore(last) + "_id";
};
var merge = function(original, hash) {
for (var prop in hash) {
if (!hash.hasOwnProperty(prop)) { continue; }
if (original.hasOwnProperty(prop)) { continue; }
original[prop] = hash[prop];
}
};
/**
@class
@extends Ember.Mixin
*/
Ember.Routable = Ember.Mixin.create({
init: function() {
var redirection;
this.on('connectOutlets', this, this.stashContext);
if (redirection = get(this, 'redirectsTo')) {
Ember.assert("You cannot use `redirectsTo` if you already have a `connectOutlets` method", this.connectOutlets === Ember.K);
this.connectOutlets = function(router) {
router.transitionTo(redirection);
};
}
// normalize empty route to '/'
var route = get(this, 'route');
if (route === '') {
route = '/';
}
this._super();
Ember.assert("You cannot use `redirectsTo` on a state that has child states", !redirection || (!!redirection && !!get(this, 'isLeaf')));
},
/**
@private
Whenever a routable state is entered, the context it was entered with
is stashed so that we can regenerate the state's `absoluteURL` on
demand.
*/
stashContext: function(manager, context) {
var serialized = this.serialize(manager, context);
Ember.assert('serialize must return a hash', !serialized || typeof serialized === 'object');
manager.setStateMeta(this, 'context', context);
manager.setStateMeta(this, 'serialized', serialized);
if (get(this, 'isRoutable') && !get(manager, 'isRouting')) {
this.updateRoute(manager, get(manager, 'location'));
}
},
/**
@private
Whenever a routable state is entered, the router's location object
is notified to set the URL to the current absolute path.
In general, this will update the browser's URL.
*/
updateRoute: function(manager, location) {
if (get(this, 'isLeafRoute')) {
var path = this.absoluteRoute(manager);
location.setURL(path);
}
},
/**
@private
Get the absolute route for the current state and a given
hash.
This method is private, as it expects a serialized hash,
not the original context object.
*/
absoluteRoute: function(manager, hash) {
var parentState = get(this, 'parentState');
var path = '', generated;
// If the parent state is routable, use its current path
// as this route's prefix.
if (get(parentState, 'isRoutable')) {
path = parentState.absoluteRoute(manager, hash);
}
var matcher = get(this, 'routeMatcher'),
serialized = manager.getStateMeta(this, 'serialized');
// merge the existing serialized object in with the passed
// in hash.
hash = hash || {};
merge(hash, serialized);
generated = matcher && matcher.generate(hash);
if (generated) {
path = path + '/' + generated;
}
return path;
},
/**
@private
At the moment, a state is routable if it has a string `route`
property. This heuristic may change.
*/
isRoutable: Ember.computed(function() {
return typeof get(this, 'route') === 'string';
}).cacheable(),
/**
@private
Determine if this is the last routeable state
*/
isLeafRoute: Ember.computed(function() {
if (get(this, 'isLeaf')) { return true; }
return !get(this, 'childStates').findProperty('isRoutable');
}).cacheable(),
/**
@private
A _RouteMatcher object generated from the current route's `route`
string property.
*/
routeMatcher: Ember.computed(function() {
var route = get(this, 'route');
if (route) {
return Ember._RouteMatcher.create({ route: route });
}
}).cacheable(),
/**
@private
Check whether the route has dynamic segments and therefore takes
a context.
*/
hasContext: Ember.computed(function() {
var routeMatcher = get(this, 'routeMatcher');
if (routeMatcher) {
return routeMatcher.identifiers.length > 0;
}
}).cacheable(),
/**
@private
The model class associated with the current state. This property
uses the `modelType` property, in order to allow it to be
specified as a String.
*/
modelClass: Ember.computed(function() {
var modelType = get(this, 'modelType');
if (typeof modelType === 'string') {
return Ember.get(window, modelType);
} else {
return modelType;
}
}).cacheable(),
/**
@private
Get the model class for the state. The heuristic is:
* The state must have a single dynamic segment
* The dynamic segment must end in `_id`
* A dynamic segment like `blog_post_id` is converted into `BlogPost`
* The name is then looked up on the passed in namespace
The process of initializing an application with a router will
pass the application's namespace into the router, which will be
used here.
*/
modelClassFor: function(namespace) {
var modelClass, routeMatcher, identifiers, match, className;
// if an explicit modelType was specified, use that
if (modelClass = get(this, 'modelClass')) { return modelClass; }
// if the router has no lookup namespace, we won't be able to guess
// the modelType
if (!namespace) { return; }
// make sure this state is actually a routable state
routeMatcher = get(this, 'routeMatcher');
if (!routeMatcher) { return; }
// only guess modelType for states with a single dynamic segment
// (no more, no fewer)
identifiers = routeMatcher.identifiers;
if (identifiers.length !== 2) { return; }
// extract the `_id` from the end of the dynamic segment; if the
// dynamic segment does not end in `_id`, we can't guess the
// modelType
match = identifiers[1].match(/^(.*)_id$/);
if (!match) { return; }
// convert the underscored type into a class form and look it up
// on the router's namespace
className = Ember.String.classify(match[1]);
return get(namespace, className);
},
/**
The default method that takes a `params` object and converts
it into an object.
By default, a params hash that looks like `{ post_id: 1 }`
will be looked up as `namespace.Post.find(1)`. This is
designed to work seamlessly with Ember Data, but will work
fine with any class that has a `find` method.
*/
deserialize: function(manager, params) {
var modelClass, routeMatcher, param;
if (modelClass = this.modelClassFor(get(manager, 'namespace'))) {
Ember.assert("Expected "+modelClass.toString()+" to implement `find` for use in '"+this.get('path')+"' `deserialize`. Please implement the `find` method or overwrite `deserialize`.", modelClass.find);
return modelClass.find(params[paramForClass(modelClass)]);
}
return params;
},
/**
The default method that takes an object and converts it into
a params hash.
By default, if there is a single dynamic segment named
`blog_post_id` and the object is a `BlogPost` with an
`id` of `12`, the serialize method will produce:
{ blog_post_id: 12 }
*/
serialize: function(manager, context) {
var modelClass, routeMatcher, namespace, param, id;
if (Ember.empty(context)) { return ''; }
if (modelClass = this.modelClassFor(get(manager, 'namespace'))) {
param = paramForClass(modelClass);
id = get(context, 'id');
context = {};
context[param] = id;
}
return context;
},
/**
@private
*/
resolvePath: function(manager, path) {
if (get(this, 'isLeafRoute')) { return Ember.A(); }
var childStates = get(this, 'childStates'), match;
childStates = Ember.A(childStates.filterProperty('isRoutable'));
childStates = childStates.sort(function(a, b) {
var aDynamicSegments = get(a, 'routeMatcher.identifiers.length'),
bDynamicSegments = get(b, 'routeMatcher.identifiers.length'),
aRoute = get(a, 'route'),
bRoute = get(b, 'route');
if (aRoute.indexOf(bRoute) === 0) {
return -1;
} else if (bRoute.indexOf(aRoute) === 0) {
return 1;
}
if (aDynamicSegments !== bDynamicSegments) {
return aDynamicSegments - bDynamicSegments;
}
return get(b, 'route.length') - get(a, 'route.length');
});
var state = childStates.find(function(state) {
var matcher = get(state, 'routeMatcher');
if (match = matcher.match(path)) { return true; }
});
Ember.assert("Could not find state for path " + path, !!state);
var resolvedState = Ember._ResolvedState.create({
manager: manager,
state: state,
match: match
});
var states = state.resolvePath(manager, match.remaining);
return Ember.A([resolvedState]).pushObjects(states);
},
/**
@private
Once `unroute` has finished unwinding, `routePath` will be called
with the remainder of the route.
For example, if you were in the /posts/1/comments state, and you
moved into the /posts/2/comments state, `routePath` will be called
on the state whose path is `/posts` with the path `/2/comments`.
*/
routePath: function(manager, path) {
if (get(this, 'isLeafRoute')) { return; }
var resolvedStates = this.resolvePath(manager, path),
hasPromises = resolvedStates.some(function(s) { return get(s, 'hasPromise'); });
function runTransition() {
resolvedStates.forEach(function(rs) { rs.transition(); });
}
if (hasPromises) {
manager.transitionTo('loading');
Ember.assert('Loading state should be the child of a route', Ember.Routable.detect(get(manager, 'currentState.parentState')));
Ember.assert('Loading state should not be a route', !Ember.Routable.detect(get(manager, 'currentState')));
manager.handleStatePromises(resolvedStates, runTransition);
} else {
runTransition();
}
},
/**
@private
When you move to a new route by pressing the back
or forward button, this method is called first.
Its job is to move the state manager into a parent
state of the state it will eventually move into.
*/
unroutePath: function(router, path) {
var parentState = get(this, 'parentState');
// If we're at the root state, we're done
if (parentState === router) {
return;
}
path = path.replace(/^(?=[^\/])/, "/");
var absolutePath = this.absoluteRoute(router);
var route = get(this, 'route');
// If the current path is empty, move up one state,
// because the index ('/') state must be a leaf node.
if (route !== '/') {
// If the current path is a prefix of the path we're trying
// to go to, we're done.
var index = path.indexOf(absolutePath),
next = path.charAt(absolutePath.length);
if (index === 0 && (next === "/" || next === "")) {
return;
}
}
// Transition to the parent and call unroute again.
router.enterState({
exitStates: [this],
enterStates: [],
finalState: parentState
});
router.send('unroutePath', path);
},
/**
The `connectOutlets` event will be triggered once a
state has been entered. It will be called with the
route's context.
*/
connectOutlets: Ember.K,
/**
The `navigateAway` event will be triggered when the
URL changes due to the back/forward button
*/
navigateAway: Ember.K
});
})();
(function() {
/**
@class
@extends Ember.Routable
*/
Ember.Route = Ember.State.extend(Ember.Routable);
})();
(function() {
var escapeForRegex = function(text) {
return text.replace(/[\-\[\]{}()*+?.,\\\^\$|#\s]/g, "\\$&");
};
Ember._RouteMatcher = Ember.Object.extend({
state: null,
init: function() {
var route = this.route,
identifiers = [],
count = 1,
escaped;
// Strip off leading slash if present
if (route.charAt(0) === '/') {
route = this.route = route.substr(1);
}
escaped = escapeForRegex(route);
var regex = escaped.replace(/:([a-z_]+)(?=$|\/)/gi, function(match, id) {
identifiers[count++] = id;
return "([^/]+)";
});
this.identifiers = identifiers;
this.regex = new RegExp("^/?" + regex);
},
match: function(path) {
var match = path.match(this.regex);
if (match) {
var identifiers = this.identifiers,
hash = {};
for (var i=1, l=identifiers.length; i 0 ? hash : null
};
}
},
generate: function(hash) {
var identifiers = this.identifiers, route = this.route, id;
for (var i=1, l=identifiers.length; i
Will delegate `click` events on the rendered `h1` to the application's router instance. In this case the
`anActionOnTheRouter` method of the state at 'root.aRoute' will be called with the view's controller
as the context argument. This context will be passed to the `connectOutlets` as its second argument.
Different `context` can be supplied from within the `{{action}}` helper, allowing specific context passing
between application states:
See Handlebars.helpers.action for additional usage examples.
## Changing View Hierarchy in Response To State Change
Changes in application state that change the URL should be accompanied by associated changes in view
hierarchy. This can be accomplished by calling 'connectOutlet' on the injected controller singletons from
within the 'connectOutlets' event of an Ember.Route:
App = Ember.Application.create({
OneController: Ember.ObjectController.extend(),
OneView: Ember.View.extend(),
AnotherController: Ember.ObjectController.extend(),
AnotherView: Ember.View.extend(),
Router: Ember.Router.extend({
root: Ember.Route.extend({
aRoute: Ember.Route.extend({
route: '/',
connectOutlets: function(router, context) {
router.get('oneController').connectOutlet('another');
},
})
})
})
});
App.initialize();
This will detect the '{{outlet}}' portion of `oneController`'s view (an instance of `App.OneView`) and
fill it with a rendered instance of `App.AnotherView` whose `context` will be the single instance of
`App.AnotherController` stored on the router in the `anotherController` property.
For more information about Outlets, see `Ember.Handlebars.helpers.outlet`. For additional information on
the `connectOutlet` method, see `Ember.Controller.connectOutlet`. For more information on
controller injections, see `Ember.Application#initialize()`. For additional information about view context,
see `Ember.View`.
@extends Ember.StateManager
*/
Ember.Router = Ember.StateManager.extend(
/** @scope Ember.Router.prototype */ {
/**
@property {String}
@default 'root'
*/
initialState: 'root',
/**
The `Ember.Location` implementation to be used to manage the application
URL state. The following values are supported:
* 'hash': Uses URL fragment identifiers (like #/blog/1) for routing.
* 'none': Does not read or set the browser URL, but still allows for
routing to happen. Useful for testing.
@type String
@default 'hash'
*/
location: 'hash',
/**
This is only used when a history location is used so that applications that
don't live at the root of the domain can append paths to their root.
@type String
@default '/'
*/
rootURL: '/',
/**
On router, transitionEvent should be called connectOutlets
@property {String}
@default 'connectOutlets'
*/
transitionEvent: 'connectOutlets',
transitionTo: function() {
this.abortRoutingPromises();
this._super.apply(this, arguments);
},
route: function(path) {
this.abortRoutingPromises();
set(this, 'isRouting', true);
var routableState;
try {
path = path.replace(/^(?=[^\/])/, "/");
this.send('navigateAway');
this.send('unroutePath', path);
routableState = get(this, 'currentState');
while (routableState && !routableState.get('isRoutable')) {
routableState = get(routableState, 'parentState');
}
var currentURL = routableState ? routableState.absoluteRoute(this) : '';
var rest = path.substr(currentURL.length);
this.send('routePath', rest);
} finally {
set(this, 'isRouting', false);
}
routableState = get(this, 'currentState');
while (routableState && !routableState.get('isRoutable')) {
routableState = get(routableState, 'parentState');
}
if (routableState) {
routableState.updateRoute(this, get(this, 'location'));
}
},
urlFor: function(path, hash) {
var currentState = get(this, 'currentState') || this,
state = this.findStateByPath(currentState, path);
Ember.assert(Ember.String.fmt("Could not find route with path '%@'", [path]), !!state);
Ember.assert("To get a URL for a state, it must have a `route` property.", !!get(state, 'routeMatcher'));
var location = get(this, 'location'),
absoluteRoute = state.absoluteRoute(this, hash);
return location.formatURL(absoluteRoute);
},
urlForEvent: function(eventName) {
var contexts = Array.prototype.slice.call(arguments, 1);
var currentState = get(this, 'currentState');
var targetStateName = currentState.lookupEventTransition(eventName);
Ember.assert(Ember.String.fmt("You must specify a target state for event '%@' in order to link to it in the current state '%@'.", [eventName, get(currentState, 'path')]), !!targetStateName);
var targetState = this.findStateByPath(currentState, targetStateName);
Ember.assert("Your target state name " + targetStateName + " for event " + eventName + " did not resolve to a state", !!targetState);
var hash = this.serializeRecursively(targetState, contexts, {});
return this.urlFor(targetStateName, hash);
},
/** @private */
serializeRecursively: function(state, contexts, hash) {
var parentState,
context = get(state, 'hasContext') ? contexts.pop() : null;
merge(hash, state.serialize(this, context));
parentState = state.get("parentState");
if (parentState && parentState instanceof Ember.Route) {
return this.serializeRecursively(parentState, contexts, hash);
} else {
return hash;
}
},
abortRoutingPromises: function() {
if (this._routingPromises) {
this._routingPromises.abort();
this._routingPromises = null;
}
},
/**
@private
*/
handleStatePromises: function(states, complete) {
this.abortRoutingPromises();
this.set('isLocked', true);
var manager = this;
this._routingPromises = Ember._PromiseChain.create({
promises: states.slice(),
successCallback: function() {
manager.set('isLocked', false);
complete();
},
failureCallback: function() {
throw "Unable to load object";
},
promiseSuccessCallback: function(item, args) {
set(item, 'object', args[0]);
},
abortCallback: function() {
manager.set('isLocked', false);
}
}).start();
},
/** @private */
init: function() {
this._super();
var location = get(this, 'location'),
rootURL = get(this, 'rootURL');
if ('string' === typeof location) {
set(this, 'location', Ember.Location.create({
implementation: location,
rootURL: rootURL
}));
}
},
/** @private */
willDestroy: function() {
get(this, 'location').destroy();
}
});
})();
(function() {
// ==========================================================================
// Project: Ember Routing
// Copyright: ©2012 Tilde Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
})();
(function() {
var get = Ember.get;
Ember.StateManager.reopen(
/** @scope Ember.StateManager.prototype */ {
/**
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.
@type Ember.View
*/
currentView: Ember.computed(function() {
var currentState = get(this, 'currentState'),
view;
while (currentState) {
// TODO: Remove this when view state is removed
if (get(currentState, 'isViewState')) {
view = get(currentState, 'view');
if (view) { return view; }
}
currentState = get(currentState, 'parentState');
}
return null;
}).property('currentState').cacheable()
});
})();
(function() {
var get = Ember.get, set = Ember.set;
/**
@class
@deprecated
Ember.ViewState extends Ember.State to control the presence of a childView within a
container based on the current state of the ViewState's StateManager.
## Interactions with Ember's View System.
When combined with instances of `Ember.StateManager`, ViewState is designed to
interact with Ember's view system to control which views are added to
and removed from the DOM based on the manager's current state.
By default, a StateManager will manage views inside the 'body' element. This can be
customized by setting the `rootElement` property to a CSS selector of an existing
HTML element you would prefer to receive view rendering.
viewStates = Ember.StateManager.create({
rootElement: '#some-other-element'
})
You can also specify a particular instance of `Ember.ContainerView` you would like to receive
view rendering by setting the `rootView` property. You will be responsible for placing
this element into the DOM yourself.
aLayoutView = Ember.ContainerView.create()
// make sure this view instance is added to the browser
aLayoutView.appendTo('body')
App.viewStates = Ember.StateManager.create({
rootView: aLayoutView
})
Once you have an instance of StateManager controlling a view, you can provide states
that are instances of `Ember.ViewState`. When the StateManager enters a state
that is an instance of `Ember.ViewState` that `ViewState`'s `view` property will be
instantiated and inserted into the StateManager's `rootView` or `rootElement`.
When a state is exited, the `ViewState`'s view will be removed from the StateManager's
view.
ContactListView = Ember.View.extend({
classNames: ['my-contacts-css-class'],
template: Ember.Handlebars.compile('People ')
})
PhotoListView = Ember.View.extend({
classNames: ['my-photos-css-class'],
template: Ember.Handlebars.compile('Photos ')
})
viewStates = Ember.StateManager.create({
showingPeople: Ember.ViewState.create({
view: ContactListView
}),
showingPhotos: Ember.ViewState.create({
view: PhotoListView
})
})
viewStates.transitionTo('showingPeople')
The above code will change the rendered HTML from
to
People
Changing the current state via `transitionTo` from `showingPeople` to
`showingPhotos` will remove the `showingPeople` view and add the `showingPhotos` view:
viewStates.transitionTo('showingPhotos')
will change the rendered HTML to
Photos
When entering nested `ViewState`s, each state's view will be draw into the the StateManager's
`rootView` or `rootElement` as siblings.
ContactListView = Ember.View.extend({
classNames: ['my-contacts-css-class'],
template: Ember.Handlebars.compile('People ')
})
EditAContactView = Ember.View.extend({
classNames: ['editing-a-contact-css-class'],
template: Ember.Handlebars.compile('Editing...')
})
viewStates = Ember.StateManager.create({
showingPeople: Ember.ViewState.create({
view: ContactListView,
withEditingPanel: Ember.ViewState.create({
view: EditAContactView
})
})
})
viewStates.transitionTo('showingPeople.withEditingPanel')
Will result in the following rendered HTML:
People
Editing...
ViewState views are added and removed from their StateManager's view via their
`enter` and `exit` methods. If you need to override these methods, be sure to call
`_super` to maintain the adding and removing behavior:
viewStates = Ember.StateManager.create({
aState: Ember.ViewState.create({
view: Ember.View.extend({}),
enter: function(manager){
// calling _super ensures this view will be
// properly inserted
this._super(manager);
// now you can do other things
}
})
})
## Managing Multiple Sections of A Page With States
Multiple StateManagers can be combined to control multiple areas of an application's rendered views.
Given the following HTML body:
You could separately manage view state for each section with two StateManagers
navigationStates = Ember.StateManager.create({
rootElement: '#sidebar-nav',
userAuthenticated: Em.ViewState.create({
view: Ember.View.extend({})
}),
userNotAuthenticated: Em.ViewState.create({
view: Ember.View.extend({})
})
})
contentStates = Ember.StateManager.create({
rootElement: '#content-area',
books: Em.ViewState.create({
view: Ember.View.extend({})
}),
music: Em.ViewState.create({
view: Ember.View.extend({})
})
})
If you prefer to start with an empty body and manage state programmatically you
can also take advantage of StateManager's `rootView` property and the ability of
`Ember.ContainerView`s to manually manage their child views.
dashboard = Ember.ContainerView.create({
childViews: ['navigationAreaView', 'contentAreaView'],
navigationAreaView: Ember.ContainerView.create({}),
contentAreaView: Ember.ContainerView.create({})
})
navigationStates = Ember.StateManager.create({
rootView: dashboard.get('navigationAreaView'),
userAuthenticated: Em.ViewState.create({
view: Ember.View.extend({})
}),
userNotAuthenticated: Em.ViewState.create({
view: Ember.View.extend({})
})
})
contentStates = Ember.StateManager.create({
rootView: dashboard.get('contentAreaView'),
books: Em.ViewState.create({
view: Ember.View.extend({})
}),
music: Em.ViewState.create({
view: Ember.View.extend({})
})
})
dashboard.appendTo('body')
## User Manipulation of State via `{{action}}` Helpers
The Handlebars `{{action}}` helper is StateManager-aware and will use StateManager action sending
to connect user interaction to action-based state transitions.
Given the following body and handlebars template
And application code
App = Ember.Application.create()
App.appStates = Ember.StateManager.create({
initialState: 'aState',
aState: Ember.State.create({
anAction: function(manager, context){}
}),
bState: Ember.State.create({})
})
A user initiated click or touch event on "Go" will trigger the 'anAction' method of
`App.appStates.aState` with `App.appStates` as the first argument and a
`jQuery.Event` object as the second object. The `jQuery.Event` will include a property
`view` that references the `Ember.View` object that was interacted with.
**/
Ember.ViewState = Ember.State.extend(
/** @scope Ember.ViewState.prototype */ {
isViewState: true,
init: function() {
Ember.deprecate("Ember.ViewState is deprecated and will be removed from future releases. Consider using the outlet pattern to display nested views instead. For more information, see http://emberjs.com/guides/outlets/.");
return this._super();
},
enter: function(stateManager) {
var view = get(this, 'view'), root, childViews;
if (view) {
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) {
// 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() {
// ==========================================================================
// Project: Ember Statecharts
// Copyright: ©2011 Living Social Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
})();
(function() {
// ==========================================================================
// Project: metamorph
// Copyright: ©2011 My Company Inc. All rights reserved.
// ==========================================================================
(function(window) {
var K = function(){},
guid = 0,
document = window.document,
// Feature-detect the W3C range API, the extended check is for IE9 which only partially supports ranges
supportsRange = ('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 = (function(){
var testEl = document.createElement('div');
testEl.innerHTML = "
";
testEl.firstChild.innerHTML = "";
return testEl.firstChild.innerHTML === '';
})();
// 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() {
return "";
};
endTagFunc = function() {
return "";
};
// 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);
};
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, "", " " ],
fieldset: [ 1, "", " " ],
table: [ 1, "" ],
tbody: [ 2, "" ],
tr: [ 3, "" ],
colgroup: [ 2, "" ],
map: [ 1, "", " " ],
_default: [ 0, "", "" ]
};
/**
* 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');
element.innerHTML = 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 = "";
* 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 . Other browsers create it
* before the first node, no matter what.
*
* This means the the following code:
*
* div = document.createElement("div");
* div.innerHTML = "
*
* 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);
// 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());
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.");
}
};
window.Metamorph = Metamorph;
})(this);
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
/*globals Handlebars */
var objectCreate = Ember.create;
/**
@namespace
@name Handlebars
@private
*/
/**
@namespace
@name Handlebars.helpers
@description Helpers for Handlebars templates
*/
Ember.assert("Ember Handlebars requires Handlebars 1.0.beta.5 or greater", window.Handlebars && window.Handlebars.VERSION.match(/^1\.0\.beta\.[56789]$|^1\.0\.rc\.[123456789]+/));
/**
@class
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.
*/
Ember.Handlebars = objectCreate(Handlebars);
Ember.Handlebars.helpers = objectCreate(Handlebars.helpers);
/**
Override the the opcode compiler and JavaScript compiler for Handlebars.
@private
*/
Ember.Handlebars.Compiler = function() {};
Ember.Handlebars.Compiler.prototype = objectCreate(Handlebars.Compiler.prototype);
Ember.Handlebars.Compiler.prototype.compiler = Ember.Handlebars.Compiler;
/** @private */
Ember.Handlebars.JavaScriptCompiler = function() {};
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
*/
Ember.Handlebars.JavaScriptCompiler.prototype.appendToBuffer = function(string) {
return "data.buffer.push("+string+");";
};
/**
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
*/
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(['_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.
@param {String} string The template to precompile
*/
Ember.Handlebars.precompile = function(string) {
var ast = Handlebars.parse(string);
var options = {
knownHelpers: {
action: true,
unbound: true,
bindAttr: 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);
};
/**
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
*/
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);
return Handlebars.template(templateSpec);
};
/**
If a path starts with a reserved keyword, returns the root
that should be used.
@private
*/
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.
@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.getPath = function(root, path, options) {
var data = options && options.data,
normalizedPath = normalizePath(root, path, data),
value;
// In cases where the path begins with a keyword, change the
// root to the value represented by that keyword, and ensure
// the path is relative to it.
root = normalizedPath.root;
path = normalizedPath.path;
value = Ember.get(root, path);
if (value === undefined && root !== window && Ember.isGlobalPath(path)) {
value = Ember.get(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
track down where the problem is happening.
@name Handlebars.helpers.helperMissing
@param {String} path
@param {Hash} options
*/
Ember.Handlebars.registerHelper('helperMissing', function(path, options) {
var error, view = "";
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]));
});
})();
(function() {
Ember.String.htmlSafe = function(str) {
return new Handlebars.SafeString(str);
};
var htmlSafe = Ember.String.htmlSafe;
if (Ember.EXTEND_PROTOTYPES) {
/**
@see Ember.String.htmlSafe
*/
String.prototype.htmlSafe = function() {
return htmlSafe(this);
};
}
})();
(function() {
/*jshint newcap:false*/
var set = Ember.set, get = Ember.get;
var DOMManager = {
remove: function(view) {
var morph = view.morph;
if (morph.isRemoved()) { return; }
set(view, 'element', null);
view._lastInsert = null;
morph.remove();
},
prepend: function(view, childView) {
childView._insertElementLater(function() {
var morph = view.morph;
morph.prepend(childView.outerHTML);
childView.outerHTML = null;
});
},
after: function(view, nextView) {
nextView._insertElementLater(function() {
var morph = view.morph;
morph.after(nextView.outerHTML);
nextView.outerHTML = null;
});
},
replace: function(view) {
var morph = view.morph;
view.transitionTo('preRender');
view.clearRenderedChildren();
var buffer = view.renderToBuffer();
Ember.run.schedule('render', this, function() {
if (get(view, 'isDestroyed')) { return; }
view.invalidateRecursively('element');
view._notifyWillInsertElement();
morph.replaceWith(buffer.string());
view.transitionTo('inDOM');
view._notifyDidInsertElement();
});
},
empty: function(view) {
view.morph.html("");
}
};
// The `morph` and `outerHTML` properties are internal only
// and not observable.
Ember._Metamorph = Ember.Mixin.create({
isVirtual: true,
tagName: '',
init: function() {
this._super();
this.morph = Metamorph();
},
beforeRender: function(buffer) {
buffer.push(this.morph.startTag());
},
afterRender: function(buffer) {
buffer.push(this.morph.endTag());
},
createElement: function() {
var buffer = this.renderToBuffer();
this.outerHTML = buffer.string();
this.clearBuffer();
},
domManager: DOMManager
});
Ember._MetamorphView = Ember.View.extend(Ember._Metamorph);
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// 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.Handlebars.getPath;
/**
@ignore
@private
@class
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.
*/
Ember._HandlebarsBoundView = Ember._MetamorphView.extend({
/** @scope Ember._HandlebarsBoundView.prototype */
/**
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.
@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`.
@type Boolean
@default false
*/
preserveContext: false,
/**
If `preserveContext` is true, this is the object that will be used
to render the template.
@type Object
*/
previousContext: null,
/**
The template to render when `shouldDisplayFunc` evaluates to true.
@type Function
@default null
*/
displayTemplate: null,
/**
The template to render when `shouldDisplayFunc` evaluates to false.
@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.
@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.
@type Object
*/
pathRoot: null,
normalizedValue: Ember.computed(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 = getPath(pathRoot, path, { data: templateData });
}
return valueNormalizer ? valueNormalizer(result) : result;
}).property('path', 'pathRoot', 'valueNormalizerFunc').volatile(),
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 `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`.
@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 inverseTemplate = get(this, 'inverseTemplate'),
displayTemplate = get(this, 'displayTemplate');
var result = get(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) {
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() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
var getPath = Ember.Handlebars.getPath, normalizePath = Ember.Handlebars.normalizePath;
var forEach = Ember.ArrayPolyfills.forEach;
var EmberHandlebars = Ember.Handlebars, helpers = EmberHandlebars.helpers;
// 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.
/** @private */
function bind(property, options, preserveContext, shouldDisplay, valueNormalizer) {
var data = options.data,
fn = options.fn,
inverse = options.inverse,
view = data.view,
currentContext = this,
pathRoot, path, normalized;
normalized = normalizePath(currentContext, property, data);
pathRoot = normalized.root;
path = normalized.path;
// Set up observers for observable objects
if ('object' === typeof this) {
// 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: path,
pathRoot: pathRoot,
previousContext: currentContext,
isEscaped: !options.hash.unescaped,
templateData: options.data
});
view.appendChild(bindView);
/** @private */
var observer = function() {
Ember.run.once(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 (path !== '') {
Ember.addObserver(pathRoot, path, observer);
}
} else {
// The object is not observable, so just render it out and
// be done with it.
data.buffer.push(getPath(pathRoot, path, options));
}
}
/**
'_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"}}
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
@name Handlebars.helpers.bind
@param {String} property Property to bind
@param {Function} fn Context to provide for rendering
@returns {String} HTML string
*/
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) {
return !Ember.none(result);
});
});
/**
Use the `boundIf` helper to create a conditional that re-evaluates
whenever the bound value changes.
{{#boundIf "content.shouldDisplayTitle"}}
{{content.title}}
{{/boundIf}}
@private
@name Handlebars.helpers.boundIf
@param {String} property Property to bind
@param {Function} fn Context to provide for rendering
@returns {String} HTML string
*/
EmberHandlebars.registerHelper('boundIf', function(property, fn) {
var context = (fn.contexts && fn.contexts[0]) || this;
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
*/
EmberHandlebars.registerHelper('with', function(context, options) {
if (arguments.length === 4) {
var keywordName, path, rootPath, normalized;
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);
if (Ember.isGlobalPath(path)) {
Ember.bind(options.data.keywords, keywordName, 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);
options.data.keywords[contextKey] = rootPath;
// if the path is '' ("this"), just bind directly to the current context
var contextPath = path ? contextKey + '.' + path : contextKey;
Ember.bind(options.data.keywords, keywordName, contextPath);
}
return bind.call(this, path, options.fn, true, function(result) {
return !Ember.none(result);
});
} 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);
}
});
/**
@name Handlebars.helpers.if
@param {Function} context
@param {Hash} options
@returns {String} HTML string
*/
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 helpers.boundIf.call(options.contexts[0], context, options);
});
/**
@name Handlebars.helpers.unless
@param {Function} context
@param {Hash} options
@returns {String} HTML string
*/
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 helpers.boundIf.call(options.contexts[0], context, options);
});
/**
`bindAttr` allows you to create a binding between DOM element attributes and
Ember objects. For example:
@name Handlebars.helpers.bindAttr
@param {Hash} options
@returns {String} HTML string
*/
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);
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 && classBindings !== undefined) {
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],
pathRoot, normalized;
Ember.assert(fmt("You must provide a String for a bound attribute, not %@", [path]), typeof path === 'string');
normalized = normalizePath(ctx, path, options.data);
pathRoot = normalized.root;
path = normalized.path;
var value = (path === 'this') ? pathRoot : getPath(pathRoot, 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;
/** @private */
observer = function observer() {
var result = getPath(pathRoot, 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(pathRoot, path, invoker);
return;
}
Ember.View.applyAttributeBindings(elem, attr, result);
};
/** @private */
invoker = function() {
Ember.run.once(observer);
};
// 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.
if (path !== 'this') {
Ember.addObserver(pathRoot, path, invoker);
}
// 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(' '));
});
/**
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.
@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
@returns {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 = getPath(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.
/** @private */
observer = function() {
// Get the current value of the property
newClass = classStringForPath(pathRoot, 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;
}
}
};
/** @private */
invoker = function() {
Ember.run.once(observer);
};
if (path !== '' && path !== 'this') {
Ember.addObserver(pathRoot, path, invoker);
}
// 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(pathRoot, 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() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
/*globals Handlebars */
// TODO: Don't require the entire module
var get = Ember.get, set = Ember.set;
var PARENT_VIEW_PATH = /^parentView\./;
var EmberHandlebars = Ember.Handlebars;
var VIEW_PRESERVES_CONTEXT = Ember.VIEW_PRESERVES_CONTEXT;
/** @private */
EmberHandlebars.ViewHelper = Ember.Object.create({
propertiesFromHTMLOptions: function(options, thisContext) {
var hash = options.hash, data = options.data;
var extensions = {},
classes = hash['class'],
dup = false;
if (hash.id) {
extensions.elementId = hash.id;
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['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="bindingContext.isGreen:green"
var parsedPath = Ember.View._parsePropertyPath(full);
path = this.contextualizeBindingPath(parsedPath.path, data);
if (path) { extensions.classNameBindings[b] = path + parsedPath.classNames; }
}
}
}
// Make the current template context available to the view
// for the bindings set up above.
extensions.bindingContext = thisContext;
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') {
return 'bindingContext';
} else {
return 'bindingContext.' + path;
}
},
helper: function(thisContext, path, options) {
var inverse = options.inverse,
data = options.data,
view = data.view,
fn = options.fn,
hash = options.hash,
newView;
if ('string' === typeof path) {
newView = EmberHandlebars.getPath(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 class to the #view helper, not %@ (%@)', [path, newView]), Ember.View.detect(newView));
var viewOptions = this.propertiesFromHTMLOptions(options, thisContext);
var currentView = data.view;
viewOptions.templateData = options.data;
if (fn) {
Ember.assert("You cannot provide a template block if you also specified a templateName", !get(viewOptions, 'templateName') && !get(newView.proto(), '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 (VIEW_PRESERVES_CONTEXT && !newView.proto().controller && !newView.proto().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 `` and the following template:
Will result in HTML structure:
A span:
Hello.
### 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.
aView = Ember.View.create({
template: Ember.Handlebars.compile("{{#view}} my parent: {{parentView.elementId}} {{/view}}")
})
aView.appendTo('body')
Will result in HTML structure:
### 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`.
Results in the following HTML structure:
hello.
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:
Results in the following HTML structure:
hello.
### 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.
The first argument can also be a relative path. Ember will search for the view class
starting at the `Ember.View` of the template where `{{view}}` was used as the root object:
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 "innerViewClass"}} hi {{/view}}')
})
MyApp.OuterView.create().appendTo('body')
Will result in the following HTML:
### 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.
### 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.
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
@name Handlebars.helpers.view
@param {String} path
@param {Hash} options
@returns {String} HTML string
*/
EmberHandlebars.registerHelper('view', function(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() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
/*globals Handlebars */
// TODO: Don't require all of this module
var get = Ember.get, getPath = Ember.Handlebars.getPath, fmt = Ember.String.fmt;
/**
`{{collection}}` is a `Ember.Handlebars` helper for adding instances of
`Ember.CollectionView` to a template. See `Ember.CollectionView` 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 `` the following template:
And the following application code
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
### Blockless Use
If you provide an `itemViewClass` option that has its own `template` you can omit
the block.
The following template:
And application code
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 {{content.name}}")
})
Will result in the HTML structure below
Greetings Dave
Greetings Mary
Greetings Sara
### 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:
### 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):
Will result in the following HTML structure:
Howdy Dave
Howdy Mary
Howdy Sara
@name Handlebars.helpers.collection
@param {String} path
@param {Hash} options
@returns {String} HTML string
*/
Ember.Handlebars.registerHelper('collection', function(path, options) {
// 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;
// If passed a path string, convert that into an object.
// Otherwise, just default to the standard class.
var collectionClass;
collectionClass = path ? getPath(this, path, options) : Ember.CollectionView;
Ember.assert(fmt("%@ #collection: Could not find collection class %@", [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 = collectionClass.proto();
delete hash.itemViewClass;
itemViewClass = itemViewPath ? getPath(collectionPrototype, itemViewPath, options) : collectionPrototype.itemViewClass;
Ember.assert(fmt("%@ #collection: Could not find itemViewClass %@", [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) {
if (hash.hasOwnProperty(prop)) {
match = prop.match(/^item(.)(.*)$/);
if(match) {
// 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];
}
}
}
var tagName = hash.tagName || collectionPrototype.tagName;
if (fn) {
itemHash.template = fn;
delete options.fn;
}
var emptyViewClass;
if (inverse && inverse !== Handlebars.VM.noop) {
emptyViewClass = get(collectionPrototype, 'emptyViewClass');
emptyViewClass = emptyViewClass.extend({
template: inverse,
tagName: itemHash.tagName
});
} else if (hash.emptyViewClass) {
emptyViewClass = getPath(this, hash.emptyViewClass, options);
}
hash.emptyView = emptyViewClass;
if (hash.eachHelper === 'each') {
itemHash._context = Ember.computed(function() {
return get(this, 'content');
}).property('content');
delete hash.eachHelper;
}
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() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
/*globals Handlebars */
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.
{{unbound somePropertyThatDoesntChange}}
@name Handlebars.helpers.unbound
@param {String} property
@returns {String} HTML string
*/
Ember.Handlebars.registerHelper('unbound', function(property, fn) {
var context = (fn.contexts && fn.contexts[0]) || this;
return getPath(context, property, fn);
});
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
/*jshint debug:true*/
var getPath = Ember.Handlebars.getPath, normalizePath = Ember.Handlebars.normalizePath;
/**
`log` allows you to output the value of a value in the current rendering
context.
{{log myVariable}}
@name Handlebars.helpers.log
@param {String} property
*/
Ember.Handlebars.registerHelper('log', function(property, options) {
var context = (options.contexts && options.contexts[0]) || this,
normalized = normalizePath(context, property, options.data),
pathRoot = normalized.root,
path = normalized.path,
value = (path === 'this') ? pathRoot : getPath(pathRoot, path, options);
Ember.Logger.log(value);
});
/**
The `debugger` helper executes the `debugger` statement in the current
context.
{{debugger}}
@name Handlebars.helpers.debugger
@param {String} property
*/
Ember.Handlebars.registerHelper('debugger', function() {
debugger;
});
})();
(function() {
var get = Ember.get, set = Ember.set;
Ember.Handlebars.EachView = Ember.CollectionView.extend(Ember._Metamorph, {
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');
if (keyword) {
var data = get(view, 'templateData');
data = Ember.copy(data);
data.keywords = view.cloneKeywords();
set(view, 'templateData', data);
var content = get(view, 'content');
// In this case, we do not bind, because the `content` of
// a #each item cannot change.
data.keywords[keyword] = content;
}
return view;
}
});
Ember.Handlebars.registerHelper('each', function(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;
} else {
options.hash.eachHelper = 'each';
}
Ember.assert("You must pass a block to the each helper", options.fn && options.fn !== Handlebars.VM.noop);
options.hash.contentBinding = path;
// Set up emptyView as a metamorph with no tag
//options.hash.emptyViewClass = Ember._MetamorphView;
return Ember.Handlebars.helpers.collection.call(this, 'Ember.Handlebars.EachView', options);
});
})();
(function() {
/**
`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:
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:
Ember.TEMPLATES["my_cool_template"] = Ember.Handlebars.compile('{{user}} ');
@name Handlebars.helpers.template
@param {String} templateName the template to render
*/
Ember.Handlebars.registerHelper('template', function(name, options) {
var template = Ember.TEMPLATES[name];
Ember.assert("Unable to find template with name '"+name+"'.", !!template);
Ember.TEMPLATES[name](this, { data: options.data });
});
})();
(function() {
var EmberHandlebars = Ember.Handlebars,
getPath = EmberHandlebars.getPath,
get = Ember.get,
a_slice = Array.prototype.slice;
var ActionHelper = EmberHandlebars.ActionHelper = {
registeredActions: {}
};
ActionHelper.registerAction = function(actionName, options) {
var actionId = (++Ember.$.uuid).toString();
ActionHelper.registeredActions[actionId] = {
eventName: options.eventName,
handler: function(event) {
var modifier = event.shiftKey || event.metaKey || event.altKey || event.ctrlKey,
secondaryClick = event.which > 1, // IE9 may return undefined
nonStandard = modifier || secondaryClick;
if (options.link && nonStandard) {
// Allow the browser to handle special link clicks normally
return;
}
event.preventDefault();
event.view = options.view;
if (options.hasOwnProperty('context')) {
event.context = options.context;
}
if (options.hasOwnProperty('contexts')) {
event.contexts = options.contexts;
}
var target = options.target;
// Check for StateManager (or compatible object)
if (target.isState && typeof target.send === 'function') {
return target.send(actionName, event);
} else {
Ember.assert(Ember.String.fmt('Target %@ does not have action %@', [target, actionName]), target[actionName]);
return target[actionName].call(target, event);
}
}
};
options.view.on('willRerender', function() {
delete ActionHelper.registeredActions[actionId];
});
return actionId;
};
/**
The `{{action}}` helper registers an HTML element within a template for
DOM event handling and forwards that interaction to the Application's router,
the template's `Ember.View` instance, or supplied `target` option (see 'Specifiying a Target').
User interaction with that element will invoke the supplied action name on
the appropriate target.
Given the following Handlebars template on the page
And application code
AView = Ember.View.extend({
templateName; 'a-template',
anActionName: function(event){}
});
aView = AView.create();
aView.appendTo('body');
Will results in the following rendered HTML
Clicking "click me" will trigger the `anActionName` method of the `aView`
object with a `jQuery.Event` object as its argument. The `jQuery.Event`
object will be extended to include a `view` property that is set to the
original view interacted with (in this case the `aView` object).
### Event Propagation
Events triggered through the action helper will automatically have
`.preventDefault()` called on them. You do not need to do so in your event
handlers. To stop propagation of the event, simply return `false` from your
handler.
If you need the default handler to trigger you should either register your
own event handler, or use event methods on your view class. See Ember.View
'Responding to Browser Events' for more information.
### Specifying DOM event type
By default the `{{action}}` helper registers for DOM `click` events. You can
supply an `on` option to the helper to specify a different DOM event name:
See Ember.View 'Responding to Browser Events' for a list of
acceptable DOM event names.
Because `{{action}}` depends on Ember's event dispatch system it will only
function if an `Ember.EventDispatcher` instance is available. An
`Ember.EventDispatcher` instance will be created when a new
`Ember.Application` is created. Having an instance of `Ember.Application`
will satisfy this requirement.
### Specifying a Target
There are several possible target objects for `{{action}}` helpers:
In a typical `Ember.Router`-backed Application where views are managed
through use of the `{{outlet}}` helper, actions will be forwarded to the
current state of the Applications's Router. See Ember.Router 'Responding
to User-initiated Events' for more information.
If you manaully set the `target` property on the controller of a template's
`Ember.View` instance, the specifed `controller.target` will become the target
for any actions. Likely custom values for a controller's `target` are the
controller itself or a StateManager other than the Application's Router.
If the templates's view lacks a controller property the view itself is the target.
Finally, a `target` option can be provided to the helper to change which object
will receive the method call. This option must be a string representing a
path to an object:
Clicking "click me" in the rendered HTML of the above template will trigger
the `anActionName` method of the object at `MyApplication.someObject`.
The first argument to this method will be a `jQuery.Event` extended to
include a `view` property that is set to the original view interacted with.
A path relative to the template's `Ember.View` instance can also be used as
a target:
Clicking "click me" in the rendered HTML of the above template will trigger
the `anActionName` method of the view's parent view.
The `{{action}}` helper is `Ember.StateManager` aware. If the target of the
action is an `Ember.StateManager` instance `{{action}}` will use the `send`
functionality of StateManagers. The documentation for `Ember.StateManager`
has additional information about this use.
If an action's target does not implement a method that matches the supplied
action name an error will be thrown.
With the following application code
AView = Ember.View.extend({
templateName; 'a-template',
// note: no method 'aMethodNameThatIsMissing'
anActionName: function(event){}
});
aView = AView.create();
aView.appendTo('body');
Will throw `Uncaught TypeError: Cannot call method 'call' of undefined` when
"click me" is clicked.
### Specifying a context
By default the `{{action}}` helper passes the current Handlebars context
along in the `jQuery.Event` object. You may specify an alternate object to
pass as the context by providing a property path:
@name Handlebars.helpers.action
@param {String} actionName
@param {Object...} contexts
@param {Hash} options
*/
EmberHandlebars.registerHelper('action', function(actionName) {
var options = arguments[arguments.length - 1],
contexts = a_slice.call(arguments, 1, -1);
var hash = options.hash,
view = options.data.view,
target, controller, link;
// create a hash to pass along to registerAction
var action = {
eventName: hash.on || "click"
};
action.view = view = get(view, 'concreteView');
if (hash.target) {
target = getPath(this, hash.target, options);
} else if (controller = options.data.keywords.controller) {
target = get(controller, 'target');
}
action.target = target = target || view;
if (contexts.length) {
action.contexts = contexts = Ember.EnumerableUtils.map(contexts, function(context) {
return getPath(this, context, options);
}, this);
action.context = contexts[0];
}
var output = [], url;
if (hash.href && target.urlForEvent) {
url = target.urlForEvent.apply(target, [actionName].concat(contexts));
output.push('href="' + url + '"');
action.link = true;
}
var actionId = ActionHelper.registerAction(actionName, action);
output.push('data-ember-action="' + actionId + '"');
return new EmberHandlebars.SafeString(output.join(" "));
});
})();
(function() {
var get = Ember.get, set = Ember.set;
/**
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 `` and the following application code:
AView = Ember.View.extend({
classNames: ['a-view-with-layout'],
layout: Ember.Handlebars.compile('{{ yield }}
'),
template: Ember.Handlebars.compile('I am wrapped ')
})
aView = AView.create()
aView.appendTo('body')
Will result in the following HTML output:
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.
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
@name Handlebars.helpers.yield
@param {Hash} options
@returns {String} HTML string
*/
Ember.Handlebars.registerHelper('yield', function(options) {
var view = options.data.view, template;
while (view && !get(view, 'layout')) {
view = get(view, 'parentView');
}
Ember.assert("You called yield in a template that was not a layout", !!view);
template = get(view, 'template');
if (template) { template(this, options); }
});
})();
(function() {
/**
The `outlet` helper allows you to specify that the current
view's controller will fill in the view for a given area.
{{outlet}}
By default, when the the current controller's `view`
property changes, the outlet will replace its current
view with the new view.
controller.set('view', someView);
You can also specify a particular name, other than view:
{{outlet masterView}}
{{outlet detailView}}
Then, you can control several outlets from a single
controller:
controller.set('masterView', postsView);
controller.set('detailView', postView);
@name Handlebars.helpers.outlet
@param {String} property the property on the controller
that holds the view for this outlet
*/
Ember.Handlebars.registerHelper('outlet', function(property, options) {
if (property && property.data && property.data.isRenderData) {
options = property;
property = 'view';
}
options.hash.currentViewBinding = "controller." + property;
return Ember.Handlebars.helpers.view.call(this, Ember.ContainerView, options);
});
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
var set = Ember.set, get = Ember.get;
/**
@class
Creates an HTML input of type 'checkbox' with HTML related properties
applied directly to the input.
{{view Ember.Checkbox classNames="applicaton-specific-checkbox"}}
You can add a `label` tag yourself in the template where the Ember.Checkbox is being used.
Some Title
{{view Ember.Checkbox classNames="applicaton-specific-checkbox"}}
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`'s layout section for more information.
@extends Ember.View
*/
Ember.Checkbox = Ember.View.extend({
classNames: ['ember-checkbox'],
tagName: 'input',
attributeBindings: ['type', 'checked', 'disabled', 'tabindex'],
type: "checkbox",
checked: false,
disabled: false,
init: function() {
this._super();
this.on("change", this, this._updateElementValue);
},
/**
@private
*/
_updateElementValue: function() {
set(this, 'checked', this.$().prop('checked'));
}
});
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
var get = Ember.get, set = Ember.set;
/** @class */
Ember.TextSupport = Ember.Mixin.create(
/** @scope Ember.TextSupport.prototype */ {
value: "",
attributeBindings: ['placeholder', 'disabled', 'maxlength', 'tabindex'],
placeholder: null,
disabled: false,
maxlength: null,
insertNewline: Ember.K,
cancel: Ember.K,
/** @private */
init: function() {
this._super();
this.on("focusOut", this, this._elementValueDidChange);
this.on("change", this, this._elementValueDidChange);
this.on("keyUp", this, this.interpretKeyEvents);
},
/**
@private
*/
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());
}
});
Ember.TextSupport.KEY_EVENTS = {
13: 'insertNewline',
27: 'cancel'
};
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
var get = Ember.get, set = Ember.set;
/**
@class
The `Ember.TextField` view class renders a text
[input](https://developer.mozilla.org/en/HTML/Element/Input) element. It
allows for binding Ember properties to the text field contents (`value`),
live-updating as the user inputs text.
Example:
{{view Ember.TextField valueBinding="firstName"}}
## Layout and LayoutName properties
Because HTML `input` elements are self closing `layout` and `layoutName` properties will
not be applied. See `Ember.View`'s layout section for more information.
@extends Ember.View
@extends Ember.TextSupport
*/
Ember.TextField = Ember.View.extend(Ember.TextSupport,
/** @scope Ember.TextField.prototype */ {
classNames: ['ember-text-field'],
tagName: "input",
attributeBindings: ['type', 'value', 'size'],
/**
The value attribute of the input element. As the user inputs text, this
property is updated live.
@type String
@default ""
*/
value: "",
/**
The type attribute of the input element.
@type String
@default "text"
*/
type: "text",
/**
The size of the text field in characters.
@type String
@default null
*/
size: null
});
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
var get = Ember.get, set = Ember.set;
Ember.Button = Ember.View.extend(Ember.TargetActionSupport, {
classNames: ['ember-button'],
classNameBindings: ['isActive'],
tagName: 'button',
propagateEvents: false,
attributeBindings: ['type', 'disabled', 'href', 'tabindex'],
/** @private
Overrides TargetActionSupport's targetObject computed
property to use Handlebars-specific path resolution.
*/
targetObject: Ember.computed(function() {
var target = get(this, 'target'),
root = get(this, 'context'),
data = get(this, 'templateData');
if (typeof target !== 'string') { return target; }
return Ember.Handlebars.getPath(root, target, { data: data });
}).property('target').cacheable(),
// 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;
}
return get(this, 'propagateEvents');
},
mouseLeave: function() {
if (this._mouseDown) {
set(this, 'isActive', false);
this._mouseEntered = false;
}
},
mouseEnter: function() {
if (this._mouseDown) {
set(this, 'isActive', true);
this._mouseEntered = true;
}
},
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) {
return this.mouseDown(touch);
},
touchEnd: function(touch) {
return this.mouseUp(touch);
},
init: function() {
Ember.deprecate("Ember.Button is deprecated and will be removed from future releases. Consider using the `{{action}}` helper.");
this._super();
}
});
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
var get = Ember.get, set = Ember.set;
/**
@class
The `Ember.TextArea` view class renders a
[textarea](https://developer.mozilla.org/en/HTML/Element/textarea) element.
It allows for binding Ember properties to the text area contents (`value`),
live-updating as the user inputs text.
## 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`'s layout section for more information.
@extends Ember.View
@extends Ember.TextSupport
*/
Ember.TextArea = Ember.View.extend(Ember.TextSupport,
/** @scope Ember.TextArea.prototype */ {
classNames: ['ember-text-area'],
tagName: "textarea",
attributeBindings: ['rows', 'cols'],
rows: null,
cols: null,
_updateElementValue: Ember.observer(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);
}
}, 'value'),
/** @private */
init: function() {
this._super();
this.on("didInsertElement", this, this._updateElementValue);
}
});
})();
(function() {
Ember.TabContainerView = Ember.View.extend({
init: function() {
Ember.deprecate("Ember.TabContainerView is deprecated and will be removed from future releases.");
this._super();
}
});
})();
(function() {
var get = Ember.get;
Ember.TabPaneView = Ember.View.extend({
tabsContainer: Ember.computed(function() {
return this.nearestInstanceOf(Ember.TabContainerView);
}).property().volatile(),
isVisible: Ember.computed(function() {
return get(this, 'viewName') === get(this, 'tabsContainer.currentView');
}).property('tabsContainer.currentView').volatile(),
init: function() {
Ember.deprecate("Ember.TabPaneView is deprecated and will be removed from future releases.");
this._super();
}
});
})();
(function() {
var get = Ember.get, setPath = Ember.setPath;
Ember.TabView = Ember.View.extend({
tabsContainer: Ember.computed(function() {
return this.nearestInstanceOf(Ember.TabContainerView);
}).property().volatile(),
mouseUp: function() {
setPath(this, 'tabsContainer.currentView', get(this, 'value'));
},
init: function() {
Ember.deprecate("Ember.TabView is deprecated and will be removed from future releases.");
this._super();
}
});
})();
(function() {
})();
(function() {
/*jshint eqeqeq:false */
var set = Ember.set, get = Ember.get;
var indexOf = Ember.EnumerableUtils.indexOf, indexesOf = Ember.EnumerableUtils.indexesOf;
/**
@class
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 selected option(s)
are updated live in the `selection` property, while the corresponding value
is updated in the `value` property.
### Using Strings
The simplest version of an Ember.Select takes an array of strings for the options
of a select box and a valueBinding to set the value.
Example:
App.controller = Ember.Object.create({
selected: null,
content: [
"Yehuda",
"Tom"
]
})
{{view Ember.Select
contentBinding="App.controller.content"
valueBinding="App.controller.selected"
}}
Would result in the following HTML:
Yehuda
Tom
Selecting Yehuda from the select box will set `App.controller.selected` to "Yehuda"
### Using Objects
An Ember.Select can also take an array of JS or Ember objects.
When using objects you need to supply optionLabelPath and optionValuePath parameters
which will be used to get the label and value for each of the options.
Usually you will bind to either the selection or the value attribute of the select.
Use selectionBinding if you would like to set the whole object as a property on the target.
Use valueBinding if you would like to set just the value.
Example using selectionBinding:
App.controller = Ember.Object.create({
selectedPerson: null,
selectedPersonId: null,
content: [
Ember.Object.create({firstName: "Yehuda", id: 1}),
Ember.Object.create({firstName: "Tom", id: 2})
]
})
{{view Ember.Select
contentBinding="App.controller.content"
optionLabelPath="content.firstName"
optionValuePath="content.id"
selectionBinding="App.controller.selectedPerson"
prompt="Please Select"}}
Please Select
Yehuda
Tom
Selecting Yehuda here will set `App.controller.selectedPerson` to
the Yehuda object.
Example using valueBinding:
{{view Ember.Select
contentBinding="App.controller.content"
optionLabelPath="content.firstName"
optionValuePath="content.id"
valueBinding="App.controller.selectedPersonId"
prompt="Please Select"}}
Selecting Yehuda in this case will set `App.controller.selectedPersonId` to 1.
@extends Ember.View
*/
Ember.Select = Ember.View.extend(
/** @scope Ember.Select.prototype */ {
tagName: 'select',
classNames: ['ember-select'],
defaultTemplate: Ember.Handlebars.compile('{{#if view.prompt}}{{view.prompt}} {{/if}}{{#each view.content}}{{view Ember.SelectOption contentBinding="this"}}{{/each}}'),
attributeBindings: ['multiple', 'tabindex'],
/**
The `multiple` attribute of the select element. Indicates whether multiple
options can be selected.
@type Boolean
@default false
*/
multiple: 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:
content: Ember.A([
{ id: 1, firstName: 'Yehuda' },
{ id: 2, firstName: 'Tom' }
]),
optionLabelPath: 'content.firstName',
optionValuePath: 'content.id'
@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.
@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.
@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').cacheable(),
/**
If given, a top-most dummy option will be rendered to serve as a user
prompt.
@type String
@default null
*/
prompt: null,
/**
The path of the option labels. See `content`.
@type String
@default 'content'
*/
optionLabelPath: 'content',
/**
The path of the option values. See `content`.
@type String
@default 'content'
*/
optionValuePath: 'content',
_change: function() {
if (get(this, 'multiple')) {
this._changeMultiple();
} else {
this._changeSingle();
}
},
selectionDidChange: Ember.observer(function() {
var selection = get(this, 'selection'),
isArray = Ember.isArray(selection);
if (get(this, 'multiple')) {
if (!isArray) {
set(this, 'selection', Ember.A([selection]));
return;
}
this._selectionDidChangeMultiple();
} else {
this._selectionDidChangeSingle();
}
}, 'selection'),
valueDidChange: Ember.observer(function() {
var content = get(this, 'content'),
value = get(this, 'value'),
valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''),
selectedValue = (valuePath ? get(this, 'selection.' + valuePath) : get(this, 'selection')),
selection;
if (value !== selectedValue) {
selection = content.find(function(obj) {
return value === (valuePath ? get(obj, valuePath) : obj);
});
this.set('selection', selection);
}
}, 'value'),
_triggerChange: function() {
var selection = get(this, 'selection');
if (selection) { this.selectionDidChange(); }
this._change();
},
_changeSingle: 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));
},
_changeMultiple: function() {
var options = this.$('option:selected'),
prompt = get(this, 'prompt'),
offset = prompt ? 1 : 0,
content = get(this, 'content');
if (!content){ return; }
if (options) {
var selectedIndexes = options.map(function(){
return this.index - offset;
}).toArray();
set(this, 'selection', content.objectsAt(selectedIndexes));
}
},
_selectionDidChangeSingle: function() {
var el = this.get('element');
if (!el) { return; }
var content = get(this, 'content'),
selection = get(this, 'selection'),
selectionIndex = content ? indexOf(content, selection) : -1,
prompt = get(this, 'prompt');
if (prompt) { selectionIndex += 1; }
if (el) { el.selectedIndex = selectionIndex; }
},
_selectionDidChangeMultiple: function() {
var content = get(this, 'content'),
selection = get(this, 'selection'),
selectedIndexes = content ? indexesOf(content, selection) : [-1],
prompt = get(this, 'prompt'),
offset = prompt ? 1 : 0,
options = this.$('option'),
adjusted;
if (options) {
options.each(function() {
adjusted = this.index > -1 ? this.index + offset : -1;
this.selected = indexOf(selectedIndexes, adjusted) > -1;
});
}
},
init: function() {
this._super();
this.on("didInsertElement", this, this._triggerChange);
this.on("change", this, this._change);
}
});
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) > -1;
} else {
// Primitives get passed through bindings as objects... since
// `new Number(4) !== 4`, we use `==` below
return content == selection;
}
}).property('content', 'parentView.selection').volatile(),
labelPathDidChange: Ember.observer(function() {
var labelPath = get(this, 'parentView.optionLabelPath');
if (!labelPath) { return; }
Ember.defineProperty(this, 'label', Ember.computed(function() {
return get(this, labelPath);
}).property(labelPath).cacheable());
}, 'parentView.optionLabelPath'),
valuePathDidChange: Ember.observer(function() {
var valuePath = get(this, 'parentView.optionValuePath');
if (!valuePath) { return; }
Ember.defineProperty(this, 'value', Ember.computed(function() {
return get(this, valuePath);
}).property(valuePath).cacheable());
}, 'parentView.optionValuePath')
});
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
/*globals Handlebars */
// Find templates stored in the head tag as script tags and make them available
// to Ember.CoreView in the global Ember.TEMPLATES object. This will be run as as
// jQuery DOM-ready callback.
//
// Script tags with "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(ctx) {
var selectors = 'script[type="text/x-handlebars"], script[type="text/x-raw-handlebars"]';
Ember.$(selectors, ctx)
.each(function() {
// Get a reference to the script tag
var script = Ember.$(this),
type = script.attr('type');
var compile = (script.attr('type') === 'text/x-raw-handlebars') ?
Ember.$.proxy(Handlebars.compile, Handlebars) :
Ember.$.proxy(Ember.Handlebars.compile, Ember.Handlebars),
// 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, elementId, options;
if (templateName) {
// For templates which have a name, we save them and then remove them from the DOM
Ember.TEMPLATES[templateName] = template;
// Remove script tag from DOM
script.remove();
} else {
if (script.parents('head').length !== 0) {
// don't allow inline templates in the head
throw new Ember.Error("Template found in without a name specified. " +
"Please provide a data-template-name attribute.\n" +
script.html());
}
// For templates which will be evaluated inline in the HTML document, instantiates a new
// view, and replaces the script tag holding the template with the new
// view's DOM representation.
//
// 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.get(viewPath) : Ember.View;
// Get the id of the script, used by Ember.View's elementId property,
// Look for data-element-id attribute.
elementId = script.attr('data-element-id');
options = { template: template };
if (elementId) { options.elementId = elementId; }
view = view.create(options);
view._insertElementLater(function() {
script.replaceWith(this.$());
// Avoid memory leak in IE
script = null;
});
}
});
};
/** @private */
function bootstrap() {
Ember.Handlebars.bootstrap( Ember.$(document) );
}
/*
We tie this to application.load to ensure that we've at least
attempted to bootstrap at the point that the application is loaded.
We also tie this to document ready since we're guaranteed that all
the inline templates are present at this point.
There's no harm to running this twice, since we remove the templates
from the DOM after processing.
*/
Ember.$(document).ready(bootstrap);
Ember.onLoad('application', bootstrap);
})();
(function() {
// ==========================================================================
// Project: Ember Handlebars Views
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
})();
// Version: v1.0.pre-5-gf1ec52a
// Last commit: f1ec52a (2012-08-06 17:23:55 -0700)
(function() {
// ==========================================================================
// Project: Ember
// Copyright: ©2011 Strobe Inc. and contributors.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
})();
(function() {
window.DS = Ember.Namespace.create({
CURRENT_API_REVISION: 4
});
})();
(function() {
var get = Ember.get, set = Ember.set;
/**
A record array is an array that contains records of a certain type. The record
array materializes records as needed when they are retrieved for the first
time. You should not create record arrays yourself. Instead, an instance of
DS.RecordArray or its subclasses will be returned by your application's store
in response to queries.
*/
DS.RecordArray = Ember.ArrayProxy.extend({
/**
The model type contained by this record array.
@type DS.Model
*/
type: null,
// The array of client ids backing the record array. When a
// record is requested from the record array, the record
// for the client id at the same index is materialized, if
// necessary, by the store.
content: null,
// The store that created this record array.
store: null,
objectAtContent: function(index) {
var content = get(this, 'content'),
clientId = content.objectAt(index),
store = get(this, 'store');
if (clientId !== undefined) {
return store.findByClientId(get(this, 'type'), clientId);
}
}
});
})();
(function() {
var get = Ember.get;
DS.FilteredRecordArray = DS.RecordArray.extend({
filterFunction: null,
replace: function() {
var type = get(this, 'type').toString();
throw new Error("The result of a client-side filter (on " + type + ") is immutable.");
},
updateFilter: Ember.observer(function() {
var store = get(this, 'store');
store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction'));
}, 'filterFunction')
});
})();
(function() {
var get = Ember.get, set = Ember.set;
DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({
query: null,
isLoaded: false,
replace: function() {
var type = get(this, 'type').toString();
throw new Error("The result of a server query (on " + type + ") is immutable.");
},
load: function(array) {
var store = get(this, 'store'), type = get(this, 'type');
var clientIds = store.loadMany(type, array).clientIds;
this.beginPropertyChanges();
set(this, 'content', Ember.A(clientIds));
set(this, 'isLoaded', true);
this.endPropertyChanges();
}
});
})();
(function() {
var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor;
var Set = function() {
this.hash = {};
this.list = [];
};
Set.prototype = {
add: function(item) {
var hash = this.hash,
guid = guidFor(item);
if (hash.hasOwnProperty(guid)) { return; }
hash[guid] = true;
this.list.push(item);
},
remove: function(item) {
var hash = this.hash,
guid = guidFor(item);
if (!hash.hasOwnProperty(guid)) { return; }
delete hash[guid];
var list = this.list,
index = Ember.EnumerableUtils.indexOf(this, item);
list.splice(index, 1);
},
isEmpty: function() {
return this.list.length === 0;
}
};
var LoadedState = Ember.State.extend({
recordWasAdded: function(manager, record) {
var dirty = manager.dirty, observer;
dirty.add(record);
observer = function() {
if (!get(record, 'isDirty')) {
record.removeObserver('isDirty', observer);
manager.send('childWasSaved', record);
}
};
record.addObserver('isDirty', observer);
},
recordWasRemoved: function(manager, record) {
var dirty = manager.dirty, observer;
dirty.add(record);
observer = function() {
record.removeObserver('isDirty', observer);
if (!get(record, 'isDirty')) { manager.send('childWasSaved', record); }
};
record.addObserver('isDirty', observer);
}
});
var states = {
loading: Ember.State.create({
isLoaded: false,
isDirty: false,
loadedRecords: function(manager, count) {
manager.decrement(count);
},
becameLoaded: function(manager) {
manager.transitionTo('clean');
}
}),
clean: LoadedState.create({
isLoaded: true,
isDirty: false,
recordWasAdded: function(manager, record) {
this._super(manager, record);
manager.goToState('dirty');
},
update: function(manager, clientIds) {
var manyArray = manager.manyArray;
set(manyArray, 'content', clientIds);
}
}),
dirty: LoadedState.create({
isLoaded: true,
isDirty: true,
childWasSaved: function(manager, child) {
var dirty = manager.dirty;
dirty.remove(child);
if (dirty.isEmpty()) { manager.send('arrayBecameSaved'); }
},
arrayBecameSaved: function(manager) {
manager.goToState('clean');
}
})
};
DS.ManyArrayStateManager = Ember.StateManager.extend({
manyArray: null,
initialState: 'loading',
states: states,
/**
This number is used to keep track of the number of outstanding
records that must be loaded before the array is considered
loaded. As results stream in, this number is decremented until
it becomes zero, at which case the `isLoaded` flag will be set
to true
*/
counter: 0,
init: function() {
this._super();
this.dirty = new Set();
this.counter = get(this, 'manyArray.length');
},
decrement: function(count) {
var counter = this.counter = this.counter - count;
Ember.assert("Somehow the ManyArray loaded counter went below 0. This is probably an ember-data bug. Please report it at https://github.com/emberjs/data/issues", counter >= 0);
if (counter === 0) {
this.send('becameLoaded');
}
}
});
})();
(function() {
var get = Ember.get, set = Ember.set;
DS.ManyArray = DS.RecordArray.extend({
init: function() {
set(this, 'stateManager', DS.ManyArrayStateManager.create({ manyArray: this }));
return this._super();
},
parentRecord: null,
isDirty: Ember.computed(function() {
return get(this, 'stateManager.currentState.isDirty');
}).property('stateManager.currentState').cacheable(),
isLoaded: Ember.computed(function() {
return get(this, 'stateManager.currentState.isLoaded');
}).property('stateManager.currentState').cacheable(),
send: function(event, context) {
this.get('stateManager').send(event, context);
},
fetch: function() {
var clientIds = get(this, 'content'),
store = get(this, 'store'),
type = get(this, 'type');
store.fetchUnloadedClientIds(type, clientIds);
},
// Overrides Ember.Array's replace method to implement
replaceContent: function(index, removed, added) {
var parentRecord = get(this, 'parentRecord');
var pendingParent = parentRecord && !get(parentRecord, 'id');
var stateManager = get(this, 'stateManager');
// Map the array of record objects into an array of client ids.
added = added.map(function(record) {
Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this association.", !get(this, 'type') || (get(this, 'type') === record.constructor));
// If the record to which this many array belongs does not yet
// have an id, notify the newly-added record that it must wait
// for the parent to receive an id before the child can be
// saved.
if (pendingParent) {
record.send('waitingOn', parentRecord);
}
var oldParent = this.assignInverse(record, parentRecord);
record.get('transaction')
.relationshipBecameDirty(record, oldParent, parentRecord);
stateManager.send('recordWasAdded', record);
return record.get('clientId');
}, this);
var store = this.store;
var len = index+removed, record;
for (var i = index; i < len; i++) {
// TODO: null out inverse FK
record = this.objectAt(i);
var oldParent = this.assignInverse(record, parentRecord, true);
record.get('transaction')
.relationshipBecameDirty(record, parentRecord, null);
// If we put the child record into a pending state because
// we were waiting on the parent record to get an id, we
// can tell the child it no longer needs to wait.
if (pendingParent) {
record.send('doneWaitingOn', parentRecord);
}
stateManager.send('recordWasAdded', record);
}
this._super(index, removed, added);
},
assignInverse: function(record, parentRecord, remove) {
var associationMap = get(record.constructor, 'associations'),
possibleAssociations = associationMap.get(parentRecord.constructor),
possible, actual, oldParent;
if (!possibleAssociations) { return; }
for (var i = 0, l = possibleAssociations.length; i < l; i++) {
possible = possibleAssociations[i];
if (possible.kind === 'belongsTo') {
actual = possible;
break;
}
}
if (actual) {
oldParent = get(record, actual.name);
set(record, actual.name, remove ? null : parentRecord);
return oldParent;
}
},
// Create a child record within the parentRecord
createRecord: function(hash, transaction) {
var parentRecord = get(this, 'parentRecord'),
store = get(parentRecord, 'store'),
type = get(this, 'type'),
record;
transaction = transaction || get(parentRecord, 'transaction');
record = store.createRecord.call(store, type, hash, transaction);
this.pushObject(record);
return record;
}
});
})();
(function() {
})();
(function() {
var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt,
removeObject = Ember.EnumerableUtils.removeObject;
/**
A transaction allows you to collect multiple records into a unit of work
that can be committed or rolled back as a group.
For example, if a record has local modifications that have not yet
been saved, calling `commit()` on its transaction will cause those
modifications to be sent to the adapter to be saved. Calling
`rollback()` on its transaction would cause all of the modifications to
be discarded and the record to return to the last known state before
changes were made.
If a newly created record's transaction is rolled back, it will
immediately transition to the deleted state.
If you do not explicitly create a transaction, a record is assigned to
an implicit transaction called the default transaction. In these cases,
you can treat your application's instance of `DS.Store` as a transaction
and call the `commit()` and `rollback()` methods on the store itself.
Once a record has been successfully committed or rolled back, it will
be moved back to the implicit transaction. Because it will now be in
a clean state, it can be moved to a new transaction if you wish.
### Creating a Transaction
To create a new transaction, call the `transaction()` method of your
application's `DS.Store` instance:
var transaction = App.store.transaction();
This will return a new instance of `DS.Transaction` with no records
yet assigned to it.
### Adding Existing Records
Add records to a transaction using the `add()` method:
record = App.store.find(Person, 1);
transaction.add(record);
Note that only records whose `isDirty` flag is `false` may be added
to a transaction. Once modifications to a record have been made
(its `isDirty` flag is `true`), it is not longer able to be added to
a transaction.
### Creating New Records
Because newly created records are dirty from the time they are created,
and because dirty records can not be added to a transaction, you must
use the `createRecord()` method to assign new records to a transaction.
For example, instead of this:
var transaction = store.transaction();
var person = Person.createRecord({ name: "Steve" });
// won't work because person is dirty
transaction.add(person);
Call `createRecord()` on the transaction directly:
var transaction = store.transaction();
transaction.createRecord(Person, { name: "Steve" });
### Asynchronous Commits
Typically, all of the records in a transaction will be committed
together. However, new records that have a dependency on other new
records need to wait for their parent record to be saved and assigned an
ID. In that case, the child record will continue to live in the
transaction until its parent is saved, at which time the transaction will
attempt to commit again.
For this reason, you should not re-use transactions once you have committed
them. Always make a new transaction and move the desired records to it before
calling commit.
*/
DS.Transaction = Ember.Object.extend({
/**
@private
Creates the bucket data structure used to segregate records by
type.
*/
init: function() {
set(this, 'buckets', {
clean: Ember.Map.create(),
created: Ember.Map.create(),
updated: Ember.Map.create(),
deleted: Ember.Map.create(),
inflight: Ember.Map.create()
});
this.dirtyRelationships = {
byChild: Ember.Map.create(),
byNewParent: Ember.Map.create(),
byOldParent: Ember.Map.create()
};
},
/**
Creates a new record of the given type and assigns it to the transaction
on which the method was called.
This is useful as only clean records can be added to a transaction and
new records created using other methods immediately become dirty.
@param {DS.Model} type the model type to create
@param {Object} hash the data hash to assign the new record
*/
createRecord: function(type, hash) {
var store = get(this, 'store');
return store.createRecord(type, hash, this);
},
/**
Adds an existing record to this transaction. Only records without
modficiations (i.e., records whose `isDirty` property is `false`)
can be added to a transaction.
@param {DS.Model} record the record to add to the transaction
*/
add: function(record) {
// we could probably make this work if someone has a valid use case. Do you?
Ember.assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty'));
var recordTransaction = get(record, 'transaction'),
defaultTransaction = get(this, 'store.defaultTransaction');
Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction);
this.adoptRecord(record);
},
/**
Commits the transaction, which causes all of the modified records that
belong to the transaction to be sent to the adapter to be saved.
Once you call `commit()` on a transaction, you should not re-use it.
When a record is saved, it will be removed from this transaction and
moved back to the store's default transaction.
*/
commit: function() {
var self = this,
iterate;
iterate = function(bucketType, fn, binding) {
var dirty = self.bucketForType(bucketType);
dirty.forEach(function(type, records) {
if (records.isEmpty()) { return; }
var array = [];
records.forEach(function(record) {
record.send('willCommit');
if (get(record, 'isPending') === false) {
array.push(record);
}
});
fn.call(binding, type, array);
});
};
var commitDetails = {
updated: {
eachType: function(fn, binding) { iterate('updated', fn, binding); }
},
created: {
eachType: function(fn, binding) { iterate('created', fn, binding); }
},
deleted: {
eachType: function(fn, binding) { iterate('deleted', fn, binding); }
}
};
var store = get(this, 'store');
var adapter = get(store, '_adapter');
this.removeCleanRecords();
if (adapter && adapter.commit) { adapter.commit(store, commitDetails); }
else { throw fmt("Adapter is either null or does not implement `commit` method", this); }
},
/**
Rolling back a transaction resets the records that belong to
that transaction.
Updated records have their properties reset to the last known
value from the persistence layer. Deleted records are reverted
to a clean, non-deleted state. Newly created records immediately
become deleted, and are not sent to the adapter to be persisted.
After the transaction is rolled back, any records that belong
to it will return to the store's default transaction, and the
current transaction should not be used again.
*/
rollback: function() {
var store = get(this, 'store'),
dirty;
// Loop through all of the records in each of the dirty states
// and initiate a rollback on them. As a side effect of telling
// the record to roll back, it should also move itself out of
// the dirty bucket and into the clean bucket.
['created', 'updated', 'deleted', 'inflight'].forEach(function(bucketType) {
dirty = this.bucketForType(bucketType);
dirty.forEach(function(type, records) {
records.forEach(function(record) {
record.send('rollback');
});
});
}, this);
// Now that all records in the transaction are guaranteed to be
// clean, migrate them all to the store's default transaction.
this.removeCleanRecords();
},
/**
@private
Removes a record from this transaction and back to the store's
default transaction.
Note: This method is private for now, but should probably be exposed
in the future once we have stricter error checking (for example, in the
case of the record being dirty).
@param {DS.Model} record
*/
remove: function(record) {
var defaultTransaction = get(this, 'store.defaultTransaction');
defaultTransaction.adoptRecord(record);
},
/**
@private
Removes all of the records in the transaction's clean bucket.
*/
removeCleanRecords: function() {
var clean = this.bucketForType('clean'),
self = this;
clean.forEach(function(type, records) {
records.forEach(function(record) {
self.remove(record);
});
});
},
/**
@private
Returns the bucket for the given bucket type. For example, you might call
`this.bucketForType('updated')` to get the `Ember.Map` that contains all
of the records that have changes pending.
@param {String} bucketType the type of bucket
@returns Ember.Map
*/
bucketForType: function(bucketType) {
var buckets = get(this, 'buckets');
return get(buckets, bucketType);
},
/**
@private
This method moves a record into a different transaction without the normal
checks that ensure that the user is not doing something weird, like moving
a dirty record into a new transaction.
It is designed for internal use, such as when we are moving a clean record
into a new transaction when the transaction is committed.
This method must not be called unless the record is clean.
@param {DS.Model} record
*/
adoptRecord: function(record) {
var oldTransaction = get(record, 'transaction');
if (oldTransaction) {
oldTransaction.removeFromBucket('clean', record);
}
this.addToBucket('clean', record);
set(record, 'transaction', this);
},
/**
@private
Adds a record to the named bucket.
@param {String} bucketType one of `clean`, `created`, `updated`, or `deleted`
*/
addToBucket: function(bucketType, record) {
var bucket = this.bucketForType(bucketType),
type = record.constructor;
var records = bucket.get(type);
if (!records) {
records = Ember.OrderedSet.create();
bucket.set(type, records);
}
records.add(record);
},
/**
@private
Removes a record from the named bucket.
@param {String} bucketType one of `clean`, `created`, `updated`, or `deleted`
*/
removeFromBucket: function(bucketType, record) {
var bucket = this.bucketForType(bucketType),
type = record.constructor;
var records = bucket.get(type);
records.remove(record);
},
/**
@private
Called by a ManyArray when a new record is added to it. This
method will index a relationship description by the child
record, its old parent, and its new parent.
The store will provide this description to the adapter's
shouldCommit method, so it can determine whether any of
the records is pending another record. The store will also
provide a list of these descriptions to the adapter's commit
method.
@param {DS.Model} record the new child record
@param {DS.Model} oldParent the parent that the child is
moving from, or null
@param {DS.Model} newParent the parent that the child is
moving to, or null
*/
relationshipBecameDirty: function(child, oldParent, newParent) {
var relationships = this.dirtyRelationships, relationship;
var relationshipsForChild = relationships.byChild.get(child),
possibleRelationship,
needsNewEntries = true;
// If the child has any existing dirty relationships in this
// transaction, we need to collapse the old relationship
// into the new one. For example, if we change the parent of
// a child record before saving, there is no need to save the
// record that was its parent temporarily.
if (relationshipsForChild) {
// Loop through all of the relationships we know about that
// contain the same child as the new relationship.
for (var i=0, l=relationshipsForChild.length; i B
// B -> C
//
// In this case, there is no need to remember the A->B
// change. We can collapse both changes into:
//
// A -> C
//
// Another possible case is:
//
// A -> B
// B -> A
//
// In this case, we don't need to do anything. We can
// simply remove the original A->B change and call it
// a day.
if (relationship.newParent === oldParent) {
oldParent = relationship.oldParent;
this.removeRelationship(relationship);
// This is the case of A->B followed by B->A.
if (relationship.oldParent === newParent) {
needsNewEntries = false;
}
}
}
}
relationship = {
child: child,
oldParent: oldParent,
newParent: newParent
};
// If we didn't go A->B and then B->A, add new dirty relationship
// entries.
if (needsNewEntries) {
this.addRelationshipTo('byChild', child, relationship);
this.addRelationshipTo('byOldParent', oldParent, relationship);
this.addRelationshipTo('byNewParent', newParent, relationship);
}
},
removeRelationship: function(relationship) {
var relationships = this.dirtyRelationships;
removeObject(relationships.byOldParent.get(relationship.oldParent), relationship);
removeObject(relationships.byNewParent.get(relationship.newParent), relationship);
removeObject(relationships.byChild.get(relationship.child), relationship);
},
addRelationshipTo: function(type, record, description) {
var map = this.dirtyRelationships[type];
var relationships = map.get(record);
if (!relationships) {
relationships = [ description ];
map.set(record, relationships);
} else {
relationships.push(description);
}
},
/**
@private
Called by a record's state manager to indicate that the record has entered
a dirty state. The record will be moved from the `clean` bucket and into
the appropriate dirty bucket.
@param {String} bucketType one of `created`, `updated`, or `deleted`
*/
recordBecameDirty: function(bucketType, record) {
this.removeFromBucket('clean', record);
this.addToBucket(bucketType, record);
},
/**
@private
Called by a record's state manager to indicate that the record has entered
inflight state. The record will be moved from its current dirty bucket and into
the `inflight` bucket.
@param {String} bucketType one of `created`, `updated`, or `deleted`
*/
recordBecameInFlight: function(kind, record) {
this.removeFromBucket(kind, record);
this.addToBucket('inflight', record);
},
/**
@private
Called by a record's state manager to indicate that the record has entered
a clean state. The record will be moved from its current dirty or inflight bucket and into
the `clean` bucket.
@param {String} bucketType one of `created`, `updated`, or `deleted`
*/
recordBecameClean: function(kind, record) {
this.removeFromBucket(kind, record);
this.remove(record);
}
});
})();
(function() {
/*globals Ember*/
var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;
var DATA_PROXY = {
get: function(name) {
return this.savedData[name];
}
};
// These values are used in the data cache when clientIds are
// needed but the underlying data has not yet been loaded by
// the server.
var UNLOADED = 'unloaded';
var LOADING = 'loading';
// Implementors Note:
//
// The variables in this file are consistently named according to the following
// scheme:
//
// * +id+ means an identifier managed by an external source, provided inside the
// data hash provided by that source.
// * +clientId+ means a transient numerical identifier generated at runtime by
// the data store. It is important primarily because newly created objects may
// not yet have an externally generated id.
// * +type+ means a subclass of DS.Model.
/**
The store contains all of the hashes for records loaded from the server.
It is also responsible for creating instances of DS.Model when you request one
of these data hashes, so that they can be bound to in your Handlebars templates.
Create a new store like this:
MyApp.store = DS.Store.create();
You can retrieve DS.Model instances from the store in several ways. To retrieve
a record for a specific id, use the `find()` method:
var record = MyApp.store.find(MyApp.Contact, 123);
By default, the store will talk to your backend using a standard REST mechanism.
You can customize how the store talks to your backend by specifying a custom adapter:
MyApp.store = DS.Store.create({
adapter: 'MyApp.CustomAdapter'
});
You can learn more about writing a custom adapter by reading the `DS.Adapter`
documentation.
*/
DS.Store = Ember.Object.extend({
/**
Many methods can be invoked without specifying which store should be used.
In those cases, the first store created will be used as the default. If
an application has multiple stores, it should specify which store to use
when performing actions, such as finding records by id.
The init method registers this store as the default if none is specified.
*/
init: function() {
// Enforce API revisioning. See BREAKING_CHANGES.md for more.
var revision = get(this, 'revision');
if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) {
throw new Error("Error: The Ember Data library has had breaking API changes since the last time you updated the library. Please review the list of breaking changes at https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md, then update your store's `revision` property to " + DS.CURRENT_API_REVISION);
}
if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) {
set(DS, 'defaultStore', this);
}
// internal bookkeeping; not observable
this.typeMaps = {};
this.recordCache = [];
this.clientIdToId = {};
this.recordArraysByClientId = {};
// Internally, we maintain a map of all unloaded IDs requested by
// a ManyArray. As the adapter loads hashes into the store, the
// store notifies any interested ManyArrays. When the ManyArray's
// total number of loading records drops to zero, it becomes
// `isLoaded` and fires a `didLoad` event.
this.loadingRecordArrays = {};
set(this, 'defaultTransaction', this.transaction());
return this._super();
},
/**
Returns a new transaction scoped to this store.
@see {DS.Transaction}
@returns DS.Transaction
*/
transaction: function() {
return DS.Transaction.create({ store: this });
},
/**
@private
This is used only by the record's DataProxy. Do not use this directly.
*/
dataForRecord: function(record) {
var type = record.constructor,
clientId = get(record, 'clientId'),
typeMap = this.typeMapFor(type);
return typeMap.cidToHash[clientId];
},
/**
The adapter to use to communicate to a backend server or other persistence layer.
This can be specified as an instance, a class, or a property path that specifies
where the adapter can be located.
@property {DS.Adapter|String}
*/
adapter: null,
/**
@private
This property returns the adapter, after resolving a possible String.
@returns DS.Adapter
*/
_adapter: Ember.computed(function() {
var adapter = get(this, 'adapter');
if (typeof adapter === 'string') {
return get(this, adapter, false) || get(window, adapter);
}
return adapter;
}).property('adapter').cacheable(),
// A monotonically increasing number to be used to uniquely identify
// data hashes and records.
clientIdCounter: 1,
// .....................
// . CREATE NEW RECORD .
// .....................
/**
Create a new record in the current store. The properties passed
to this method are set on the newly created record.
@param {subclass of DS.Model} type
@param {Object} properties a hash of properties to set on the
newly created record.
@returns DS.Model
*/
createRecord: function(type, properties, transaction) {
properties = properties || {};
// Create a new instance of the model `type` and put it
// into the specified `transaction`. If no transaction is
// specified, the default transaction will be used.
//
// NOTE: A `transaction` is specified when the
// `transaction.createRecord` API is used.
var record = type._create({
store: this
});
transaction = transaction || get(this, 'defaultTransaction');
transaction.adoptRecord(record);
// Extract the primary key from the `properties` hash,
// based on the `primaryKey` for the model type.
var primaryKey = get(record, 'primaryKey'),
id = properties[primaryKey] || null;
// If the passed properties do not include a primary key,
// give the adapter an opportunity to generate one.
var adapter;
if (Ember.none(id)) {
adapter = get(this, 'adapter');
if (adapter && adapter.generateIdForRecord) {
id = adapter.generateIdForRecord(this, record);
properties.id = id;
}
}
var hash = {}, clientId;
// Push the hash into the store. If present, associate the
// extracted `id` with the hash.
clientId = this.pushHash(hash, id, type);
record.send('didChangeData');
var recordCache = get(this, 'recordCache');
// Now that we have a clientId, attach it to the record we
// just created.
set(record, 'clientId', clientId);
// Store the record we just created in the record cache for
// this clientId.
recordCache[clientId] = record;
// Set the properties specified on the record.
record.setProperties(properties);
this.updateRecordArrays(type, clientId, get(record, 'data'));
return record;
},
// .................
// . DELETE RECORD .
// .................
/**
For symmetry, a record can be deleted via the store.
@param {DS.Model} record
*/
deleteRecord: function(record) {
record.send('deleteRecord');
},
// ................
// . FIND RECORDS .
// ................
/**
This is the main entry point into finding records. The first
parameter to this method is always a subclass of `DS.Model`.
You can use the `find` method on a subclass of `DS.Model`
directly if your application only has one store. For
example, instead of `store.find(App.Person, 1)`, you could
say `App.Person.find(1)`.
---
To find a record by ID, pass the `id` as the second parameter:
store.find(App.Person, 1);
App.Person.find(1);
If the record with that `id` had not previously been loaded,
the store will return an empty record immediately and ask
the adapter to find the data by calling the adapter's `find`
method.
The `find` method will always return the same object for a
given type and `id`. To check whether the adapter has populated
a record, you can check its `isLoaded` property.
---
To find all records for a type, call `find` with no additional
parameters:
store.find(App.Person);
App.Person.find();
This will return a `RecordArray` representing all known records
for the given type and kick off a request to the adapter's
`findAll` method to load any additional records for the type.
The `RecordArray` returned by `find()` is live. If any more
records for the type are added at a later time through any
mechanism, it will automatically update to reflect the change.
---
To find a record by a query, call `find` with a hash as the
second parameter:
store.find(App.Person, { page: 1 });
App.Person.find({ page: 1 });
This will return a `RecordArray` immediately, but it will always
be an empty `RecordArray` at first. It will call the adapter's
`findQuery` method, which will populate the `RecordArray` once
the server has returned results.
You can check whether a query results `RecordArray` has loaded
by checking its `isLoaded` property.
*/
find: function(type, id, query) {
if (id === undefined) {
return this.findAll(type);
}
if (query !== undefined) {
return this.findMany(type, id, query);
} else if (Ember.typeOf(id) === 'object') {
return this.findQuery(type, id);
}
if (Ember.isArray(id)) {
return this.findMany(type, id);
}
var clientId = this.typeMapFor(type).idToCid[id];
return this.findByClientId(type, clientId, id);
},
findByClientId: function(type, clientId, id) {
var recordCache = get(this, 'recordCache'),
dataCache, record;
// If there is already a clientId assigned for this
// type/id combination, try to find an existing
// record for that id and return. Otherwise,
// materialize a new record and set its data to the
// value we already have.
if (clientId !== undefined) {
record = recordCache[clientId];
if (!record) {
// create a new instance of the model type in the
// 'isLoading' state
record = this.materializeRecord(type, clientId);
dataCache = this.typeMapFor(type).cidToHash;
if (typeof dataCache[clientId] === 'object') {
record.send('didChangeData');
}
}
} else {
clientId = this.pushHash(LOADING, id, type);
// create a new instance of the model type in the
// 'isLoading' state
record = this.materializeRecord(type, clientId, id);
// let the adapter set the data, possibly async
var adapter = get(this, '_adapter');
if (adapter && adapter.find) { adapter.find(this, type, id); }
else { throw fmt("Adapter is either null or does not implement `find` method", this); }
}
return record;
},
/**
@private
Given a type and array of `clientId`s, determines which of those
`clientId`s has not yet been loaded.
In preparation for loading, this method also marks any unloaded
`clientId`s as loading.
*/
neededClientIds: function(type, clientIds) {
var neededClientIds = [],
typeMap = this.typeMapFor(type),
dataCache = typeMap.cidToHash,
clientId;
for (var i=0, l=clientIds.length; i "created.uncommitted"
The `DS.Model` states are themselves stateless. What we mean is that,
though each instance of a record also has a unique instance of a
`DS.StateManager`, the hierarchical states that each of *those* points
to is a shared data structure. For performance reasons, instead of each
record getting its own copy of the hierarchy of states, each state
manager points to this global, immutable shared instance. How does a
state know which record it should be acting on? We pass a reference to
the current state manager as the first parameter to every method invoked
on a state.
The state manager passed as the first parameter is where you should stash
state about the record if needed; you should never store data on the state
object itself. If you need access to the record being acted on, you can
retrieve the state manager's `record` property. For example, if you had
an event handler `myEvent`:
myEvent: function(manager) {
var record = manager.get('record');
record.doSomething();
}
For more information about state managers in general, see the Ember.js
documentation on `Ember.StateManager`.
### Events, Flags, and Transitions
A state may implement zero or more events, flags, or transitions.
#### Events
Events are named functions that are invoked when sent to a record. The
state manager will first look for a method with the given name on the
current state. If no method is found, it will search the current state's
parent, and then its grandparent, and so on until reaching the top of
the hierarchy. If the root is reached without an event handler being found,
an exception will be raised. This can be very helpful when debugging new
features.
Here's an example implementation of a state with a `myEvent` event handler:
aState: DS.State.create({
myEvent: function(manager, param) {
console.log("Received myEvent with "+param);
}
})
To trigger this event:
record.send('myEvent', 'foo');
//=> "Received myEvent with foo"
Note that an optional parameter can be sent to a record's `send()` method,
which will be passed as the second parameter to the event handler.
Events should transition to a different state if appropriate. This can be
done by calling the state manager's `goToState()` method with a path to the
desired state. The state manager will attempt to resolve the state path
relative to the current state. If no state is found at that path, it will
attempt to resolve it relative to the current state's parent, and then its
parent, and so on until the root is reached. For example, imagine a hierarchy
like this:
* created
* start <-- currentState
* inFlight
* updated
* inFlight
If we are currently in the `start` state, calling
`goToState('inFlight')` would transition to the `created.inFlight` state,
while calling `goToState('updated.inFlight')` would transition to
the `updated.inFlight` state.
Remember that *only events* should ever cause a state transition. You should
never call `goToState()` from outside a state's event handler. If you are
tempted to do so, create a new event and send that to the state manager.
#### Flags
Flags are Boolean values that can be used to introspect a record's current
state in a more user-friendly way than examining its state path. For example,
instead of doing this:
var statePath = record.get('stateManager.currentState.path');
if (statePath === 'created.inFlight') {
doSomething();
}
You can say:
if (record.get('isNew') && record.get('isSaving')) {
doSomething();
}
If your state does not set a value for a given flag, the value will
be inherited from its parent (or the first place in the state hierarchy
where it is defined).
The current set of flags are defined below. If you want to add a new flag,
in addition to the area below, you will also need to declare it in the
`DS.Model` class.
#### Transitions
Transitions are like event handlers but are called automatically upon
entering or exiting a state. To implement a transition, just call a method
either `enter` or `exit`:
myState: DS.State.create({
// Gets called automatically when entering
// this state.
enter: function(manager) {
console.log("Entered myState");
}
})
Note that enter and exit events are called once per transition. If the
current state changes, but changes to another child state of the parent,
the transition event on the parent will not be triggered.
*/
var stateProperty = Ember.computed(function(key) {
var parent = get(this, 'parentState');
if (parent) {
return get(parent, key);
}
}).property();
var isEmptyObject = function(object) {
for (var name in object) {
if (object.hasOwnProperty(name)) { return false; }
}
return true;
};
var hasDefinedProperties = function(object) {
for (var name in object) {
if (object.hasOwnProperty(name) && object[name]) { return true; }
}
return false;
};
DS.State = Ember.State.extend({
isLoaded: stateProperty,
isDirty: stateProperty,
isSaving: stateProperty,
isDeleted: stateProperty,
isError: stateProperty,
isNew: stateProperty,
isValid: stateProperty,
isPending: stateProperty,
// For states that are substates of a
// DirtyState (updated or created), it is
// useful to be able to determine which
// type of dirty state it is.
dirtyType: stateProperty
});
var setProperty = function(manager, context) {
var key = context.key, value = context.value;
var record = get(manager, 'record'),
data = get(record, 'data');
set(data, key, value);
};
var setAssociation = function(manager, context) {
var key = context.key, value = context.value;
var record = get(manager, 'record'),
data = get(record, 'data');
data.setAssociation(key, value);
};
var didChangeData = function(manager) {
var record = get(manager, 'record'),
data = get(record, 'data');
data._savedData = null;
record.notifyPropertyChange('data');
};
// The waitingOn event shares common functionality
// between the different dirty states, but each is
// treated slightly differently. This method is exposed
// so that each implementation can invoke the common
// behavior, and then implement the behavior specific
// to the state.
var waitingOn = function(manager, object) {
var record = get(manager, 'record'),
pendingQueue = get(record, 'pendingQueue'),
objectGuid = guidFor(object);
var observer = function() {
if (get(object, 'id')) {
manager.send('doneWaitingOn', object);
Ember.removeObserver(object, 'id', observer);
}
};
pendingQueue[objectGuid] = [object, observer];
Ember.addObserver(object, 'id', observer);
};
// Implementation notes:
//
// Each state has a boolean value for all of the following flags:
//
// * isLoaded: The record has a populated `data` property. When a
// record is loaded via `store.find`, `isLoaded` is false
// until the adapter sets it. When a record is created locally,
// its `isLoaded` property is always true.
// * isDirty: The record has local changes that have not yet been
// saved by the adapter. This includes records that have been
// created (but not yet saved) or deleted.
// * isSaving: The record's transaction has been committed, but
// the adapter has not yet acknowledged that the changes have
// been persisted to the backend.
// * isDeleted: The record was marked for deletion. When `isDeleted`
// is true and `isDirty` is true, the record is deleted locally
// but the deletion was not yet persisted. When `isSaving` is
// true, the change is in-flight. When both `isDirty` and
// `isSaving` are false, the change has persisted.
// * isError: The adapter reported that it was unable to save
// local changes to the backend. This may also result in the
// record having its `isValid` property become false if the
// adapter reported that server-side validations failed.
// * isNew: The record was created on the client and the adapter
// did not yet report that it was successfully saved.
// * isValid: No client-side validations have failed and the
// adapter did not report any server-side validation failures.
// * isPending: A record `isPending` when it belongs to an
// association on another record and that record has not been
// saved. A record in this state cannot be saved because it
// lacks a "foreign key" that will be supplied by its parent
// association when the parent record has been created. When
// the adapter reports that the parent has saved, the
// `isPending` property on all children will become `false`
// and the transaction will try to commit the records.
// This mixin is mixed into various uncommitted states. Make
// sure to mix it in *after* the class definition, so its
// super points to the class definition.
var Uncommitted = Ember.Mixin.create({
setProperty: setProperty,
setAssociation: setAssociation
});
// These mixins are mixed into substates of the concrete
// subclasses of DirtyState.
var CreatedUncommitted = Ember.Mixin.create({
deleteRecord: function(manager) {
var record = get(manager, 'record');
this._super(manager);
record.withTransaction(function(t) {
t.recordBecameClean('created', record);
});
manager.goToState('deleted.saved');
}
});
var UpdatedUncommitted = Ember.Mixin.create({
deleteRecord: function(manager) {
this._super(manager);
var record = get(manager, 'record');
record.withTransaction(function(t) {
t.recordBecameClean('updated', record);
});
manager.goToState('deleted');
}
});
// The dirty state is a abstract state whose functionality is
// shared between the `created` and `updated` states.
//
// The deleted state shares the `isDirty` flag with the
// subclasses of `DirtyState`, but with a very different
// implementation.
var DirtyState = DS.State.extend({
initialState: 'uncommitted',
// FLAGS
isDirty: true,
// SUBSTATES
// When a record first becomes dirty, it is `uncommitted`.
// This means that there are local pending changes,
// but they have not yet begun to be saved.
uncommitted: DS.State.extend({
// TRANSITIONS
enter: function(manager) {
var dirtyType = get(this, 'dirtyType'),
record = get(manager, 'record');
record.withTransaction(function (t) {
t.recordBecameDirty(dirtyType, record);
});
},
// EVENTS
deleteRecord: Ember.K,
waitingOn: function(manager, object) {
waitingOn(manager, object);
manager.goToState('pending');
},
willCommit: function(manager) {
manager.goToState('inFlight');
},
becameInvalid: function(manager) {
var dirtyType = get(this, 'dirtyType'),
record = get(manager, 'record');
record.withTransaction(function (t) {
t.recordBecameInFlight(dirtyType, record);
});
manager.goToState('invalid');
},
rollback: function(manager) {
var record = get(manager, 'record'),
dirtyType = get(this, 'dirtyType'),
data = get(record, 'data');
data.rollback();
record.withTransaction(function(t) {
t.recordBecameClean(dirtyType, record);
});
manager.goToState('saved');
}
}, Uncommitted),
// Once a record has been handed off to the adapter to be
// saved, it is in the 'in flight' state. Changes to the
// record cannot be made during this window.
inFlight: DS.State.extend({
// FLAGS
isSaving: true,
// TRANSITIONS
enter: function(manager) {
var dirtyType = get(this, 'dirtyType'),
record = get(manager, 'record');
record.withTransaction(function (t) {
t.recordBecameInFlight(dirtyType, record);
});
},
// EVENTS
didCommit: function(manager) {
var dirtyType = get(this, 'dirtyType'),
record = get(manager, 'record');
record.withTransaction(function(t) {
t.recordBecameClean('inflight', record);
});
manager.goToState('saved');
manager.send('invokeLifecycleCallbacks', dirtyType);
},
becameInvalid: function(manager, errors) {
var record = get(manager, 'record');
set(record, 'errors', errors);
manager.goToState('invalid');
manager.send('invokeLifecycleCallbacks');
},
becameError: function(manager) {
manager.goToState('error');
manager.send('invokeLifecycleCallbacks');
},
didChangeData: didChangeData
}),
// If a record becomes associated with a newly created
// parent record, it will be `pending` until the parent
// record has successfully persisted. Once this happens,
// this record can use the parent's primary key as its
// foreign key.
//
// If the record's transaction had already started to
// commit, the record will transition to the `inFlight`
// state. If it had not, the record will transition to
// the `uncommitted` state.
pending: DS.State.extend({
initialState: 'uncommitted',
// FLAGS
isPending: true,
// SUBSTATES
// A pending record whose transaction has not yet
// started to commit is in this state.
uncommitted: DS.State.extend({
// EVENTS
deleteRecord: function(manager) {
var record = get(manager, 'record'),
pendingQueue = get(record, 'pendingQueue'),
tuple;
// since we are leaving the pending state, remove any
// observers we have registered on other records.
for (var prop in pendingQueue) {
if (!pendingQueue.hasOwnProperty(prop)) { continue; }
tuple = pendingQueue[prop];
Ember.removeObserver(tuple[0], 'id', tuple[1]);
}
},
willCommit: function(manager) {
manager.goToState('committing');
},
doneWaitingOn: function(manager, object) {
var record = get(manager, 'record'),
pendingQueue = get(record, 'pendingQueue'),
objectGuid = guidFor(object);
delete pendingQueue[objectGuid];
if (isEmptyObject(pendingQueue)) {
manager.send('doneWaiting');
}
},
doneWaiting: function(manager) {
var dirtyType = get(this, 'dirtyType');
manager.goToState(dirtyType + '.uncommitted');
}
}, Uncommitted),
// A pending record whose transaction has started
// to commit is in this state. Since it has not yet
// been sent to the adapter, it is not `inFlight`
// until all of its dependencies have been committed.
committing: DS.State.extend({
// FLAGS
isSaving: true,
// EVENTS
doneWaitingOn: function(manager, object) {
var record = get(manager, 'record'),
pendingQueue = get(record, 'pendingQueue'),
objectGuid = guidFor(object);
delete pendingQueue[objectGuid];
if (isEmptyObject(pendingQueue)) {
manager.send('doneWaiting');
}
},
doneWaiting: function(manager) {
var record = get(manager, 'record'),
transaction = get(record, 'transaction');
// Now that the record is no longer pending, schedule
// the transaction to commit.
Ember.run.once(transaction, transaction.commit);
},
willCommit: function(manager) {
var record = get(manager, 'record'),
pendingQueue = get(record, 'pendingQueue');
if (isEmptyObject(pendingQueue)) {
var dirtyType = get(this, 'dirtyType');
manager.goToState(dirtyType + '.inFlight');
}
}
})
}),
// A record is in the `invalid` state when its client-side
// invalidations have failed, or if the adapter has indicated
// the the record failed server-side invalidations.
invalid: DS.State.extend({
// FLAGS
isValid: false,
exit: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function (t) {
t.recordBecameClean('inflight', record);
});
},
// EVENTS
deleteRecord: function(manager) {
manager.goToState('deleted');
},
setAssociation: setAssociation,
setProperty: function(manager, context) {
setProperty(manager, context);
var record = get(manager, 'record'),
errors = get(record, 'errors'),
key = context.key;
set(errors, key, null);
if (!hasDefinedProperties(errors)) {
manager.send('becameValid');
}
},
rollback: function(manager) {
manager.send('becameValid');
manager.send('rollback');
},
becameValid: function(manager) {
manager.goToState('uncommitted');
},
invokeLifecycleCallbacks: function(manager) {
var record = get(manager, 'record');
record.trigger('becameInvalid', record);
}
})
});
// The created and updated states are created outside the state
// chart so we can reopen their substates and add mixins as
// necessary.
var createdState = DirtyState.create({
dirtyType: 'created',
// FLAGS
isNew: true
});
var updatedState = DirtyState.create({
dirtyType: 'updated'
});
// The created.uncommitted state and created.pending.uncommitted share
// some logic defined in CreatedUncommitted.
createdState.states.uncommitted.reopen(CreatedUncommitted);
createdState.states.pending.states.uncommitted.reopen(CreatedUncommitted);
// The created.uncommitted state needs to immediately transition to the
// deleted state if it is rolled back.
createdState.states.uncommitted.reopen({
rollback: function(manager) {
this._super(manager);
manager.goToState('deleted.saved');
}
});
// The updated.uncommitted state and updated.pending.uncommitted share
// some logic defined in UpdatedUncommitted.
updatedState.states.uncommitted.reopen(UpdatedUncommitted);
updatedState.states.pending.states.uncommitted.reopen(UpdatedUncommitted);
updatedState.states.inFlight.reopen({
didSaveData: function(manager) {
var record = get(manager, 'record'),
data = get(record, 'data');
data.saveData();
data.adapterDidUpdate();
}
});
var states = {
rootState: Ember.State.create({
// FLAGS
isLoaded: false,
isDirty: false,
isSaving: false,
isDeleted: false,
isError: false,
isNew: false,
isValid: true,
isPending: false,
// SUBSTATES
// A record begins its lifecycle in the `empty` state.
// If its data will come from the adapter, it will
// transition into the `loading` state. Otherwise, if
// the record is being created on the client, it will
// transition into the `created` state.
empty: DS.State.create({
// EVENTS
loadingData: function(manager) {
manager.goToState('loading');
},
didChangeData: function(manager) {
didChangeData(manager);
manager.goToState('loaded.created');
}
}),
// A record enters this state when the store askes
// the adapter for its data. It remains in this state
// until the adapter provides the requested data.
//
// Usually, this process is asynchronous, using an
// XHR to retrieve the data.
loading: DS.State.create({
// TRANSITIONS
exit: function(manager) {
var record = get(manager, 'record');
record.trigger('didLoad');
},
// EVENTS
didChangeData: function(manager, data) {
didChangeData(manager);
manager.send('loadedData');
},
loadedData: function(manager) {
manager.goToState('loaded');
}
}),
// A record enters this state when its data is populated.
// Most of a record's lifecycle is spent inside substates
// of the `loaded` state.
loaded: DS.State.create({
initialState: 'saved',
// FLAGS
isLoaded: true,
// SUBSTATES
// If there are no local changes to a record, it remains
// in the `saved` state.
saved: DS.State.create({
// EVENTS
setProperty: function(manager, context) {
setProperty(manager, context);
manager.goToState('updated');
},
setAssociation: function(manager, context) {
setAssociation(manager, context);
manager.goToState('updated');
},
didChangeData: didChangeData,
deleteRecord: function(manager) {
manager.goToState('deleted');
},
waitingOn: function(manager, object) {
waitingOn(manager, object);
manager.goToState('updated.pending');
},
invokeLifecycleCallbacks: function(manager, dirtyType) {
var record = get(manager, 'record');
if (dirtyType === 'created') {
record.trigger('didCreate', record);
} else {
record.trigger('didUpdate', record);
}
}
}),
// A record is in this state after it has been locally
// created but before the adapter has indicated that
// it has been saved.
created: createdState,
// A record is in this state if it has already been
// saved to the server, but there are new local changes
// that have not yet been saved.
updated: updatedState
}),
// A record is in this state if it was deleted from the store.
deleted: DS.State.create({
// FLAGS
isDeleted: true,
isLoaded: true,
isDirty: true,
// TRANSITIONS
enter: function(manager) {
var record = get(manager, 'record'),
store = get(record, 'store');
store.removeFromRecordArrays(record);
},
// SUBSTATES
// When a record is deleted, it enters the `start`
// state. It will exit this state when the record's
// transaction starts to commit.
start: DS.State.create({
// TRANSITIONS
enter: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function(t) {
t.recordBecameDirty('deleted', record);
});
},
// EVENTS
willCommit: function(manager) {
manager.goToState('inFlight');
},
rollback: function(manager) {
var record = get(manager, 'record'),
data = get(record, 'data');
data.rollback();
record.withTransaction(function(t) {
t.recordBecameClean('deleted', record);
});
manager.goToState('loaded');
}
}),
// After a record's transaction is committing, but
// before the adapter indicates that the deletion
// has saved to the server, a record is in the
// `inFlight` substate of `deleted`.
inFlight: DS.State.create({
// FLAGS
isSaving: true,
// TRANSITIONS
enter: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function (t) {
t.recordBecameInFlight('deleted', record);
});
},
// EVENTS
didCommit: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function(t) {
t.recordBecameClean('inflight', record);
});
manager.goToState('saved');
manager.send('invokeLifecycleCallbacks');
}
}),
// Once the adapter indicates that the deletion has
// been saved, the record enters the `saved` substate
// of `deleted`.
saved: DS.State.create({
// FLAGS
isDirty: false,
invokeLifecycleCallbacks: function(manager) {
var record = get(manager, 'record');
record.trigger('didDelete', record);
}
})
}),
// If the adapter indicates that there was an unknown
// error saving a record, the record enters the `error`
// state.
error: DS.State.create({
isError: true,
// EVENTS
invokeLifecycleCallbacks: function(manager) {
var record = get(manager, 'record');
record.trigger('becameError', record);
}
})
})
};
DS.StateManager = Ember.StateManager.extend({
record: null,
initialState: 'rootState',
states: states
});
})();
(function() {
var get = Ember.get, set = Ember.set;
// When a record is changed on the client, it is considered "dirty"--there are
// pending changes that need to be saved to a persistence layer, such as a
// server.
//
// If the record is rolled back, it re-enters a clean state, any changes are
// discarded, and its attributes are reset back to the last known good copy
// of the data that came from the server.
//
// If the record is committed, the changes are sent to the server to be saved,
// and once the server confirms that they are valid, the record's "canonical"
// data becomes the original canonical data plus the changes merged in.
//
// A DataProxy is an object that encapsulates this change tracking. It
// contains three buckets:
//
// * `savedData` - the last-known copy of the data from the server
// * `unsavedData` - a hash that contains any changes that have not yet
// been committed
// * `associations` - this is similar to `savedData`, but holds the client
// ids of associated records
//
// When setting a property on the object, the value is placed into the
// `unsavedData` bucket:
//
// proxy.set('key', 'value');
//
// // unsavedData:
// {
// key: "value"
// }
//
// When retrieving a property from the object, it first looks to see
// if that value exists in the `unsavedData` bucket, and returns it if so.
// Otherwise, it returns the value from the `savedData` bucket.
//
// When the adapter notifies a record that it has been saved, it merges the
// `unsavedData` bucket into the `savedData` bucket. If the record's
// transaction is rolled back, the `unsavedData` hash is simply discarded.
//
// This object is a regular JS object for performance. It is only
// used internally for bookkeeping purposes.
var DataProxy = DS._DataProxy = function(record) {
this.record = record;
this.unsavedData = {};
this.associations = {};
};
DataProxy.prototype = {
get: function(key) { return Ember.get(this, key); },
set: function(key, value) { return Ember.set(this, key, value); },
setAssociation: function(key, value) {
this.associations[key] = value;
},
savedData: function() {
var savedData = this._savedData;
if (savedData) { return savedData; }
var record = this.record,
clientId = get(record, 'clientId'),
store = get(record, 'store');
if (store) {
savedData = store.dataForRecord(record);
this._savedData = savedData;
return savedData;
}
},
unknownProperty: function(key) {
var unsavedData = this.unsavedData,
associations = this.associations,
savedData = this.savedData(),
store;
var value = unsavedData[key], association;
// if this is a belongsTo association, this will
// be a clientId.
association = associations[key];
if (association !== undefined) {
store = get(this.record, 'store');
return store.clientIdToId[association];
}
if (savedData && value === undefined) {
value = savedData[key];
}
return value;
},
setUnknownProperty: function(key, value) {
var record = this.record,
unsavedData = this.unsavedData;
unsavedData[key] = value;
record.hashWasUpdated();
return value;
},
commit: function() {
this.saveData();
this.record.notifyPropertyChange('data');
},
rollback: function() {
this.unsavedData = {};
this.record.notifyPropertyChange('data');
},
saveData: function() {
var record = this.record;
var unsavedData = this.unsavedData;
var savedData = this.savedData();
for (var prop in unsavedData) {
if (unsavedData.hasOwnProperty(prop)) {
savedData[prop] = unsavedData[prop];
delete unsavedData[prop];
}
}
},
adapterDidUpdate: function() {
this.unsavedData = {};
}
};
})();
(function() {
var get = Ember.get, set = Ember.set, none = Ember.none;
var retrieveFromCurrentState = Ember.computed(function(key) {
return get(get(this, 'stateManager.currentState'), key);
}).property('stateManager.currentState').cacheable();
DS.Model = Ember.Object.extend(Ember.Evented, {
isLoaded: retrieveFromCurrentState,
isDirty: retrieveFromCurrentState,
isSaving: retrieveFromCurrentState,
isDeleted: retrieveFromCurrentState,
isError: retrieveFromCurrentState,
isNew: retrieveFromCurrentState,
isPending: retrieveFromCurrentState,
isValid: retrieveFromCurrentState,
clientId: null,
transaction: null,
stateManager: null,
pendingQueue: null,
errors: null,
// because unknownProperty is used, any internal property
// must be initialized here.
primaryKey: 'id',
id: Ember.computed(function(key, value) {
var primaryKey = get(this, 'primaryKey'),
data = get(this, 'data');
if (arguments.length === 2) {
set(data, primaryKey, value);
return value;
}
var id = get(data, primaryKey);
return id ? id : this._id;
}).property('primaryKey', 'data'),
// The following methods are callbacks invoked by `toJSON`. You
// can override one of the callbacks to override specific behavior,
// or toJSON itself.
//
// If you override toJSON, you can invoke these callbacks manually
// to get the default behavior.
/**
Add the record's primary key to the JSON hash.
The default implementation uses the record's specified `primaryKey`
and the `id` computed property, which are passed in as parameters.
@param {Object} json the JSON hash being built
@param {Number|String} id the record's id
@param {String} key the primaryKey for the record
*/
addIdToJSON: function(json, id, key) {
if (id) { json[key] = id; }
},
/**
Add the attributes' current values to the JSON hash.
The default implementation gets the current value of each
attribute from the `data`, and uses a `defaultValue` if
specified in the `DS.attr` definition.
@param {Object} json the JSON hash being build
@param {Ember.Map} attributes a Map of attributes
@param {DataProxy} data the record's data, accessed with `get` and `set`.
*/
addAttributesToJSON: function(json, attributes, data) {
attributes.forEach(function(name, meta) {
var key = meta.key(this.constructor),
value = get(data, key);
if (value === undefined) {
value = meta.options.defaultValue;
}
json[key] = value;
}, this);
},
/**
Add the value of a `hasMany` association to the JSON hash.
The default implementation honors the `embedded` option
passed to `DS.hasMany`. If embedded, `toJSON` is recursively
called on the child records. If not, the `id` of each
record is added.
Note that if a record is not embedded and does not
yet have an `id` (usually provided by the server), it
will not be included in the output.
@param {Object} json the JSON hash being built
@param {DataProxy} data the record's data, accessed with `get` and `set`.
@param {Object} meta information about the association
@param {Object} options options passed to `toJSON`
*/
addHasManyToJSON: function(json, data, meta, options) {
var key = meta.key,
manyArray = get(this, key),
records = [], i, l,
clientId, id;
if (meta.options.embedded) {
// TODO: Avoid materializing embedded hashes if possible
manyArray.forEach(function(record) {
records.push(record.toJSON(options));
});
} else {
var clientIds = get(manyArray, 'content');
for (i=0, l=clientIds.length; i>> 0;
if (len === 0) {
return -1;
}
var n = 0;
if (arguments.length > 0) {
n = Number(arguments[1]);
if (n !== n) { // shortcut for verifying if it's NaN
n = 0;
} else if (n !== 0 && n !== (Infinity) && n !== -(Infinity)) {
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
}
if (n >= len) {
return -1;
}
var k = n >= 0
? n
: Math.max(len - Math.abs(n), 0);
for (; k < len; k++) {
if (k in t && t[k] === searchElement) {
return k;
}
}
return -1;
};
}
// Instantiate the object
var I18n = I18n || {};
// Set default locale to english
I18n.defaultLocale = "en";
// Set default handling of translation fallbacks to false
I18n.fallbacks = false;
// Set default separator
I18n.defaultSeparator = ".";
// Set current locale to null
I18n.locale = null;
// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
I18n.fallbackRules = {
};
I18n.pluralizationRules = {
en: function (n) {
return n == 0 ? ["zero", "none", "other"] : n == 1 ? "one" : "other";
}
};
I18n.getFallbacks = function(locale) {
if (locale === I18n.defaultLocale) {
return [];
} else if (!I18n.fallbackRules[locale]) {
var rules = []
, components = locale.split("-");
for (var l = 1; l < components.length; l++) {
rules.push(components.slice(0, l).join("-"));
}
rules.push(I18n.defaultLocale);
I18n.fallbackRules[locale] = rules;
}
return I18n.fallbackRules[locale];
}
I18n.isValidNode = function(obj, node, undefined) {
return obj[node] !== null && obj[node] !== undefined;
};
I18n.lookup = function(scope, options) {
var options = options || {}
, lookupInitialScope = scope
, translations = this.prepareOptions(I18n.translations)
, locale = options.locale || I18n.currentLocale()
, messages = translations[locale] || {}
, options = this.prepareOptions(options)
, currentScope
;
if (typeof(scope) == "object") {
scope = scope.join(this.defaultSeparator);
}
if (options.scope) {
scope = options.scope.toString() + this.defaultSeparator + scope;
}
scope = scope.split(this.defaultSeparator);
while (messages && scope.length > 0) {
currentScope = scope.shift();
messages = messages[currentScope];
}
if (!messages) {
if (I18n.fallbacks) {
var fallbacks = this.getFallbacks(locale);
for (var fallback = 0; fallback < fallbacks.length; fallbacks++) {
messages = I18n.lookup(lookupInitialScope, this.prepareOptions({locale: fallbacks[fallback]}, options));
if (messages) {
break;
}
}
}
if (!messages && this.isValidNode(options, "defaultValue")) {
messages = options.defaultValue;
}
}
return messages;
};
// Merge serveral hash options, checking if value is set before
// overwriting any value. The precedence is from left to right.
//
// I18n.prepareOptions({name: "John Doe"}, {name: "Mary Doe", role: "user"});
// #=> {name: "John Doe", role: "user"}
//
I18n.prepareOptions = function() {
var options = {}
, opts
, count = arguments.length
;
for (var i = 0; i < count; i++) {
opts = arguments[i];
if (!opts) {
continue;
}
for (var key in opts) {
if (!this.isValidNode(options, key)) {
options[key] = opts[key];
}
}
}
return options;
};
I18n.interpolate = function(message, options) {
options = this.prepareOptions(options);
var matches = message.match(this.PLACEHOLDER)
, placeholder
, value
, name
;
if (!matches) {
return message;
}
for (var i = 0; placeholder = matches[i]; i++) {
name = placeholder.replace(this.PLACEHOLDER, "$1");
value = options[name];
if (!this.isValidNode(options, name)) {
value = "[missing " + placeholder + " value]";
}
regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}"));
message = message.replace(regex, value);
}
return message;
};
I18n.translate = function(scope, options) {
options = this.prepareOptions(options);
var translation = this.lookup(scope, options);
try {
if (typeof(translation) == "object") {
if (typeof(options.count) == "number") {
return this.pluralize(options.count, scope, options);
} else {
return translation;
}
} else {
return this.interpolate(translation, options);
}
} catch(err) {
return this.missingTranslation(scope);
}
};
I18n.localize = function(scope, value) {
switch (scope) {
case "currency":
return this.toCurrency(value);
case "number":
scope = this.lookup("number.format");
return this.toNumber(value, scope);
case "percentage":
return this.toPercentage(value);
default:
if (scope.match(/^(date|time)/)) {
return this.toTime(scope, value);
} else {
return value.toString();
}
}
};
I18n.parseDate = function(date) {
var matches, convertedDate;
// we have a date, so just return it.
if (typeof(date) == "object") {
return date;
};
// it matches the following formats:
// yyyy-mm-dd
// yyyy-mm-dd[ T]hh:mm::ss
// yyyy-mm-dd[ T]hh:mm::ss
// yyyy-mm-dd[ T]hh:mm::ssZ
// yyyy-mm-dd[ T]hh:mm::ss+0000
//
matches = date.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|\+0000)?/);
if (matches) {
for (var i = 1; i <= 6; i++) {
matches[i] = parseInt(matches[i], 10) || 0;
}
// month starts on 0
matches[2] -= 1;
if (matches[7]) {
convertedDate = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]));
} else {
convertedDate = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]);
}
} else if (typeof(date) == "number") {
// UNIX timestamp
convertedDate = new Date();
convertedDate.setTime(date);
} else if (date.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) {
// a valid javascript format with timezone info
convertedDate = new Date();
convertedDate.setTime(Date.parse(date))
} else {
// an arbitrary javascript string
convertedDate = new Date();
convertedDate.setTime(Date.parse(date));
}
return convertedDate;
};
I18n.toTime = function(scope, d) {
var date = this.parseDate(d)
, format = this.lookup(scope)
;
if (date.toString().match(/invalid/i)) {
return date.toString();
}
if (!format) {
return date.toString();
}
return this.strftime(date, format);
};
I18n.strftime = function(date, format) {
var options = this.lookup("date");
if (!options) {
return date.toString();
}
options.meridian = options.meridian || ["AM", "PM"];
var weekDay = date.getDay()
, day = date.getDate()
, year = date.getFullYear()
, month = date.getMonth() + 1
, hour = date.getHours()
, hour12 = hour
, meridian = hour > 11 ? 1 : 0
, secs = date.getSeconds()
, mins = date.getMinutes()
, offset = date.getTimezoneOffset()
, absOffsetHours = Math.floor(Math.abs(offset / 60))
, absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60)
, timezoneoffset = (offset > 0 ? "-" : "+") + (absOffsetHours.toString().length < 2 ? "0" + absOffsetHours : absOffsetHours) + (absOffsetMinutes.toString().length < 2 ? "0" + absOffsetMinutes : absOffsetMinutes)
;
if (hour12 > 12) {
hour12 = hour12 - 12;
} else if (hour12 === 0) {
hour12 = 12;
}
var padding = function(n) {
var s = "0" + n.toString();
return s.substr(s.length - 2);
};
var f = format;
f = f.replace("%a", options.abbr_day_names[weekDay]);
f = f.replace("%A", options.day_names[weekDay]);
f = f.replace("%b", options.abbr_month_names[month]);
f = f.replace("%B", options.month_names[month]);
f = f.replace("%d", padding(day));
f = f.replace("%e", day);
f = f.replace("%-d", day);
f = f.replace("%H", padding(hour));
f = f.replace("%-H", hour);
f = f.replace("%I", padding(hour12));
f = f.replace("%-I", hour12);
f = f.replace("%m", padding(month));
f = f.replace("%-m", month);
f = f.replace("%M", padding(mins));
f = f.replace("%-M", mins);
f = f.replace("%p", options.meridian[meridian]);
f = f.replace("%S", padding(secs));
f = f.replace("%-S", secs);
f = f.replace("%w", weekDay);
f = f.replace("%y", padding(year));
f = f.replace("%-y", padding(year).replace(/^0+/, ""));
f = f.replace("%Y", year);
f = f.replace("%z", timezoneoffset);
return f;
};
I18n.toNumber = function(number, options) {
options = this.prepareOptions(
options,
this.lookup("number.format"),
{precision: 3, separator: ".", delimiter: ",", strip_insignificant_zeros: false}
);
var negative = number < 0
, string = Math.abs(number).toFixed(options.precision).toString()
, parts = string.split(".")
, precision
, buffer = []
, formattedNumber
;
number = parts[0];
precision = parts[1];
while (number.length > 0) {
buffer.unshift(number.substr(Math.max(0, number.length - 3), 3));
number = number.substr(0, number.length -3);
}
formattedNumber = buffer.join(options.delimiter);
if (options.precision > 0) {
formattedNumber += options.separator + parts[1];
}
if (negative) {
formattedNumber = "-" + formattedNumber;
}
if (options.strip_insignificant_zeros) {
var regex = {
separator: new RegExp(options.separator.replace(/\./, "\\.") + "$")
, zeros: /0+$/
};
formattedNumber = formattedNumber
.replace(regex.zeros, "")
.replace(regex.separator, "")
;
}
return formattedNumber;
};
I18n.toCurrency = function(number, options) {
options = this.prepareOptions(
options,
this.lookup("number.currency.format"),
this.lookup("number.format"),
{unit: "$", precision: 2, format: "%u%n", delimiter: ",", separator: "."}
);
number = this.toNumber(number, options);
number = options.format
.replace("%u", options.unit)
.replace("%n", number)
;
return number;
};
I18n.toHumanSize = function(number, options) {
var kb = 1024
, size = number
, iterations = 0
, unit
, precision
;
while (size >= kb && iterations < 4) {
size = size / kb;
iterations += 1;
}
if (iterations === 0) {
unit = this.t("number.human.storage_units.units.byte", {count: size});
precision = 0;
} else {
unit = this.t("number.human.storage_units.units." + [null, "kb", "mb", "gb", "tb"][iterations]);
precision = (size - Math.floor(size) === 0) ? 0 : 1;
}
options = this.prepareOptions(
options,
{precision: precision, format: "%n%u", delimiter: ""}
);
number = this.toNumber(size, options);
number = options.format
.replace("%u", unit)
.replace("%n", number)
;
return number;
};
I18n.toPercentage = function(number, options) {
options = this.prepareOptions(
options,
this.lookup("number.percentage.format"),
this.lookup("number.format"),
{precision: 3, separator: ".", delimiter: ""}
);
number = this.toNumber(number, options);
return number + "%";
};
I18n.pluralizer = function(locale) {
pluralizer = this.pluralizationRules[locale];
if (pluralizer !== undefined) return pluralizer;
return this.pluralizationRules["en"];
};
I18n.findAndTranslateValidNode = function(keys, translation) {
for (i = 0; i < keys.length; i++) {
key = keys[i];
if (this.isValidNode(translation, key)) return translation[key];
}
return null;
};
I18n.pluralize = function(count, scope, options) {
var translation;
try {
translation = this.lookup(scope, options);
} catch (error) {}
if (!translation) {
return this.missingTranslation(scope);
}
var message;
options = this.prepareOptions(options);
options.count = count.toString();
pluralizer = this.pluralizer(this.currentLocale());
key = pluralizer(Math.abs(count));
keys = ((typeof key == "object") && (key instanceof Array)) ? key : [key];
message = this.findAndTranslateValidNode(keys, translation);
if (message == null) message = this.missingTranslation(scope, keys[0]);
return this.interpolate(message, options);
};
I18n.missingTranslation = function() {
var message = '[missing "' + this.currentLocale()
, count = arguments.length
;
for (var i = 0; i < count; i++) {
message += "." + arguments[i];
}
message += '" translation]';
return message;
};
I18n.currentLocale = function() {
return (I18n.locale || I18n.defaultLocale);
};
// shortcuts
I18n.t = I18n.translate;
I18n.l = I18n.localize;
I18n.p = I18n.pluralize;
/*!
* Pusher JavaScript Library v1.6.4
* http://pusherapp.com/
*
* Copyright 2010, New Bamboo
* Released under the MIT licence.
*/
if(typeof Function.prototype.scopedTo=="undefined")Function.prototype.scopedTo=function(a,b){var c=this;return function(){return c.apply(a,Array.prototype.slice.call(b||[]).concat(Array.prototype.slice.call(arguments)))}};
var Pusher=function(a,b){this.path="/app/"+a+"?client=js&version="+Pusher.VERSION;this.key=a;this.channels=new Pusher.Channels;this.global_channel=new Pusher.Channel("pusher_global_channel");this.global_channel.global=true;this.connected=this.secure=false;this.retry_counter=0;Pusher.isReady&&this.connect();Pusher.instances.push(this);b&&this.subscribe(b);this.bind("pusher:connection_established",function(c){this.connected=true;this.retry_counter=0;this.socket_id=c.socket_id;this.subscribeAll()}.scopedTo(this));
this.bind("pusher:connection_disconnected",function(){for(var c in this.channels.channels)this.channels.channels[c].disconnect()}.scopedTo(this));this.bind("pusher:error",function(c){Pusher.log("Pusher : error : "+c.message)})};Pusher.instances=[];
Pusher.prototype={channel:function(a){return this.channels.find(a)},connect:function(){var a="ws://"+Pusher.host+":"+Pusher.ws_port+this.path;if(this.secure==true)a="wss://"+Pusher.host+":"+Pusher.wss_port+this.path;Pusher.allow_reconnect=true;Pusher.log("Pusher : connecting : "+a);var b=this;if(window.WebSocket){this.connection=new WebSocket(a);this.connection.onmessage=function(){b.onmessage.apply(b,arguments)};this.connection.onclose=function(){b.onclose.apply(b,arguments)};this.connection.onopen=
function(){b.onopen.apply(b,arguments)}}else{this.connection={};setTimeout(function(){b.send_local_event("pusher:connection_failed",{})},3E3)}},toggle_secure:function(){if(this.secure==false){this.secure=true;Pusher.log("Pusher: switching to wss:// connection")}else{this.secure=false;Pusher.log("Pusher: switching to ws:// connection")}},disconnect:function(){Pusher.log("Pusher : disconnecting");Pusher.allow_reconnect=false;Pusher.retry_count=0;this.connection.close()},bind:function(a,b){this.global_channel.bind(a,
b);return this},bind_all:function(a){this.global_channel.bind_all(a);return this},subscribeAll:function(){for(var a in this.channels.channels)this.channels.channels.hasOwnProperty(a)&&this.subscribe(a)},subscribe:function(a){var b=this.channels.add(a);this.connected&&b.authorize(this,function(c){this.send_event("pusher:subscribe",{channel:a,auth:c.auth,channel_data:c.channel_data})}.scopedTo(this));return b},unsubscribe:function(a){this.channels.remove(a);this.connected&&this.send_event("pusher:unsubscribe",
{channel:a})},send_event:function(a,b){var c=JSON.stringify({event:a,data:b});Pusher.log("Pusher : sending event : ",c);this.connection.send(c);return this},send_local_event:function(a,b,c){b=Pusher.data_decorator(a,b);if(c){var d=this.channel(c);d&&d.dispatch_with_all(a,b)}this.global_channel.dispatch_with_all(a,b);Pusher.log("Pusher : event received : channel: "+c+"; event: "+a,b)},onmessage:function(a){a=Pusher.parser(a.data);if(!(a.socket_id&&a.socket_id==this.socket_id)){var b=a.event,c=Pusher.parser(a.data);
this.send_local_event(b,c,a.channel)}},wait_and_reconnect:function(a,b){setTimeout(function(){a();this.connect()}.scopedTo(this),b)},onclose:function(){var a=this;this.global_channel.dispatch("close",null);Pusher.log("Pusher: Socket closed");var b=5E3;if(this.connected==true){this.send_local_event("pusher:connection_disconnected",{});if(Pusher.allow_reconnect){Pusher.log("Pusher : Reconnecting in 5 seconds...");this.wait_and_reconnect(function(){},b)}}else{a.send_local_event("pusher:connection_failed",
{});if(this.retry_counter==0)b=100;this.retry_counter+=1;this.wait_and_reconnect(function(){a.toggle_secure()},b)}this.connected=false},onopen:function(){this.global_channel.dispatch("open",null)}};Pusher.Util={extend:function(a,b){for(var c in b)a[c]=b[c];return a}};Pusher.VERSION="1.6.4";Pusher.host="ws.pusherapp.com";Pusher.ws_port=80;Pusher.wss_port=443;Pusher.channel_auth_endpoint="/pusher/auth";Pusher.log=function(){};Pusher.data_decorator=function(a,b){return b};Pusher.allow_reconnect=true;
Pusher.channel_auth_transport="ajax";Pusher.parser=function(a){try{return JSON.parse(a)}catch(b){Pusher.log("Pusher : data attribute not valid JSON - you may wish to implement your own Pusher.parser");return a}};Pusher.isReady=false;Pusher.ready=function(){Pusher.isReady=true;for(var a=0;a0)return false;this.remove_member(a);this.dispatch_with_all("pusher:member_removed",a)}.scopedTo(this))},disconnect:function(){this._members_map={};this._members_count={}},acknowledge_subscription:function(a){this._members_map={};this._members_count={};for(var b=0;b0?_require(a,b):b()})();
/**
* jQuery Cookie plugin
*
* Copyright (c) 2010 Klaus Hartl (stilbuero.de)
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
*/
jQuery.cookie = function (key, value, options) {
// key and at least value given, set cookie...
if (arguments.length > 1 && String(value) !== "[object Object]") {
options = jQuery.extend({}, options);
if (value === null || value === undefined) {
options.expires = -1;
}
if (typeof options.expires === 'number') {
var days = options.expires, t = options.expires = new Date();
t.setDate(t.getDate() + days);
}
value = String(value);
return (document.cookie = [
encodeURIComponent(key), '=',
options.raw ? value : encodeURIComponent(value),
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
options.path ? '; path=' + options.path : '',
options.domain ? '; domain=' + options.domain : '',
options.secure ? '; secure' : ''
].join(''));
}
// key and possibly options given, get cookie...
options = value || {};
var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent;
return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null;
};
/*
* timeago: a jQuery plugin, version: 0.9.2 (2010-09-14)
* @requires jQuery v1.2.3 or later
*
* Timeago is a jQuery plugin that makes it easy to support automatically
* updating fuzzy timestamps (e.g. '4 minutes ago' or 'about 1 day ago').
*
* For usage and examples, visit:
* http://timeago.yarp.com/
*
* Licensed under the MIT:
* http://www.opensource.org/licenses/mit-license.php
*
* Copyright (c) 2008-2010, Ryan McGeary (ryanonjavascript -[at]- mcgeary [*dot*] org)
*/
(function($) {
$.timeago = function(timestamp) {
if (timestamp instanceof Date) return inWords(timestamp);
else if (typeof timestamp == 'string') return inWords($.timeago.parse(timestamp));
else return inWords($.timeago.datetime(timestamp));
};
var $t = $.timeago;
$.extend($.timeago, {
settings: {
refreshMillis: 3000,
allowFuture: true,
strings: {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: 'ago',
suffixFromNow: 'from now',
seconds: 'less than a minute',
minute: 'about a minute',
minutes: '%d minutes',
hour: 'about an hour',
hours: 'about %d hours',
day: 'a day',
days: '%d days',
month: 'about a month',
months: '%d months',
year: 'about a year',
years: '%d years',
numbers: []
}
},
distanceInWords: function(date) {
if(!date) {
return;
}
if(typeof date == 'string') {
date = $.timeago.parse(date);
}
return $.timeago.inWords($.timeago.distance(date));
},
inWords: function(distanceMillis) {
var $l = this.settings.strings;
var prefix = $l.prefixAgo;
var suffix = $l.suffixAgo;
if (this.settings.allowFuture) {
if (distanceMillis < 0) {
prefix = $l.prefixFromNow;
suffix = $l.suffixFromNow;
}
distanceMillis = Math.abs(distanceMillis);
}
var seconds = distanceMillis / 1000;
var minutes = seconds / 60;
var hours = minutes / 60;
var days = hours / 24;
var years = days / 365;
function substitute(stringOrFunction, number) {
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
var value = ($l.numbers && $l.numbers[number]) || number;
return string.replace(/%d/i, value);
}
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
seconds < 90 && substitute($l.minute, 1) ||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
minutes < 90 && substitute($l.hour, 1) ||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
hours < 48 && substitute($l.day, 1) ||
days < 30 && substitute($l.days, Math.floor(days)) ||
days < 60 && substitute($l.month, 1) ||
days < 365 && substitute($l.months, Math.floor(days / 30)) ||
years < 2 && substitute($l.year, 1) ||
substitute($l.years, Math.floor(years));
return $.trim([prefix, words, suffix].join(' '));
},
distance: function(date) {
return (this.now() - date.getTime());
},
now: function() {
return new Date().getTime();
},
parse: function(iso8601) {
var s = $.trim(iso8601);
s = s.replace(/\.\d\d\d+/,''); // remove milliseconds
s = s.replace(/-/,'/').replace(/-/,'/');
s = s.replace(/T/,' ').replace(/Z/,' UTC');
s = s.replace(/([\+-]\d\d)\:?(\d\d)/,' $1$2'); // -04:00 -> -0400
return new Date(s);
}
});
$.fn.timeago = function() {
this.each(function() {
var data = prepareData(this);
if (!isNaN(data.datetime)) {
$(this).text(inWords(data.datetime));
}
});
return this;
};
function prepareData(element) {
element = $(element);
if (!element.data('timeago') || (element.data('timeago').title != element.attr('title'))) {
element.data('timeago', { datetime: $t.parse(element.attr('title')), title: element.attr('title') });
}
return element.data('timeago');
}
function inWords(date) {
return $t.inWords(distance(date));
}
function distance(date) {
return $t.distance(date);
}
// fix for IE6 suckage
document.createElement('abbr');
document.createElement('time');
})(jQuery);
// ==========================================================================
// Project: SproutCore - 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)
// ==========================================================================
var get = Ember.get, set = Ember.set;
/**
Wether the browser supports HTML5 history.
*/
var supportsHistory = !!(window.history && window.history.pushState);
/**
Wether the browser supports the hashchange event.
*/
var supportsHashChange = ('onhashchange' in window) && (document.documentMode === undefined || document.documentMode > 7);
/**
@class
Route is a class used internally by Ember.routes. The routes defined by your
application are stored in a tree structure, and this is the class for the
nodes.
*/
var Route = Ember.Object.extend(
/** @scope Route.prototype */ {
target: null,
method: null,
staticRoutes: null,
dynamicRoutes: null,
wildcardRoutes: null,
add: function(parts, target, method) {
var part, nextRoute;
// clone the parts array because we are going to alter it
parts = Ember.copy(parts);
if (!parts || parts.length === 0) {
this.target = target;
this.method = method;
} else {
part = parts.shift();
// there are 3 types of routes
switch (part.slice(0, 1)) {
// 1. dynamic routes
case ':':
part = part.slice(1, part.length);
if (!this.dynamicRoutes) this.dynamicRoutes = {};
if (!this.dynamicRoutes[part]) this.dynamicRoutes[part] = this.constructor.create();
nextRoute = this.dynamicRoutes[part];
break;
// 2. wildcard routes
case '*':
part = part.slice(1, part.length);
if (!this.wildcardRoutes) this.wildcardRoutes = {};
nextRoute = this.wildcardRoutes[part] = this.constructor.create();
break;
// 3. static routes
default:
if (!this.staticRoutes) this.staticRoutes = {};
if (!this.staticRoutes[part]) this.staticRoutes[part] = this.constructor.create();
nextRoute = this.staticRoutes[part];
}
// recursively add the rest of the route
if (nextRoute) nextRoute.add(parts, target, method);
}
},
routeForParts: function(parts, params) {
var part, key, route;
// clone the parts array because we are going to alter it
parts = Ember.copy(parts);
// if parts is empty, we are done
if (!parts || parts.length === 0) {
return this.method ? this : null;
} else {
part = parts.shift();
// try to match a static route
if (this.staticRoutes && this.staticRoutes[part]) {
return this.staticRoutes[part].routeForParts(parts, params);
} else {
// else, try to match a dynamic route
for (key in this.dynamicRoutes) {
route = this.dynamicRoutes[key].routeForParts(parts, params);
if (route) {
params[key] = part;
return route;
}
}
// else, try to match a wilcard route
for (key in this.wildcardRoutes) {
parts.unshift(part);
params[key] = parts.join('/');
return this.wildcardRoutes[key].routeForParts(null, params);
}
// if nothing was found, it means that there is no match
return null;
}
}
}
});
/**
@class
Ember.routes manages the browser location. You can change the hash part of the
current location. The following code
Ember.routes.set('location', 'notes/edit/4');
will change the location to http://domain.tld/my_app#notes/edit/4. Adding
routes will register a handler that will be called whenever the location
changes and matches the route:
Ember.routes.add(':controller/:action/:id', MyApp, MyApp.route);
You can pass additional parameters in the location hash that will be relayed
to the route handler:
Ember.routes.set('location', 'notes/show/4?format=xml&language=fr');
The syntax for the location hash is described in the location property
documentation, and the syntax for adding handlers is described in the
add method documentation.
Browsers keep track of the locations in their history, so when the user
presses the 'back' or 'forward' button, the location is changed, Ember.route
catches it and calls your handler. Except for Internet Explorer versions 7
and earlier, which do not modify the history stack when the location hash
changes.
Ember.routes also supports HTML5 history, which uses a '/' instead of a '#'
in the URLs, so that all your website's URLs are consistent.
*/
var routes = Ember.routes = Ember.Object.create(
/** @scope Ember.routes.prototype */{
/**
Set this property to true if you want to use HTML5 history, if available on
the browser, instead of the location hash.
HTML 5 history uses the history.pushState method and the window's popstate
event.
By default it is false, so your URLs will look like:
http://domain.tld/my_app#notes/edit/4
If set to true and the browser supports pushState(), your URLs will look
like:
http://domain.tld/my_app/notes/edit/4
You will also need to make sure that baseURI is properly configured, as
well as your server so that your routes are properly pointing to your
SproutCore application.
@see http://dev.w3.org/html5/spec/history.html#the-history-interface
@property
@type {Boolean}
*/
wantsHistory: false,
/**
A read-only boolean indicating whether or not HTML5 history is used. Based
on the value of wantsHistory and the browser's support for pushState.
@see wantsHistory
@property
@type {Boolean}
*/
usesHistory: null,
/**
The base URI used to resolve routes (which are relative URLs). Only used
when usesHistory is equal to true.
The build tools automatically configure this value if you have the
html5_history option activated in the Buildfile:
config :my_app, :html5_history => true
Alternatively, it uses by default the value of the href attribute of the
tag of the HTML document. For example:
The value can also be customized before or during the exectution of the
main() method.
@see http://www.w3.org/TR/html5/semantics.html#the-base-element
@property
@type {String}
*/
baseURI: document.baseURI,
/** @private
A boolean value indicating whether or not the ping method has been called
to setup the Ember.routes.
@property
@type {Boolean}
*/
_didSetup: false,
/** @private
Internal representation of the current location hash.
@property
@type {String}
*/
_location: null,
/** @private
Routes are stored in a tree structure, this is the root node.
@property
@type {Route}
*/
_firstRoute: null,
/** @private
An internal reference to the Route class.
@property
*/
_Route: Route,
/** @private
Internal method used to extract and merge the parameters of a URL.
@returns {Hash}
*/
_extractParametersAndRoute: function(obj) {
var params = {},
route = obj.route || '',
separator, parts, i, len, crumbs, key;
separator = (route.indexOf('?') < 0 && route.indexOf('&') >= 0) ? '&' : '?';
parts = route.split(separator);
route = parts[0];
if (parts.length === 1) {
parts = [];
} else if (parts.length === 2) {
parts = parts[1].split('&');
} else if (parts.length > 2) {
parts.shift();
}
// extract the parameters from the route string
len = parts.length;
for (i = 0; i < len; ++i) {
crumbs = parts[i].split('=');
params[crumbs[0]] = crumbs[1];
}
// overlay any parameter passed in obj
for (key in obj) {
if (obj.hasOwnProperty(key) && key !== 'route') {
params[key] = '' + obj[key];
}
}
// build the route
parts = [];
for (key in params) {
parts.push([key, params[key]].join('='));
}
params.params = separator + parts.join('&');
params.route = route;
return params;
},
/**
The current location hash. It is the part in the browser's location after
the '#' mark.
The following code
Ember.routes.set('location', 'notes/edit/4');
will change the location to http://domain.tld/my_app#notes/edit/4 and call
the correct route handler if it has been registered with the add method.
You can also pass additional parameters. They will be relayed to the route
handler. For example, the following code
Ember.routes.add(':controller/:action/:id', MyApp, MyApp.route);
Ember.routes.set('location', 'notes/show/4?format=xml&language=fr');
will change the location to
http://domain.tld/my_app#notes/show/4?format=xml&language=fr and call the
MyApp.route method with the following argument:
{ route: 'notes/show/4',
params: '?format=xml&language=fr',
controller: 'notes',
action: 'show',
id: '4',
format: 'xml',
language: 'fr' }
The location can also be set with a hash, the following code
Ember.routes.set('location',
{ route: 'notes/edit/4', format: 'xml', language: 'fr' });
will change the location to
http://domain.tld/my_app#notes/show/4?format=xml&language=fr.
The 'notes/show/4&format=xml&language=fr' syntax for passing parameters,
using a '&' instead of a '?', as used in SproutCore 1.0 is still supported.
@property
@type {String}
*/
location: function(key, value) {
this._skipRoute = false;
return this._extractLocation(key, value);
}.property(),
_extractLocation: function(key, value) {
var crumbs, encodedValue;
if (value !== undefined) {
if (value === null) {
value = '';
}
if (typeof(value) === 'object') {
crumbs = this._extractParametersAndRoute(value);
value = crumbs.route + crumbs.params;
}
if (!Ember.empty(value) || (this._location && this._location !== value)) {
encodedValue = encodeURI(value);
if (this.usesHistory) {
if (encodedValue.length > 0) {
encodedValue = '/' + encodedValue;
}
window.history.pushState(null, null, get(this, 'baseURI') + encodedValue);
} else {
window.location.hash = encodedValue;
}
}
this._location = value;
}
return this._location;
},
/**
You usually don't need to call this method. It is done automatically after
the application has been initialized.
It registers for the hashchange event if available. If not, it creates a
timer that looks for location changes every 150ms.
*/
ping: function() {
var that;
if (!this._didSetup) {
this._didSetup = true;
if (get(this, 'wantsHistory') && supportsHistory) {
this.usesHistory = true;
popState();
jQuery(window).bind('popstate', popState);
} else {
this.usesHistory = false;
if (supportsHashChange) {
hashChange();
jQuery(window).bind('hashchange', hashChange);
} else {
// we don't use a Ember.Timer because we don't want
// a run loop to be triggered at each ping
that = this;
this._invokeHashChange = function() {
that.hashChange();
setTimeout(that._invokeHashChange, 100);
};
this._invokeHashChange();
}
}
}
},
/**
Adds a route handler. Routes have the following format:
- 'users/show/5' is a static route and only matches this exact string,
- ':action/:controller/:id' is a dynamic route and the handler will be
called with the 'action', 'controller' and 'id' parameters passed in a
hash,
- '*url' is a wildcard route, it matches the whole route and the handler
will be called with the 'url' parameter passed in a hash.
Route types can be combined, the following are valid routes:
- 'users/:action/:id'
- ':controller/show/:id'
- ':controller/ *url' (ignore the space, because of jslint)
@param {String} route the route to be registered
@param {Object} target the object on which the method will be called, or
directly the function to be called to handle the route
@param {Function} method the method to be called on target to handle the
route, can be a function or a string
*/
add: function(route, target, method) {
if (!this._didSetup) {
Ember.run.once(this, 'ping');
}
if (method === undefined && Ember.typeOf(target) === 'function') {
method = target;
target = null;
} else if (Ember.typeOf(method) === 'string') {
method = target[method];
}
if (!this._firstRoute) this._firstRoute = Route.create();
this._firstRoute.add(route.split('/'), target, method);
return this;
},
/**
Observer of the 'location' property that calls the correct route handler
when the location changes.
*/
locationDidChange: function() {
this.trigger();
}.observes('location'),
/**
Triggers a route even if already in that route (does change the location, if it
is not already changed, as well).
If the location is not the same as the supplied location, this simply lets "location"
handle it (which ends up coming back to here).
*/
trigger: function() {
var location = get(this, 'location'),
params, route;
if (this._firstRoute) {
params = this._extractParametersAndRoute({ route: location });
location = params.route;
delete params.route;
delete params.params;
route = this.getRoute(location, params);
if (route && route.method) {
route.method.call(route.target || this, params);
}
}
},
getRoute: function(route, params) {
var firstRoute = this._firstRoute;
if (params == null) {
params = {}
}
return firstRoute.routeForParts(route.split('/'), params);
},
exists: function(route, params) {
route = this.getRoute(route, params);
return route != null && route.method != null;
}
});
/**
Event handler for the hashchange event. Called automatically by the browser
if it supports the hashchange event, or by our timer if not.
*/
function hashChange(event) {
var loc = window.location.hash;
// Remove the '#' prefix
loc = (loc && loc.length > 0) ? loc.slice(1, loc.length) : '';
if (!jQuery.browser.mozilla) {
// because of bug https://bugzilla.mozilla.org/show_bug.cgi?id=483304
loc = decodeURI(loc);
}
if (get(routes, 'location') !== loc && !routes._skipRoute) {
Ember.run.once(function() {
set(routes, 'location', loc);
});
}
routes._skipRoute = false;
}
function popState(event) {
var base = get(routes, 'baseURI'),
loc = document.location.href;
if (loc.slice(0, base.length) === base) {
// Remove the base prefix and the extra '/'
loc = loc.slice(base.length + 1, loc.length);
if (get(routes, 'location') !== loc && !routes._skipRoute) {
Ember.run.once(function() {
set(routes, 'location', loc);
});
}
}
routes._skipRoute = false;
}
/*
Highcharts JS v2.2.5 (2012-06-08)
(c) 2009-2011 Torstein H?nsi
License: www.highcharts.com/license
*/
(function(){function u(a,b){var c;a||(a={});for(c in b)a[c]=b[c];return a}function la(){for(var a=0,b=arguments,c=b.length,d={};a-1?b.split(".")[1].length:0):a=isNaN(b=M(b))?2:b;var b=a,c=c===void 0?e.decimalPoint:c,d=d===void 0?e.thousandsSep:d,e=f<0?"-":"",a=String(w(f=M(+f||0).toFixed(b))),g=a.length>3?a.length%3:0;return e+(g?a.substr(0,g)+d:"")+
a.substr(g).replace(/(\d{3})(?=\d)/g,"$1"+d)+(b?c+M(f-a).toFixed(b).slice(2):"")}function sa(a,b){return Array((b||2)+1-String(a).length).join(0)+a}function gb(a,b,c,d){var e,c=o(c,1);e=a/c;b||(b=[1,2,2.5,5,10],d&&d.allowDecimals===!1&&(c===1?b=[1,2,5,10]:c<=0.1&&(b=[1/c])));for(d=0;d=D[hb]&&(i.setMilliseconds(0),i.setSeconds(b>=D[Ya]?0:j*W(i.getSeconds()/j)));if(b>=D[Ya])i[wb](b>=D[Ma]?0:j*W(i[ib]()/j));
if(b>=D[Ma])i[xb](b>=D[oa]?0:j*W(i[jb]()/j));if(b>=D[oa])i[kb](b>=D[Na]?1:j*W(i[Oa]()/j));b>=D[Na]&&(i[yb](b>=D[ta]?0:j*W(i[$a]()/j)),h=i[ab]());b>=D[ta]&&(h-=h%j,i[zb](h));if(b===D[Za])i[kb](i[Oa]()-i[lb]()+o(d,1));d=1;h=i[ab]();for(var k=i.getTime(),l=i[$a](),m=i[Oa](),i=g?0:(864E5+i.getTimezoneOffset()*6E4)%864E5;kc&&(c=a[b]);return c}function Ba(a,b){for(var c in a)a[c]&&a[c]!==b&&a[c].destroy&&a[c].destroy(),delete a[c]}function Qa(a){cb||(cb=S(ia));
a&&cb.appendChild(a);cb.innerHTML=""}function mb(a,b){var c="Highcharts error #"+a+": www.highcharts.com/errors/"+a;if(b)throw c;else N.console&&console.log(c)}function ja(a){return parseFloat(a.toPrecision(14))}function ua(a,b){Ra=o(a,b.animation)}function Bb(){var a=V.global.useUTC,b=a?"getUTC":"get",c=a?"setUTC":"set";bb=a?Date.UTC:function(a,b,c,g,h,i){return(new Date(a,b,o(c,1),o(g,0),o(h,0),o(i,0))).getTime()};ib=b+"Minutes";jb=b+"Hours";lb=b+"Day";Oa=b+"Date";$a=b+"Month";ab=b+"FullYear";wb=
c+"Minutes";xb=c+"Hours";kb=c+"Date";yb=c+"Month";zb=c+"FullYear"}function va(){}function Sa(a,b,c){this.axis=a;this.pos=b;this.type=c||"";this.isNew=!0;c||this.addLabel()}function nb(a,b){this.axis=a;if(b)this.options=b,this.id=b.id;return this}function Cb(a,b,c,d,e){var f=a.chart.inverted;this.axis=a;this.isNegative=c;this.options=b;this.x=d;this.stack=e;this.alignOptions={align:b.align||(f?c?"left":"right":"center"),verticalAlign:b.verticalAlign||(f?"middle":c?"bottom":"top"),y:o(b.y,f?4:c?14:
-6),x:o(b.x,f?c?-6:6:0)};this.textAlign=b.textAlign||(f?c?"right":"left":"center")}function ob(){this.init.apply(this,arguments)}function pb(a,b){var c=b.borderWidth,d=b.style,e=b.shared,f=w(d.padding);this.chart=a;this.options=b;d.padding=0;this.crosshairs=[];this.currentY=this.currentX=0;this.tooltipIsHidden=!0;this.label=a.renderer.label("",0,0,null,null,null,b.useHTML,null,"tooltip").attr({padding:f,fill:b.backgroundColor,"stroke-width":c,r:b.borderRadius,zIndex:8}).css(d).hide().add();ga||this.label.shadow(b.shadow);
this.shared=e}function Db(a,b){var c=ga?"":b.chart.zoomType;this.zoomX=/x/.test(c);this.zoomY=/y/.test(c);this.options=b;this.chart=a;this.init(a,b.tooltip)}function qb(a){this.init(a)}function rb(a,b){var c,d=a.series;a.series=null;c=C(V,a);c.series=a.series=d;var d=c.chart,e=d.margin,e=aa(e)?e:[e,e,e,e];this.optionsMarginTop=o(d.marginTop,e[0]);this.optionsMarginRight=o(d.marginRight,e[1]);this.optionsMarginBottom=o(d.marginBottom,e[2]);this.optionsMarginLeft=o(d.marginLeft,e[3]);this.runChartClick=
(e=d.events)&&!!e.click;this.callback=b;this.isResizing=0;this.options=c;this.axes=[];this.series=[];this.hasCartesianSeries=d.showAxes;this.init(e)}var A,B=document,N=window,L=Math,t=L.round,W=L.floor,wa=L.ceil,x=L.max,O=L.min,M=L.abs,X=L.cos,da=L.sin,xa=L.PI,Eb=xa*2/360,ya=navigator.userAgent,La=/msie/i.test(ya)&&!N.opera,Ca=B.documentMode===8,Fb=/AppleWebKit/.test(ya),Gb=/Firefox/.test(ya),Da=!!B.createElementNS&&!!B.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect,Rb=Gb&&parseInt(ya.split("Firefox/")[1],
10)<4,ga=!Da&&!La&&!!B.createElement("canvas").getContext,Ta,ea=B.documentElement.ontouchstart!==A,Hb={},sb=0,cb,V,db,Ra,Ua,D,Sb=function(){},ia="div",U="none",tb="rgba(192,192,192,"+(Da?1.0E-6:0.0020)+")",vb="millisecond",hb="second",Ya="minute",Ma="hour",oa="day",Za="week",Na="month",ta="year",bb,ib,jb,lb,Oa,$a,ab,wb,xb,kb,yb,zb,Y={};N.Highcharts={};db=function(a,b,c){if(!s(b)||isNaN(b))return"Invalid date";var a=o(a,"%Y-%m-%d %H:%M:%S"),d=new Date(b),e,f=d[jb](),g=d[lb](),h=d[Oa](),i=d[$a](),j=
d[ab](),k=V.lang,l=k.weekdays,b={a:l[g].substr(0,3),A:l[g],d:sa(h),e:h,b:k.shortMonths[i],B:k.months[i],m:sa(i+1),y:j.toString().substr(2,2),Y:j,H:sa(f),I:sa(f%12||12),l:f%12||12,M:sa(d[ib]()),p:f<12?"AM":"PM",P:f<12?"am":"pm",S:sa(d.getSeconds()),L:sa(t(b%1E3),3)};for(e in b)a=a.replace("%"+e,b[e]);return c?a.substr(0,1).toUpperCase()+a.substr(1):a};Ab.prototype={wrapColor:function(a){if(this.color>=a)this.color=0},wrapSymbol:function(a){if(this.symbol>=a)this.symbol=0}};D=la(vb,1,hb,1E3,Ya,6E4,
Ma,36E5,oa,864E5,Za,6048E5,Na,2592E6,ta,31556952E3);Ua={init:function(a,b,c){var b=b||"",d=a.shift,e=b.indexOf("C")>-1,f=e?7:3,g,b=b.split(" "),c=[].concat(c),h,i,j=function(a){for(g=a.length;g--;)a[g]==="M"&&a.splice(g+1,0,a[g+1],a[g+2],a[g+1],a[g+2])};e&&(j(b),j(c));a.isArea&&(h=b.splice(b.length-6,6),i=c.splice(c.length-6,6));if(d<=c.length/f)for(;d--;)c=[].concat(c).splice(0,f).concat(c);a.shift=0;if(b.length)for(a=c.length;b.length{point.key} ',pointFormat:'{series.name} : {point.y} ',shadow:!0,shared:ga,snap:ea?25:10,style:{color:"#333333",fontSize:"12px",padding:"5px",whiteSpace:"nowrap"}},credits:{enabled:!0,text:"Highcharts.com",href:"http://www.highcharts.com",position:{align:"right",x:-10,verticalAlign:"bottom",y:-5},
style:{cursor:"pointer",color:"#909090",fontSize:"10px"}}};var Z=V.plotOptions,T=Z.line;Bb();var pa=function(a){var b=[],c;(function(a){(c=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/.exec(a))?b=[w(c[1]),w(c[2]),w(c[3]),parseFloat(c[4],10)]:(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(a))&&(b=[w(c[1],16),w(c[2],16),w(c[3],16),1])})(a);return{get:function(c){return b&&!isNaN(b[0])?c==="rgb"?"rgb("+b[0]+","+b[1]+","+b[2]+")":c==="a"?b[3]:
"rgba("+b.join(",")+")":a},brighten:function(a){if(Ka(a)&&a!==0){var c;for(c=0;c<3;c++)b[c]+=w(a*255),b[c]<0&&(b[c]=0),b[c]>255&&(b[c]=255)}return this},setOpacity:function(a){b[3]=a;return this}}};va.prototype={init:function(a,b){this.element=b==="span"?S(b):B.createElementNS("http://www.w3.org/2000/svg",b);this.renderer=a;this.attrSetters={}},animate:function(a,b,c){b=o(b,Ra,!0);Fa(this);if(b){b=C(b);if(c)b.complete=c;eb(this,a,b)}else this.attr(a),c&&c()},attr:function(a,b){var c,d,e,f,g=this.element,
h=g.nodeName,i=this.renderer,j,k=this.attrSetters,l=this.shadows,m,p,q=this;ra(a)&&s(b)&&(c=a,a={},a[c]=b);if(ra(a))c=a,h==="circle"?c={x:"cx",y:"cy"}[c]||c:c==="strokeWidth"&&(c="stroke-width"),q=z(g,c)||this[c]||0,c!=="d"&&c!=="visibility"&&(q=parseFloat(q));else for(c in a)if(j=!1,d=a[c],e=k[c]&&k[c](d,c),e!==!1){e!==A&&(d=e);if(c==="d")d&&d.join&&(d=d.join(" ")),/(NaN| {2}|^$)/.test(d)&&(d="M 0 0");else if(c==="x"&&h==="text"){for(e=0;em&&/[ \-]/.test(b.innerText)&&(F(b,{width:m+"px",display:"block",whiteSpace:"normal"}),k=m),m=a.fontMetrics(b.style.fontSize).b,r=p<0&&-k,y=q<0&&-l,fa=p*q<0,r+=q*m*(fa?1-h:h),y-=p*m*(j?fa?h:1-h:1),i&&(r-=k*h*(p<0?-1:1),j&&(y-=l*h*(q<0?-1:1)),F(b,{textAlign:g})),this.xCorr=r,this.yCorr=y;F(b,{left:e+r+"px",top:f+y+"px"});this.cTT=$}}else this.alignOnAdd=
!0},updateTransform:function(){var a=this.translateX||0,b=this.translateY||0,c=this.inverted,d=this.rotation,e=[];c&&(a+=this.attr("width"),b+=this.attr("height"));(a||b)&&e.push("translate("+a+","+b+")");c?e.push("rotate(90) scale(-1,1)"):d&&e.push("rotate("+d+" "+(this.x||0)+" "+(this.y||0)+")");e.length&&z(this.element,"transform",e.join(" "))},toFront:function(){var a=this.element;a.parentNode.appendChild(a);return this},align:function(a,b,c){a?(this.alignOptions=a,this.alignByTranslate=b,c||
this.renderer.alignedObjects.push(this)):(a=this.alignOptions,b=this.alignByTranslate);var c=o(c,this.renderer),d=a.align,e=a.verticalAlign,f=(c.x||0)+(a.x||0),g=(c.y||0)+(a.y||0),h={};/^(right|center)$/.test(d)&&(f+=(c.width-(a.width||0))/{right:1,center:2}[d]);h[b?"translateX":"x"]=t(f);/^(bottom|middle)$/.test(e)&&(g+=(c.height-(a.height||0))/({bottom:1,middle:2}[e]||1));h[b?"translateY":"y"]=t(g);this[this.placed?"animate":"attr"](h);this.placed=!0;this.alignAttr=h;return this},getBBox:function(a){var b,
c,d=this.rotation;c=this.element;var e=d*Eb;if(c.namespaceURI==="http://www.w3.org/2000/svg"||this.renderer.forExport){try{b=c.getBBox?u({},c.getBBox()):{width:c.offsetWidth,height:c.offsetHeight}}catch(f){}if(!b||b.width<0)b={width:0,height:0};a=b.width;c=b.height;if(d)b.width=M(c*da(e))+M(a*X(e)),b.height=M(c*X(e))+M(a*da(e))}else b=this.htmlGetBBox(a);return b},show:function(){return this.attr({visibility:"visible"})},hide:function(){return this.attr({visibility:"hidden"})},add:function(a){var b=
this.renderer,c=a||b,d=c.element||b.box,e=d.childNodes,f=this.element,g=z(f,"zIndex"),h;this.parentInverted=a&&a.inverted;this.textStr!==void 0&&b.buildText(this);if(g)c.handleZ=!0,g=w(g);if(c.handleZ)for(c=0;cg||!s(g)&&s(b))){d.insertBefore(f,a);h=!0;break}h||d.appendChild(f);this.added=!0;E(this,"add");return this},safeRemoveChild:function(a){var b=a.parentNode;b&&b.removeChild(a)},destroy:function(){var a=this,b=a.element||{},c=a.shadows,d=a.box,
e,f;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=null;Fa(a);if(a.clipPath)a.clipPath=a.clipPath.destroy();if(a.stops){for(f=0;f/g,'').replace(/<(i|em)>/g,'').replace(//g," ").split(//g),d=b.childNodes,e=/style="([^"]+)"/,f=/href="([^"]+)"/,g=z(b,"x"),h=a.styles,i=h&&w(h.width),j=h&&h.lineHeight,k,h=d.length,l=[];h--;)b.removeChild(d[h]);i&&!a.added&&this.box.appendChild(b);c[c.length-1]===""&&c.pop();n(c,function(c,d){var h,fa=0,r,c=c.replace(//g," |||");h=c.split("|||");n(h,function(c){if(c!==""||h.length===1){var m={},n=B.createElementNS("http://www.w3.org/2000/svg",
"tspan");e.test(c)&&z(n,"style",c.match(e)[1].replace(/(;| |^)color([ :])/,"$1fill$2"));f.test(c)&&(z(n,"onclick",'location.href="'+c.match(f)[1]+'"'),F(n,{cursor:"pointer"}));c=(c.replace(/<(.|\n)*?>/g,"")||" ").replace(/</g,"<").replace(/>/g,">");n.appendChild(B.createTextNode(c));fa?m.dx=3:m.x=g;if(!fa){if(d){!Da&&a.renderer.forExport&&F(n,{display:"block"});r=N.getComputedStyle&&w(N.getComputedStyle(k,null).getPropertyValue("line-height"));if(!r||isNaN(r)){var o;if(!(o=j))if(!(o=k.offsetHeight))l[d]=
b.getBBox().height,o=t(l[d]-(l[d-1]||0))||18;r=o}z(n,"dy",r)}k=n}z(n,m);b.appendChild(n);fa++;if(i)for(var c=c.replace(/-/g,"- ").split(" "),H=[];c.length||H.length;)o=a.getBBox().width,m=o>i,!m||c.length===1?(c=H,H=[],c.length&&(n=B.createElementNS("http://www.w3.org/2000/svg","tspan"),z(n,{dy:j||16,x:g}),b.appendChild(n),o>i&&(i=o))):(n.removeChild(n.firstChild),H.unshift(c.pop())),c.length&&n.appendChild(B.createTextNode(c.join(" ").replace(/- /g,"-")))}})})},button:function(a,b,c,d,e,f,g){var h=
this.label(a,b,c),i=0,j,k,l,m,p,a={x1:0,y1:0,x2:0,y2:1},e=C(la("stroke-width",1,"stroke","#999","fill",la("linearGradient",a,"stops",[[0,"#FFF"],[1,"#DDD"]]),"r",3,"padding",3,"style",la("color","black")),e);l=e.style;delete e.style;f=C(e,la("stroke","#68A","fill",la("linearGradient",a,"stops",[[0,"#FFF"],[1,"#ACF"]])),f);m=f.style;delete f.style;g=C(e,la("stroke","#68A","fill",la("linearGradient",a,"stops",[[0,"#9BD"],[1,"#CDF"]])),g);p=g.style;delete g.style;I(h.element,"mouseenter",function(){h.attr(f).css(m)});
I(h.element,"mouseleave",function(){j=[e,f,g][i];k=[l,m,p][i];h.attr(j).css(k)});h.setState=function(a){(i=a)?a===2&&h.attr(g).css(p):h.attr(e).css(l)};return h.on("click",function(){d.call(h)}).attr(e).css(u({cursor:"default"},l))},crispLine:function(a,b){a[1]===a[4]&&(a[1]=a[4]=t(a[1])+b%2/2);a[2]===a[5]&&(a[2]=a[5]=t(a[2])+b%2/2);return a},path:function(a){var b={fill:U};Ja(a)?b.d=a:aa(a)&&u(b,a);return this.createElement("path").attr(b)},circle:function(a,b,c){a=aa(a)?a:{x:a,y:b,r:c};return this.createElement("circle").attr(a)},
arc:function(a,b,c,d,e,f){if(aa(a))b=a.y,c=a.r,d=a.innerR,e=a.start,f=a.end,a=a.x;return this.symbol("arc",a||0,b||0,c||0,c||0,{innerR:d||0,start:e||0,end:f||0})},rect:function(a,b,c,d,e,f){e=aa(a)?a.r:e;e=this.createElement("rect").attr({rx:e,ry:e,fill:U});return e.attr(aa(a)?a:e.crisp(f,a,b,x(c,0),x(d,0)))},setSize:function(a,b,c){var d=this.alignedObjects,e=d.length;this.width=a;this.height=b;for(this.boxWrapper[o(c,!0)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b=
this.createElement("g");return s(a)?b.attr({"class":"highcharts-"+a}):b},image:function(a,b,c,d,e){var f={preserveAspectRatio:U};arguments.length>1&&u(f,{x:b,y:c,width:d,height:e});f=this.createElement("image").attr(f);f.element.setAttributeNS?f.element.setAttributeNS("http://www.w3.org/1999/xlink","href",a):f.element.setAttribute("hc-svg-href",a);return f},symbol:function(a,b,c,d,e,f){var g,h=this.symbols[a],h=h&&h(t(b),t(c),d,e,f),i=/^url\((.*?)\)$/,j,k;h?(g=this.path(h),u(g,{symbolName:a,x:b,y:c,
width:d,height:e}),f&&u(g,f)):i.test(a)&&(k=function(a,b){a.attr({width:b[0],height:b[1]});a.alignByTranslate||a.translate(-t(b[0]/2),-t(b[1]/2))},j=a.match(i)[1],a=Hb[j],g=this.image(j).attr({x:b,y:c}),a?k(g,a):(g.attr({width:0,height:0}),S("img",{onload:function(){k(g,Hb[j]=[this.width,this.height])},src:j})));return g},symbols:{circle:function(a,b,c,d){var e=0.166*c;return["M",a+c/2,b,"C",a+c+e,b,a+c+e,b+d,a+c/2,b+d,"C",a-e,b+d,a-e,b,a+c/2,b,"Z"]},square:function(a,b,c,d){return["M",a,b,"L",a+
c,b,a+c,b+d,a,b+d,"Z"]},triangle:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d,a,b+d,"Z"]},"triangle-down":function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c/2,b+d,"Z"]},diamond:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d/2,a+c/2,b+d,a,b+d/2,"Z"]},arc:function(a,b,c,d,e){var f=e.start,c=e.r||c||d,g=e.end-1.0E-6,d=e.innerR,h=e.open,i=X(f),j=da(f),k=X(g),g=da(g),e=e.end-f ');if(b)c=b===ia||b==="span"||b==="img"?c.join(""):a.prepVML(c),this.element=S(c);this.renderer=a;this.attrSetters={}},add:function(a){var b=this.renderer,c=this.element,d=b.box,d=a?a.element||a:d;a&&a.inverted&&b.invertChild(c,d);Ca&&d.gVis==="hidden"&&F(c,{visibility:"hidden"});d.appendChild(c);this.added=!0;this.alignOnAdd&&
!this.deferUpdateTransform&&this.updateTransform();E(this,"add");return this},toggleChildren:function(a,b){for(var c=a.childNodes,d=c.length;d--;)F(c[d],{visibility:b}),c[d].nodeName==="DIV"&&this.toggleChildren(c[d],b)},updateTransform:va.prototype.htmlUpdateTransform,attr:function(a,b){var c,d,e,f=this.element||{},g=f.style,h=f.nodeName,i=this.renderer,j=this.symbolName,k,l=this.shadows,m,p=this.attrSetters,q=this;ra(a)&&s(b)&&(c=a,a={},a[c]=b);if(ra(a))c=a,q=c==="strokeWidth"||c==="stroke-width"?
this.strokeweight:this[c];else for(c in a)if(d=a[c],m=!1,e=p[c]&&p[c](d,c),e!==!1&&d!==null){e!==A&&(d=e);if(j&&/^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(c))k||(this.symbolAttr(a),k=!0),m=!0;else if(c==="d"){d=d||[];this.d=d.join(" ");e=d.length;for(m=[];e--;)m[e]=Ka(d[e])?t(d[e]*10)-5:d[e]==="Z"?"x":d[e];d=m.join(" ")||"x";f.path=d;if(l)for(e=l.length;e--;)l[e].path=l[e].cutOff?this.cutOffPath(d,l[e].cutOff):d;m=!0}else if(c==="zIndex"||c==="visibility"){if(Ca&&c==="visibility"&&
h==="DIV")f.gVis=d,this.toggleChildren(f,d),d==="visible"&&(d=null);d&&(g[c]=d);m=!0}else if(c==="width"||c==="height")d=x(0,d),this[c]=d,this.updateClipping?(this[c]=d,this.updateClipping()):g[c]=d,m=!0;else if(c==="x"||c==="y")this[c]=d,g[{x:"left",y:"top"}[c]]=d;else if(c==="class")f.className=d;else if(c==="stroke")d=i.color(d,f,c),c="strokecolor";else if(c==="stroke-width"||c==="strokeWidth")f.stroked=d?!0:!1,c="strokeweight",this[c]=d,Ka(d)&&(d+="px");else if(c==="dashstyle")(f.getElementsByTagName("stroke")[0]||
S(i.prepVML([" "]),null,null,f))[c]=d||"solid",this.dashstyle=d,m=!0;else if(c==="fill")h==="SPAN"?g.color=d:(f.filled=d!==U?!0:!1,d=i.color(d,f,c),c="fillcolor");else if(h==="shape"&&c==="rotation")this[c]=d;else if(c==="translateX"||c==="translateY"||c==="rotation")this[c]=d,this.updateTransform(),m=!0;else if(c==="text")this.bBox=null,f.innerHTML=d,m=!0;if(l&&c==="visibility")for(e=l.length;e--;)l[e].style[c]=d;m||(Ca?f[c]=d:z(f,c,d))}return q},clip:function(a){var b=this,c=a.members,d=
b.element,e=d.parentNode;c.push(b);b.destroyClip=function(){za(c,b)};e&&e.className==="highcharts-tracker"&&!Ca&&F(d,{visibility:"hidden"});return b.css(a.getCSS(b))},css:va.prototype.htmlCss,safeRemoveChild:function(a){a.parentNode&&Qa(a)},destroy:function(){this.destroyClip&&this.destroyClip();return va.prototype.destroy.apply(this)},empty:function(){for(var a=this.element.childNodes,b=a.length,c;b--;)c=a[b],c.parentNode.removeChild(c)},on:function(a,b){this.element["on"+a]=function(){var a=N.event;
a.target=a.srcElement;b(a)};return this},cutOffPath:function(a,b){var c,a=a.split(/[ ,]/);c=a.length;if(c===9||c===11)a[c-4]=a[c-2]=w(a[c-2])-10*b;return a.join(" ")},shadow:function(a,b,c){var d=[],e=this.element,f=this.renderer,g,h=e.style,i,j=e.path,k,l;j&&typeof j.value!=="string"&&(j="x");l=j;if(a){for(a=1;a<=3;a++){k=7-2*a;c&&(l=this.cutOffPath(j.value,k+0.5));i=[' '];g=
S(f.prepVML(i),null,{left:w(h.left)+1,top:w(h.top)+1});if(c)g.cutOff=k+1;i=[' '];S(f.prepVML(i),null,null,g);b?b.element.appendChild(g):e.parentNode.insertBefore(g,e);d.push(g)}this.shadows=d}return this}};ka=ca(va,ka);var ha={Element:ka,isIE8:ya.indexOf("MSIE 8.0")>-1,init:function(a,b,c){var d,e;this.alignedObjects=[];d=this.createElement(ia);e=d.element;e.style.position="relative";a.appendChild(d.element);this.box=e;this.boxWrapper=d;this.setSize(b,c,
!1);if(!B.namespaces.hcv)B.namespaces.add("hcv","urn:schemas-microsoft-com:vml"),B.createStyleSheet().cssText="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "},isHidden:function(){return!this.box.offsetWidth},clipRect:function(a,b,c,d){var e=this.createElement();return u(e,{members:[],left:a,top:b,width:c,height:d,getCSS:function(a){var b=a.inverted,c=this.top,d=this.left,e=d+this.width,k=c+this.height,c={clip:"rect("+t(b?d:c)+"px,"+t(b?k:
e)+"px,"+t(b?e:k)+"px,"+t(b?c:d)+"px)"};!b&&Ca&&a.element.nodeName!=="IMG"&&u(c,{width:e+"px",height:k+"px"});return c},updateClipping:function(){n(e.members,function(a){a.css(e.getCSS(a))})}})},color:function(a,b,c){var d,e=/^rgba/,f,g=U;a&&a.linearGradient?f="gradient":a&&a.radialGradient&&(f="pattern");if(f){var h,i,j=a.linearGradient||a.radialGradient,k,l,m,p,q,o,r="",a=a.stops,y,s=[];l=a[0];y=a[a.length-1];l[0]>0&&a.unshift([0,l[1]]);y[0]<1&&a.push([1,y[1]]);n(a,function(a,b){e.test(a[1])?(d=
pa(a[1]),h=d.get("rgb"),i=d.get("a")):(h=a[1],i=1);s.push(a[0]*100+"% "+h);b?(p=i,q=h):(m=i,o=h)});f==="gradient"?(k=j.x1||j[0]||0,a=j.y1||j[1]||0,l=j.x2||j[2]||0,j=j.y2||j[3]||0,k=90-L.atan((j-a)/(l-k))*180/xa):(g=j.r*2,r='src="http://code.highcharts.com/gfx/radial-gradient.png" size="'+g+","+g+'" origin="0.5,0.5" position="'+j.cx+","+j.cy+'" color2="'+o+'" ',g=q);c==="fill"?(c=[' '],
S(this.prepVML(c),null,null,b)):g=h}else if(e.test(a)&&b.tagName!=="IMG")d=pa(a),c=["<",c,' opacity="',d.get("a"),'"/>'],S(this.prepVML(c),null,null,b),g=d.get("rgb");else{b=b.getElementsByTagName(c);if(b.length)b[0].opacity=1;g=a}return g},prepVML:function(a){var b=this.isIE8,a=a.join("");b?(a=a.replace("/>",' xmlns="urn:schemas-microsoft-com:vml" />'),a=a.indexOf('style="')===-1?a.replace("/>",' style="display:inline-block;behavior:url(#default#VML);" />'):a.replace('style="','style="display:inline-block;behavior:url(#default#VML);')):
a=a.replace("<","1&&f.css({left:b,top:c,width:d,
height:e});return f},rect:function(a,b,c,d,e,f){if(aa(a))b=a.y,c=a.width,d=a.height,f=a.strokeWidth,a=a.x;var g=this.symbol("rect");g.r=e;return g.attr(g.crisp(f,a,b,x(c,0),x(d,0)))},invertChild:function(a,b){var c=b.style;F(a,{flip:"x",left:w(c.width)-1,top:w(c.height)-1,rotation:-90})},symbols:{arc:function(a,b,c,d,e){var f=e.start,g=e.end,h=e.r||c||d,c=X(f),d=da(f),i=X(g),j=da(g),k=e.innerR,l=0.08/h,m=k&&0.1/k||0;if(g-f===0)return["x"];else 2*xa-g+fj&&(c=!1)):h+k>m&&(h=m-k,d&&h+l0&&b.height>0){f=C({align:c&&k&&"center",x:c?!k&&4:10,verticalAlign:!c&&k&&"middle",y:c?k?16:10:k?6:-4,rotation:c&&!k&&90},f);if(!g)a.label=g=u.text(f.text,0,0).attr({align:f.textAlign||f.align,rotation:f.rotation,
zIndex:y}).css(f.style).add();b=[q[1],q[4],o(q[6],q[1])];q=[q[2],q[5],o(q[7],q[2])];c=Pa(b);k=Pa(q);g.align(f,!1,{x:c,y:k,width:Aa(b)-c,height:Aa(q)-k});g.show()}else g&&g.hide();return a},destroy:function(){za(this.axis.plotLinesAndBands,this);Ba(this,this.axis)}};Cb.prototype={destroy:function(){Ba(this,this.axis)},setTotal:function(a){this.cum=this.total=a},render:function(a){var b=this.options.formatter.call(this);this.label?this.label.attr({text:b,visibility:"hidden"}):this.label=this.axis.chart.renderer.text(b,
0,0).css(this.options.style).attr({align:this.textAlign,rotation:this.options.rotation,visibility:"hidden"}).add(a)},setOffset:function(a,b){var c=this.axis,d=c.chart,e=d.inverted,f=this.isNegative,g=c.translate(this.total,0,0,0,1),c=c.translate(0),c=M(g-c),h=d.xAxis[0].translate(this.x)+a,d=d.plotHeight,e={x:e?f?g:g-c:h,y:e?d-h-b:f?d-g-c:d-g,width:e?c:b,height:e?b:c};this.label&&this.label.align(this.alignOptions,null,e).attr({visibility:"visible"})}};ob.prototype={defaultOptions:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L",
second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%e. %b",week:"%e. %b",month:"%b '%y",year:"%Y"},endOnTick:!1,gridLineColor:"#C0C0C0",labels:G,lineColor:"#C0D0E0",lineWidth:1,minPadding:0.01,maxPadding:0.01,minorGridLineColor:"#E0E0E0",minorGridLineWidth:1,minorTickColor:"#A0A0A0",minorTickLength:2,minorTickPosition:"outside",startOfWeek:1,startOnTick:!1,tickColor:"#C0D0E0",tickLength:5,tickmarkPlacement:"between",tickPixelInterval:100,tickPosition:"outside",tickWidth:1,title:{align:"middle",style:{color:"#6D869F",
fontWeight:"bold"}},type:"linear"},defaultYAxisOptions:{endOnTick:!0,gridLineWidth:1,tickPixelInterval:72,showLastLabel:!0,labels:{align:"right",x:-8,y:3},lineWidth:0,maxPadding:0.05,minPadding:0.05,startOnTick:!0,tickWidth:0,title:{rotation:270,text:"Y-values"},stackLabels:{enabled:!1,formatter:function(){return this.total},style:G.style}},defaultLeftAxisOptions:{labels:{align:"right",x:-8,y:null},title:{rotation:270}},defaultRightAxisOptions:{labels:{align:"left",x:8,y:null},title:{rotation:90}},
defaultBottomAxisOptions:{labels:{align:"center",x:0,y:14},title:{rotation:0}},defaultTopAxisOptions:{labels:{align:"center",x:0,y:-5},title:{rotation:0}},init:function(a,b){var c=b.isX;this.horiz=a.inverted?!c:c;this.xOrY=(this.isXAxis=c)?"x":"y";this.opposite=b.opposite;this.side=this.horiz?this.opposite?0:2:this.opposite?1:3;this.setOptions(b);var d=this.options,e=d.type,f=e==="datetime";this.labelFormatter=d.labels.formatter||this.defaultLabelFormatter;this.staggerLines=this.horiz&&d.labels.staggerLines;
this.userOptions=b;this.minPixelPadding=0;this.chart=a;this.reversed=d.reversed;this.categories=d.categories;this.isLog=e==="logarithmic";this.isLinked=s(d.linkedTo);this.isDatetimeAxis=f;this.ticks={};this.minorTicks={};this.plotLinesAndBands=[];this.alternateBands={};this.len=0;this.minRange=this.userMinRange=d.minRange||d.maxZoom;this.range=d.range;this.offset=d.offset||0;this.stacks={};this.min=this.max=null;var g,d=this.options.events;a.axes.push(this);a[c?"xAxis":"yAxis"].push(this);this.series=
[];if(a.inverted&&c&&this.reversed===A)this.reversed=!0;this.removePlotLine=this.removePlotBand=this.removePlotBandOrLine;this.addPlotLine=this.addPlotBand=this.addPlotBandOrLine;for(g in d)I(this,g,d[g]);if(this.isLog)this.val2lin=ma,this.lin2val=ba},setOptions:function(a){this.options=C(this.defaultOptions,this.isXAxis?{}:this.defaultYAxisOptions,[this.defaultTopAxisOptions,this.defaultRightAxisOptions,this.defaultBottomAxisOptions,this.defaultLeftAxisOptions][this.side],a)},defaultLabelFormatter:function(){var a=
this.axis,b=this.value,c=a.tickInterval,d=this.dateTimeLabelFormat;return a.categories?b:d?db(d,b):c%1E6===0?b/1E6+"M":c%1E3===0?b/1E3+"k":b>=1E3?Xa(b,0):Xa(b,-1)},getSeriesExtremes:function(){var a=this,b=a.chart,c=a.stacks,d=[],e=[],f;a.dataMin=a.dataMax=null;n(a.series,function(g){if(g.visible||!b.options.chart.ignoreHiddenSeries){var h=g.options,i,j,k,l,m,p,q,n,r,y=h.threshold,t,u=[],v=0;if(a.isLog&&y<=0)y=h.threshold=null;if(a.isXAxis){if(h=g.xData,h.length)a.dataMin=O(o(a.dataMin,h[0]),Pa(h)),
a.dataMax=x(o(a.dataMax,h[0]),Aa(h))}else{var H,J,K,C=g.cropped,Ha=g.xAxis.getExtremes(),w=!!g.modifyValue;i=h.stacking;a.usePercentage=i==="percent";if(i)m=h.stack,l=g.type+o(m,""),p="-"+l,g.stackKey=l,j=d[l]||[],d[l]=j,k=e[p]||[],e[p]=k;if(a.usePercentage)a.dataMin=0,a.dataMax=99;h=g.processedXData;q=g.processedYData;t=q.length;for(f=0;f=Ha.min&&(h[f-1]||n)<=Ha.max))if(n=r.length)for(;n--;)r[n]!==null&&(u[v++]=r[n]);else u[v++]=r;if(!a.usePercentage&&u.length)a.dataMin=O(o(a.dataMin,u[0]),Pa(u)),a.dataMax=x(o(a.dataMax,u[0]),Aa(u));if(s(y))if(a.dataMin>=y)a.dataMin=y,a.ignoreMinPadding=!0;else if(a.dataMaxe+this.width)l=
!0}else if(c=e,h=k-this.right,gf+this.height)l=!0;return l?null:d.renderer.crispLine(["M",c,g,"L",h,i],b||0)},getPlotBandPath:function(a,b){var c=this.getPlotLinePath(b),d=this.getPlotLinePath(a);d&&c?d.push(c[4],c[5],c[1],c[2]):d=null;return d},getLinearTickPositions:function(a,b,c){for(var d,b=ja(W(b/a)*a),c=ja(wa(c/a)*a),e=[];b<=c;){e.push(b);b=ja(b+a);if(b===d)break;d=b}return e},getLogTickPositions:function(a,b,c,d){var e=this.options,f=this.len,g=[];if(!d)this._minorAutoInterval=null;
if(a>=0.5)a=t(a),g=this.getLinearTickPositions(a,b,c);else if(a>=0.08)for(var f=W(b),h,i,j,k,l,e=a>0.3?[1,2,4]:a>0.15?[1,2,4,6,8]:[1,2,3,4,5,6,7,8,9];fb&&g.push(k),k>c&&(l=!0),k=j}else if(b=ba(b),c=ba(c),a=e[d?"minorTickInterval":"tickInterval"],a=o(a==="auto"?null:a,this._minorAutoInterval,(c-b)*(e.tickPixelInterval/(d?5:1))/((d?f/this.tickPositions.length:f)||1)),a=gb(a,null,L.pow(10,W(L.log(a)/L.LN10))),g=Ea(this.getLinearTickPositions(a,
b,c),ma),!d)this._minorAutoInterval=a/5;if(!d)this.tickInterval=a;return g},getMinorTickPositions:function(){var a=this.tickPositions,b=this.minorTickInterval,c=[],d,e;if(this.isLog){e=a.length;for(d=1;d=this.minRange,f,g,h,i,j;if(this.isXAxis&&this.minRange===A&&
!this.isLog)s(a.min)||s(a.max)?this.minRange=null:(n(this.series,function(a){i=a.xData;for(g=j=a.xIncrement?1:i.length-1;g>0;g--)if(h=i[g]-i[g-1],f===A||h0||!b.ignoreMaxPadding))b.max+=c*j}b.tickInterval=b.min===b.max||b.min===void 0||b.max===void 0?1:h&&!l&&m===b.linkedParent.options.tickPixelInterval?b.linkedParent.tickInterval:o(l,p?1:(b.max-b.min)*m/(b.len||1));g&&!a&&n(b.series,function(a){a.processData(b.min!==b.oldMin||b.max!==b.oldMax)});
b.setAxisTranslation();b.beforeSetTickPositions&&b.beforeSetTickPositions();if(b.postProcessTickInterval)b.tickInterval=b.postProcessTickInterval(b.tickInterval);if(!f&&!e&&(a=L.pow(10,W(L.log(b.tickInterval)/L.LN10)),!s(d.tickInterval)))b.tickInterval=gb(b.tickInterval,null,a,d);b.minorTickInterval=d.minorTickInterval==="auto"&&b.tickInterval?b.tickInterval/5:d.minorTickInterval;b.tickPositions=i=d.tickPositions||i&&i.apply(b,[b.min,b.max]);if(!i)i=f?(b.getNonLinearTimeTicks||Pb)(Ob(b.tickInterval,
d.units),b.min,b.max,d.startOfWeek,b.ordinalPositions,b.closestPointRange,!0):e?b.getLogTickPositions(b.tickInterval,b.min,b.max):b.getLinearTickPositions(b.tickInterval,b.min,b.max),b.tickPositions=i;if(!h)e=i[0],f=i[i.length-1],d.startOnTick?b.min=e:b.min>e&&i.shift(),d.endOnTick?b.max=f:b.maxb[d]&&this.options.alignTicks!==!1)b[d]=
c.length;a.maxTicks=b},adjustTickAmount:function(){var a=this.xOrY,b=this.tickPositions,c=this.chart.maxTicks;if(c&&c[a]&&!this.isDatetimeAxis&&!this.categories&&!this.isLinked&&this.options.alignTicks!==!1){var d=this.tickAmount,e=b.length;this.tickAmount=a=c[a];if(ea||a===null?a=c:b =a.min&&b<=a.max)j[b]||(j[b]=new Sa(a,b)),r&&j[b].isNew&&j[b].render(c,!0),j[b].isActive=!0,j[b].render(c)}),p&&n(g,function(b,c){if(c%2===0&&b 1||
M(b-c.currentY)>1?function(){c.move(a,b)}:null},hide:function(){if(!this.tooltipIsHidden){var a=this.chart.hoverPoints;this.label.hide();a&&n(a,function(a){a.setState()});this.chart.hoverPoints=null;this.tooltipIsHidden=!0}},hideCrosshairs:function(){n(this.crosshairs,function(a){a&&a.hide()})},getAnchor:function(a,b){var c,d=this.chart,e=d.inverted,f=0,g=0,a=na(a);c=a[0].tooltipPos;c||(n(a,function(a){f+=a.plotX;g+=a.plotLow?(a.plotLow+a.plotHigh)/2:a.plotY}),f/=a.length,g/=a.length,c=[e?d.plotWidth-
g:f,this.shared&&!e&&a.length>1&&b?b.chartY-d.plotTop:e?d.plotHeight-f:g]);return Ea(c,t)},getPosition:function(a,b,c){var d=this.chart,e=d.plotLeft,f=d.plotTop,g=d.plotWidth,h=d.plotHeight,i=o(this.options.distance,12),j=c.plotX,c=c.plotY,d=j+e+(d.inverted?i:-a-i),k=c-b+f+15,l;d<7&&(d=e+j+i);d+a>e+g&&(d-=d+a-(e+g),k=c-b+f-i,l=!0);k=k&&c<=k+b&&(k=c+f+i));k+b>f+h&&(k=x(f,f+h-b-i));return{x:d,y:k}},refresh:function(a,b){function c(){var a=this.points||na(this),b=a[0].series,c;c=[b.tooltipHeaderFormatter(a[0].key)];
n(a,function(a){b=a.series;c.push(b.tooltipFormatter&&b.tooltipFormatter(a)||a.point.tooltipFormatter(b.tooltipOptions.pointFormat))});c.push(f.footerFormat||"");return c.join("")}var d=this.chart,e=this.label,f=this.options,g,h,i,j={},k,l=[];k=f.formatter||c;var j=d.hoverPoints,m,p=f.crosshairs;i=this.shared;h=this.getAnchor(a,b);g=h[0];h=h[1];i&&(!a.series||!a.series.noSharedTooltip)?(j&&n(j,function(a){a.setState()}),d.hoverPoints=a,n(a,function(a){a.setState("hover");l.push(a.getLabelConfig())}),
j={x:a[0].category,y:a[0].y},j.points=l,a=a[0]):j=a.getLabelConfig();k=k.call(j);j=a.series;i=i||!j.isCartesian||j.tooltipOutsidePlot||d.isInsidePlot(g,h);k===!1||!i?this.hide():(this.tooltipIsHidden&&e.show(),e.attr({text:k}),m=f.borderColor||a.color||j.color||"#606060",e.attr({stroke:m}),e=(f.positioner||this.getPosition).call(this,e.width,e.height,{plotX:g,plotY:h}),this.move(t(e.x),t(e.y)),this.tooltipIsHidden=!1);if(p){p=na(p);for(e=p.length;e--;)if(i=a.series[e?"yAxis":"xAxis"],p[e]&&i)if(i=
i.getPlotLinePath(e?o(a.stackY,a.y):a.x,1),this.crosshairs[e])this.crosshairs[e].attr({d:i,visibility:"visible"});else{j={"stroke-width":p[e].width||1,stroke:p[e].color||"#C0C0C0",zIndex:p[e].zIndex||2};if(p[e].dashStyle)j.dashstyle=p[e].dashStyle;this.crosshairs[e]=d.renderer.path(i).attr(j).add()}}E(d,"tooltipRefresh",{text:k,x:g+d.plotLeft,y:h+d.plotTop,borderColor:m})},tick:function(){this.tooltipTick&&this.tooltipTick()}};Db.prototype={normalizeMouseEvent:function(a){var b,c,d,a=a||N.event;if(!a.target)a.target=
a.srcElement;if(a.originalEvent)a=a.originalEvent;if(a.event)a=a.event;d=a.touches?a.touches.item(0):a;this.chartPosition=b=Jb(this.chart.container);d.pageX===A?(c=a.x,b=a.y):(c=d.pageX-b.left,b=d.pageY-b.top);return u(a,{chartX:t(c),chartY:t(b)})},getMouseCoordinates:function(a){var b={xAxis:[],yAxis:[]},c=this.chart;n(c.axes,function(d){var e=d.isXAxis;b[e?"xAxis":"yAxis"].push({axis:d,value:d.translate((c.inverted?!e:e)?a.chartX-c.plotLeft:c.plotHeight-a.chartY+c.plotTop,!0)})});return b},onmousemove:function(a){var b=
this.chart,c=b.series,d,e,f=b.hoverPoint,g=b.hoverSeries,h,i,j=b.chartWidth,k=b.inverted?b.plotHeight+b.plotTop-a.chartY:a.chartX-b.plotLeft;if(b.tooltip&&this.options.tooltip.shared&&(!g||!g.noSharedTooltip)){e=[];h=c.length;for(i=0;ij&&e.splice(h,1);if(e.length&&e[0].plotX!==this.hoverX)b.tooltip.refresh(e,
a),this.hoverX=e[0].plotX}if(g&&g.tracker&&(d=g.tooltipPoints[k])&&d!==f)d.onMouseOver()},resetTracker:function(a){var b=this.chart,c=b.hoverSeries,d=b.hoverPoint,e=b.hoverPoints||d,b=b.tooltip;(a=a&&b&&e)&&na(e)[0].plotX===A&&(a=!1);if(a)b.refresh(e);else{if(d)d.onMouseOut();if(c)c.onMouseOut();b&&(b.hide(),b.hideCrosshairs());this.hoverX=null}},setDOMEvents:function(){function a(){if(b.selectionMarker){var f={xAxis:[],yAxis:[]},g=b.selectionMarker.getBBox(),h=g.x-c.plotLeft,l=g.y-c.plotTop,m;e&&
(n(c.axes,function(a){if(a.options.zoomEnabled!==!1){var b=a.isXAxis,d=c.inverted?!b:b,e=a.translate(d?h:c.plotHeight-l-g.height,!0,0,0,1),d=a.translate(d?h+g.width:c.plotHeight-l,!0,0,0,1);!isNaN(e)&&!isNaN(d)&&(f[b?"xAxis":"yAxis"].push({axis:a,min:O(e,d),max:x(e,d)}),m=!0)}}),m&&E(c,"selection",f,function(a){c.zoom(a)}));b.selectionMarker=b.selectionMarker.destroy()}if(c)F(d,{cursor:"auto"}),c.cancelClick=e,c.mouseIsDown=e=!1;P(B,ea?"touchend":"mouseup",a)}var b=this,c=b.chart,d=c.container,e,
f=b.zoomX&&!c.inverted||b.zoomY&&c.inverted,g=b.zoomY&&!c.inverted||b.zoomX&&c.inverted;b.hideTooltipOnMouseMove=function(a){Kb(a);b.chartPosition&&c.hoverSeries&&c.hoverSeries.isCartesian&&!c.isInsidePlot(a.pageX-b.chartPosition.left-c.plotLeft,a.pageY-b.chartPosition.top-c.plotTop)&&b.resetTracker()};b.hideTooltipOnMouseLeave=function(){b.resetTracker();b.chartPosition=null};d.onmousedown=function(d){d=b.normalizeMouseEvent(d);!ea&&d.preventDefault&&d.preventDefault();c.mouseIsDown=!0;c.cancelClick=
!1;c.mouseDownX=b.mouseDownX=d.chartX;b.mouseDownY=d.chartY;I(B,ea?"touchend":"mouseup",a)};var h=function(a){if(!a||!(a.touches&&a.touches.length>1)){a=b.normalizeMouseEvent(a);if(!ea)a.returnValue=!1;var d=a.chartX,h=a.chartY,l=!c.isInsidePlot(d-c.plotLeft,h-c.plotTop);ea&&a.type==="touchstart"&&(z(a.target,"isTracker")?c.runTrackerClick||a.preventDefault():!c.runChartClick&&!l&&a.preventDefault());if(l)dc.plotLeft+c.plotWidth&&(d=c.plotLeft+c.plotWidth),hc.plotTop+c.plotHeight&&(h=c.plotTop+c.plotHeight);if(c.mouseIsDown&&a.type!=="touchstart"){if(e=Math.sqrt(Math.pow(b.mouseDownX-d,2)+Math.pow(b.mouseDownY-h,2)),e>10){a=c.isInsidePlot(b.mouseDownX-c.plotLeft,b.mouseDownY-c.plotTop);if(c.hasCartesianSeries&&(b.zoomX||b.zoomY)&&a&&!b.selectionMarker)b.selectionMarker=c.renderer.rect(c.plotLeft,c.plotTop,f?1:c.plotWidth,g?1:c.plotHeight,0).attr({fill:b.options.chart.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add();if(b.selectionMarker&&
f){var m=d-b.mouseDownX;b.selectionMarker.attr({width:M(m),x:(m>0?0:m)+b.mouseDownX})}b.selectionMarker&&g&&(h-=b.mouseDownY,b.selectionMarker.attr({height:M(h),y:(h>0?0:h)+b.mouseDownY}));a&&!b.selectionMarker&&b.options.chart.panning&&c.pan(d)}}else if(!l)b.onmousemove(a);return l||!c.hasCartesianSeries}};d.onmousemove=h;I(d,"mouseleave",b.hideTooltipOnMouseLeave);I(B,"mousemove",b.hideTooltipOnMouseMove);d.ontouchstart=function(a){if(b.zoomX||b.zoomY)d.onmousedown(a);h(a)};d.ontouchmove=h;d.ontouchend=
function(){e&&b.resetTracker()};d.onclick=function(a){var d=c.hoverPoint,e,f,a=b.normalizeMouseEvent(a);a.cancelBubble=!0;if(!c.cancelClick)d&&(z(a.target,"isTracker")||z(a.target.parentNode,"isTracker"))?(e=d.plotX,f=d.plotY,u(d,{pageX:b.chartPosition.left+c.plotLeft+(c.inverted?c.plotWidth-f:e),pageY:b.chartPosition.top+c.plotTop+(c.inverted?c.plotHeight-e:f)}),E(d.series,"click",u(a,{point:d})),d.firePointEvent("click",a)):(u(a,b.getMouseCoordinates(a)),c.isInsidePlot(a.chartX-c.plotLeft,a.chartY-
c.plotTop)&&E(c,"click",a))}},destroy:function(){var a=this.chart,b=a.container;if(a.trackerGroup)a.trackerGroup=a.trackerGroup.destroy();P(b,"mouseleave",this.hideTooltipOnMouseLeave);P(B,"mousemove",this.hideTooltipOnMouseMove);b.onclick=b.onmousedown=b.onmousemove=b.ontouchstart=b.ontouchend=b.ontouchmove=null;clearInterval(this.tooltipInterval)},init:function(a,b){if(!a.trackerGroup)a.trackerGroup=a.renderer.g("tracker").attr({zIndex:9}).add();if(b.enabled)a.tooltip=new pb(a,b),this.tooltipInterval=
setInterval(function(){a.tooltip.tick()},32);this.setDOMEvents()}};qb.prototype={init:function(a){var b=this,c=b.options=a.options.legend;if(c.enabled){var d=c.itemStyle,e=o(c.padding,8),f=c.itemMarginTop||0;b.baseline=w(d.fontSize)+3+f;b.itemStyle=d;b.itemHiddenStyle=C(d,c.itemHiddenStyle);b.itemMarginTop=f;b.padding=e;b.initialItemX=e;b.initialItemY=e-5;b.maxItemWidth=0;b.chart=a;b.itemHeight=0;b.lastLineHeight=0;b.render();I(b.chart,"endResize",function(){b.positionCheckboxes()})}},colorizeItem:function(a,
b){var c=this.options,d=a.legendItem,e=a.legendLine,f=a.legendSymbol,g=this.itemHiddenStyle.color,c=b?c.itemStyle.color:g,g=b?a.color:g;d&&d.css({fill:c});e&&e.attr({stroke:g});f&&f.attr({stroke:g,fill:g})},positionItem:function(a){var b=this.options,c=b.symbolPadding,b=!b.rtl,d=a._legendItemPos,e=d[0],d=d[1],f=a.checkbox;a.legendGroup&&a.legendGroup.translate(b?e:this.legendWidth-e-2*c-4,d);if(f)f.x=e,f.y=d},destroyItem:function(a){var b=a.checkbox;n(["legendItem","legendLine","legendSymbol","legendGroup"],
function(b){a[b]&&a[b].destroy()});b&&Qa(a.checkbox)},destroy:function(){var a=this.group,b=this.box;if(b)this.box=b.destroy();if(a)this.group=a.destroy()},positionCheckboxes:function(){var a=this;n(a.allItems,function(b){var c=b.checkbox,d=a.group.alignAttr;c&&F(c,{left:d.translateX+b.legendItemWidth+c.x-20+"px",top:d.translateY+c.y+3+"px"})})},renderItem:function(a){var q;var b=this,c=b.chart,d=c.renderer,e=b.options,f=e.layout==="horizontal",g=e.symbolWidth,h=e.symbolPadding,i=b.itemStyle,j=b.itemHiddenStyle,
k=b.padding,l=!e.rtl,m=e.width,p=e.itemMarginBottom||0,n=b.itemMarginTop,o=b.initialItemX,r=a.legendItem,s=a.series||a,t=s.options,u=t.showCheckbox;if(!r&&(a.legendGroup=d.g("legend-item").attr({zIndex:1}).add(b.scrollGroup),s.drawLegendSymbol(b,a),a.legendItem=r=d.text(e.labelFormatter.call(a),l?g+h:-h,b.baseline,e.useHTML).css(C(a.visible?i:j)).attr({align:l?"left":"right",zIndex:2}).add(a.legendGroup),a.legendGroup.on("mouseover",function(){a.setState("hover");r.css(b.options.itemHoverStyle)}).on("mouseout",
function(){r.css(a.visible?i:j);a.setState()}).on("click",function(b){var c=function(){a.setVisible()},b={browserEvent:b};a.firePointEvent?a.firePointEvent("legendItemClick",b,c):E(a,"legendItemClick",b,c)}),b.colorizeItem(a,a.visible),t&&u))a.checkbox=S("input",{type:"checkbox",checked:a.selected,defaultChecked:a.selected},e.itemCheckboxStyle,c.container),I(a.checkbox,"click",function(b){E(a,"checkboxClick",{checked:b.target.checked},function(){a.select()})});d=r.getBBox();q=a.legendItemWidth=e.itemWidth||
g+h+d.width+k+(u?20:0),e=q;b.itemHeight=g=d.height;if(f&&b.itemX-o+e>(m||c.chartWidth-2*k-o))b.itemX=o,b.itemY+=n+b.lastLineHeight+p,b.lastLineHeight=0;b.maxItemWidth=x(b.maxItemWidth,e);b.lastItemY=n+b.itemY+p;b.lastLineHeight=x(g,b.lastLineHeight);a._legendItemPos=[b.itemX,b.itemY];f?b.itemX+=e:(b.itemY+=n+g+p,b.lastLineHeight=g);b.offsetWidth=m||x(f?b.itemX-o:e,b.offsetWidth)},render:function(){var a=this,b=a.chart,c=b.renderer,d=a.group,e,f,g,h,i=a.box,j=a.options,k=a.padding,l=j.borderWidth,
m=j.backgroundColor;a.itemX=a.initialItemX;a.itemY=a.initialItemY;a.offsetWidth=0;a.lastItemY=0;if(!d)a.group=d=c.g("legend").attr({zIndex:7}).add(),a.contentGroup=c.g().attr({zIndex:1}).add(d),a.scrollGroup=c.g().add(a.contentGroup),a.clipRect=c.clipRect(0,0,9999,b.chartHeight),a.contentGroup.clip(a.clipRect);e=[];n(b.series,function(a){var b=a.options;b.showInLegend&&(e=e.concat(a.legendItems||(b.legendType==="point"?a.data:a)))});Qb(e,function(a,b){return(a.options.legendIndex||0)-(b.options.legendIndex||
0)});j.reversed&&e.reverse();a.allItems=e;a.display=f=!!e.length;n(e,function(b){a.renderItem(b)});g=j.width||a.offsetWidth;h=a.lastItemY+a.lastLineHeight;h=a.handleOverflow(h);if(l||m){g+=k;h+=k;if(i){if(g>0&&h>0)i[i.isNew?"attr":"animate"](i.crisp(null,null,null,g,h)),i.isNew=!1}else a.box=i=c.rect(0,0,g,h,j.borderRadius,l||0).attr({stroke:j.borderColor,"stroke-width":l||0,fill:m||U}).add(d).shadow(j.shadow),i.isNew=!0;i[f?"show":"hide"]()}a.legendWidth=g;a.legendHeight=h;n(e,function(b){a.positionItem(b)});
f&&d.align(u({width:g,height:h},j),!0,b.spacingBox);b.isResizing||this.positionCheckboxes()},handleOverflow:function(a){var b=this,c=this.chart,d=c.renderer,e=this.options,f=e.y,f=c.spacingBox.height+(e.verticalAlign==="top"?-f:f)-this.padding,g=e.maxHeight,h=this.clipRect,i=e.navigation,j=o(i.animation,!0),k=i.arrowSize||12,l=this.nav;e.layout==="horizontal"&&(f/=2);g&&(f=O(f,g));if(a>f){this.clipHeight=c=f-20;this.pageCount=wa(a/c);this.currentPage=o(this.currentPage,1);this.fullHeight=a;h.attr({height:c});
if(!l)this.nav=l=d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle",0,0,k,k).on("click",function(){b.scroll(-1,j)}).add(l),this.pager=d.text("",15,10).css(i.style).add(l),this.down=d.symbol("triangle-down",0,0,k,k).on("click",function(){b.scroll(1,j)}).add(l);b.scroll(0);a=f}else l&&(h.attr({height:c.chartHeight}),l.hide(),this.scrollGroup.attr({translateY:1}));return a},scroll:function(a,b){var c=this.pageCount,d=this.currentPage+a,e=this.clipHeight,f=this.options.navigation,g=f.activeColor,
f=f.inactiveColor,h=this.pager,i=this.padding;d>c&&(d=c);if(d>0)b!==A&&ua(b,this.chart),this.nav.attr({translateX:i,translateY:e+7,visibility:"visible"}),this.up.attr({fill:d===1?f:g}).css({cursor:d===1?"default":"pointer"}),h.attr({text:d+"/"+this.pageCount}),this.down.attr({x:18+this.pager.getBBox().width,fill:d===c?f:g}).css({cursor:d===c?"default":"pointer"}),this.scrollGroup.animate({translateY:-O(e*(d-1),this.fullHeight-e+i)+1}),h.attr({text:d+"/"+c}),this.currentPage=d}};rb.prototype={initSeries:function(a){var b=
this.options.chart,b=new Y[a.type||b.type||b.defaultSeriesType];b.init(this,a);return b},addSeries:function(a,b,c){var d=this;a&&(ua(c,d),b=o(b,!0),E(d,"addSeries",{options:a},function(){d.initSeries(a);d.isDirtyLegend=!0;b&&d.redraw()}))},isInsidePlot:function(a,b){return a>=0&&a<=this.plotWidth&&b>=0&&b<=this.plotHeight},adjustTickAmounts:function(){this.options.chart.alignTicks!==!1&&n(this.axes,function(a){a.adjustTickAmount()});this.maxTicks=null},redraw:function(a){var b=this.axes,c=this.series,
d=this.tracker,e=this.legend,f=this.isDirtyLegend,g,h=this.isDirtyBox,i=c.length,j=i,k=this.clipRect,l=this.renderer,m=l.isHidden();ua(a,this);for(m&&this.cloneRenderTo();j--;)if(a=c[j],a.isDirty&&a.options.stacking){g=!0;break}if(g)for(j=i;j--;)if(a=c[j],a.options.stacking)a.isDirty=!0;n(c,function(a){a.isDirty&&a.options.legendType==="point"&&(f=!0)});if(f&&e.options.enabled)e.render(),this.isDirtyLegend=!1;if(this.hasCartesianSeries){if(!this.isResizing)this.maxTicks=null,n(b,function(a){a.setScale()});
this.adjustTickAmounts();this.getMargins();n(b,function(a){if(a.isDirtyExtremes)a.isDirtyExtremes=!1,E(a,"afterSetExtremes",a.getExtremes());if(a.isDirty||h||g)a.redraw(),h=!0})}h&&(this.drawChartBox(),k&&(Fa(k),k.animate({width:this.plotSizeX,height:this.plotSizeY+1})));n(c,function(a){a.isDirty&&a.visible&&(!a.isCartesian||a.xAxis)&&a.redraw()});d&&d.resetTracker&&d.resetTracker(!0);l.draw();E(this,"redraw");m&&this.cloneRenderTo(!0)},showLoading:function(a){var b=this.options,c=this.loadingDiv,
d=b.loading;if(!c)this.loadingDiv=c=S(ia,{className:"highcharts-loading"},u(d.style,{left:this.plotLeft+"px",top:this.plotTop+"px",width:this.plotWidth+"px",height:this.plotHeight+"px",zIndex:10,display:U}),this.container),this.loadingSpan=S("span",null,d.labelStyle,c);this.loadingSpan.innerHTML=a||b.lang.loading;if(!this.loadingShown)F(c,{opacity:0,display:""}),eb(c,{opacity:d.style.opacity},{duration:d.showDuration||0}),this.loadingShown=!0},hideLoading:function(){var a=this.options,b=this.loadingDiv;
b&&eb(b,{opacity:0},{duration:a.loading.hideDuration||100,complete:function(){F(b,{display:U})}});this.loadingShown=!1},get:function(a){var b=this.axes,c=this.series,d,e;for(d=0;dO(e.dataMin,e.min)&&c19?this.containerHeight:400)},cloneRenderTo:function(a){var b=this.renderToClone,c=this.container;a?b&&(this.renderTo.appendChild(c),Qa(b),delete this.renderToClone):(c&&this.renderTo.removeChild(c),this.renderToClone=b=this.renderTo.cloneNode(0),F(b,{position:"absolute",top:"-9999px",display:"block"}),
B.body.appendChild(b),c&&b.appendChild(c))},getContainer:function(){var a,b=this.options.chart,c,d,e;this.renderTo=a=b.renderTo;e="highcharts-"+sb++;if(ra(a))this.renderTo=a=B.getElementById(a);a||mb(13,!0);a.innerHTML="";a.offsetWidth||this.cloneRenderTo();this.getChartSize();c=this.chartWidth;d=this.chartHeight;this.container=a=S(ia,{className:"highcharts-container"+(b.className?" "+b.className:""),id:e},u({position:"relative",overflow:"hidden",width:c+"px",height:d+"px",textAlign:"left",lineHeight:"normal"},
b.style),this.renderToClone||a);this.renderer=b.forExport?new qa(a,c,d,!0):new Ta(a,c,d);ga&&this.renderer.create(this,a,c,d)},getMargins:function(){var a=this.options.chart,b=a.spacingTop,c=a.spacingRight,d=a.spacingBottom,a=a.spacingLeft,e,f=this.legend,g=this.optionsMarginTop,h=this.optionsMarginLeft,i=this.optionsMarginRight,j=this.optionsMarginBottom,k=this.chartTitleOptions,l=this.chartSubtitleOptions,m=this.options.legend,p=o(m.margin,10),q=m.x,t=m.y,r=m.align,y=m.verticalAlign;this.resetMargins();
e=this.axisOffset;if((this.title||this.subtitle)&&!s(this.optionsMarginTop))if(l=x(this.title&&!k.floating&&!k.verticalAlign&&k.y||0,this.subtitle&&!l.floating&&!l.verticalAlign&&l.y||0))this.plotTop=x(this.plotTop,l+o(k.margin,15)+b);if(f.display&&!m.floating)if(r==="right"){if(!s(i))this.marginRight=x(this.marginRight,f.legendWidth-q+p+c)}else if(r==="left"){if(!s(h))this.plotLeft=x(this.plotLeft,f.legendWidth+q+p+a)}else if(y==="top"){if(!s(g))this.plotTop=x(this.plotTop,f.legendHeight+t+p+b)}else if(y===
"bottom"&&!s(j))this.marginBottom=x(this.marginBottom,f.legendHeight-t+p+d);this.extraBottomMargin&&(this.marginBottom+=this.extraBottomMargin);this.extraTopMargin&&(this.plotTop+=this.extraTopMargin);this.hasCartesianSeries&&n(this.axes,function(a){a.getOffset()});s(h)||(this.plotLeft+=e[3]);s(g)||(this.plotTop+=e[0]);s(j)||(this.marginBottom+=e[2]);s(i)||(this.marginRight+=e[1]);this.setChartSize()},initReflow:function(){function a(a){var g=c.width||Va(d,"width"),h=c.height||Va(d,"height"),a=a?
a.target:N;if(g&&h&&(a===N||a===B)){if(g!==b.containerWidth||h!==b.containerHeight)clearTimeout(e),e=setTimeout(function(){b.resize(g,h,!1)},100);b.containerWidth=g;b.containerHeight=h}}var b=this,c=b.options.chart,d=b.renderTo,e;I(N,"resize",a);I(b,"destroy",function(){P(N,"resize",a)})},fireEndResize:function(){var a=this;a&&E(a,"endResize",null,function(){a.isResizing-=1})},resize:function(a,b,c){var d,e,f=this.title,g=this.subtitle;this.isResizing+=1;ua(c,this);this.oldChartHeight=this.chartHeight;
this.oldChartWidth=this.chartWidth;if(s(a))this.chartWidth=d=t(a);if(s(b))this.chartHeight=e=t(b);F(this.container,{width:d+"px",height:e+"px"});this.renderer.setSize(d,e,c);this.plotWidth=d-this.plotLeft-this.marginRight;this.plotHeight=e-this.plotTop-this.marginBottom;this.maxTicks=null;n(this.axes,function(a){a.isDirty=!0;a.setScale()});n(this.series,function(a){a.isDirty=!0});this.isDirtyBox=this.isDirtyLegend=!0;this.getMargins();a=this.spacingBox;f&&f.align(null,null,a);g&&g.align(null,null,
a);this.redraw(c);this.oldChartHeight=null;E(this,"resize");Ra===!1?this.fireEndResize():setTimeout(this.fireEndResize,Ra&&Ra.duration||500)},setChartSize:function(){var a=this.inverted,b=this.chartWidth,c=this.chartHeight,d=this.options.chart,e=d.spacingTop,f=d.spacingRight,g=d.spacingBottom,d=d.spacingLeft;this.plotLeft=t(this.plotLeft);this.plotTop=t(this.plotTop);this.plotWidth=t(b-this.plotLeft-this.marginRight);this.plotHeight=t(c-this.plotTop-this.marginBottom);this.plotSizeX=a?this.plotHeight:
this.plotWidth;this.plotSizeY=a?this.plotWidth:this.plotHeight;this.spacingBox={x:d,y:e,width:b-d-f,height:c-e-g};n(this.axes,function(a){a.setAxisSize();a.setAxisTranslation()})},resetMargins:function(){var a=this.options.chart,b=a.spacingRight,c=a.spacingBottom,d=a.spacingLeft;this.plotTop=o(this.optionsMarginTop,a.spacingTop);this.marginRight=o(this.optionsMarginRight,b);this.marginBottom=o(this.optionsMarginBottom,c);this.plotLeft=o(this.optionsMarginLeft,d);this.axisOffset=[0,0,0,0]},drawChartBox:function(){var a=
this.options.chart,b=this.renderer,c=this.chartWidth,d=this.chartHeight,e=this.chartBackground,f=this.plotBackground,g=this.plotBorder,h=this.plotBGImage,i=a.borderWidth||0,j=a.backgroundColor,k=a.plotBackgroundColor,l=a.plotBackgroundImage,m,p={x:this.plotLeft,y:this.plotTop,width:this.plotWidth,height:this.plotHeight};m=i+(a.shadow?8:0);if(i||j)if(e)e.animate(e.crisp(null,null,null,c-m,d-m));else{e={fill:j||U};if(i)e.stroke=a.borderColor,e["stroke-width"]=i;this.chartBackground=b.rect(m/2,m/2,c-
m,d-m,a.borderRadius,i).attr(e).add().shadow(a.shadow)}if(k)f?f.animate(p):this.plotBackground=b.rect(this.plotLeft,this.plotTop,this.plotWidth,this.plotHeight,0).attr({fill:k}).add().shadow(a.plotShadow);if(l)h?h.animate(p):this.plotBGImage=b.image(l,this.plotLeft,this.plotTop,this.plotWidth,this.plotHeight).add();if(a.plotBorderWidth)g?g.animate(g.crisp(null,this.plotLeft,this.plotTop,this.plotWidth,this.plotHeight)):this.plotBorder=b.rect(this.plotLeft,this.plotTop,this.plotWidth,this.plotHeight,
0,a.plotBorderWidth).attr({stroke:a.plotBorderColor,"stroke-width":a.plotBorderWidth,zIndex:4}).add();this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart,c,d=a.options.series,e,f;n(["inverted","angular","polar"],function(g){c=Y[b.type||b.defaultSeriesType];f=a[g]||b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&&e--;)(c=Y[d[e].type])&&c.prototype[g]&&(f=!0);a[g]=f})},render:function(){var a=this,b=a.axes,c=a.renderer,d=a.options,e=d.labels,d=d.credits,f;a.setTitle();a.legend=new qb(a);
n(b,function(a){a.setScale()});a.getMargins();a.maxTicks=null;n(b,function(a){a.setTickPositions(!0);a.setMaxTicks()});a.adjustTickAmounts();a.getMargins();a.drawChartBox();a.hasCartesianSeries&&n(b,function(a){a.render()});if(!a.seriesGroup)a.seriesGroup=c.g("series-group").attr({zIndex:3}).add();n(a.series,function(a){a.translate();a.setTooltipPoints();a.render()});e.items&&n(e.items,function(){var b=u(e.style,this.style),d=w(b.left)+a.plotLeft,f=w(b.top)+a.plotTop+12;delete b.left;delete b.top;
c.text(this.html,d,f).attr({zIndex:2}).css(b).add()});if(d.enabled&&!a.credits)f=d.href,a.credits=c.text(d.text,0,0).on("click",function(){if(f)location.href=f}).attr({align:d.position.align,zIndex:8}).css(d.style).add().align(d.position);a.hasRendered=!0},destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;if(a!==null){E(a,"destroy");P(a);for(e=b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();n("title,subtitle,chartBackground,plotBackground,plotBGImage,plotBorder,seriesGroup,clipRect,credits,tracker,scroller,rangeSelector,legend,resetZoomButton,tooltip,renderer".split(","),
function(b){var c=a[b];c&&(a[b]=c.destroy())});if(d)d.innerHTML="",P(d),f&&Qa(d),d=null;for(e in a)delete a[e];a=a.options=null}},firstRender:function(){var a=this,b=a.options,c=a.callback;if(!Da&&N==N.top&&B.readyState!=="complete"||ga&&!N.canvg)ga?Nb.push(function(){a.firstRender()},b.global.canvasToolsURL):B.attachEvent("onreadystatechange",function(){B.detachEvent("onreadystatechange",a.firstRender);B.readyState==="complete"&&a.firstRender()});else{a.getContainer();E(a,"init");if(Highcharts.RangeSelector&&
b.rangeSelector.enabled)a.rangeSelector=new Highcharts.RangeSelector(a);a.resetMargins();a.setChartSize();a.propFromSeries();a.getAxes();n(b.series||[],function(b){a.initSeries(b)});if(Highcharts.Scroller&&(b.navigator.enabled||b.scrollbar.enabled))a.scroller=new Highcharts.Scroller(a);a.tracker=new Db(a,b);a.render();a.renderer.draw();c&&c.apply(a,[a]);n(a.callbacks,function(b){b.apply(a,[a])});a.cloneRenderTo(!0);E(a,"load")}},init:function(a){var b=this.options.chart,c;b.reflow!==!1&&I(this,"load",
this.initReflow);if(a)for(c in a)I(this,c,a[c]);this.xAxis=[];this.yAxis=[];this.animation=ga?!1:o(b.animation,!0);this.setSize=this.resize;this.pointCount=0;this.counters=new Ab;this.firstRender()}};rb.prototype.callbacks=[];var Wa=function(){};Wa.prototype={init:function(a,b,c){var d=a.chart.counters;this.series=a;this.applyOptions(b,c);this.pointAttr={};if(a.options.colorByPoint){b=a.chart.options.colors;if(!this.options)this.options={};this.color=this.options.color=this.color||b[d.color++];d.wrapColor(b.length)}a.chart.pointCount++;
return this},applyOptions:function(a,b){var c=this.series,d=typeof a;this.config=a;if(d==="number"||a===null)this.y=a;else if(typeof a[0]==="number")this.x=a[0],this.y=a[1];else if(d==="object"&&typeof a.length!=="number"){if(u(this,a),this.options=a,a.dataLabels)c._hasPointLabels=!0}else if(typeof a[0]==="string")this.name=a[0],this.y=a[1];if(this.x===A)this.x=b===A?c.autoIncrement():b},destroy:function(){var a=this.series.chart,b=a.hoverPoints,c;a.pointCount--;if(b&&(this.setState(),za(b,this),
!b.length))a.hoverPoints=null;if(this===a.hoverPoint)this.onMouseOut();if(this.graphic||this.dataLabel)P(this),this.destroyElements();this.legendItem&&a.legend.destroyItem(this);for(c in this)this[c]=null},destroyElements:function(){for(var a="graphic,tracker,dataLabel,group,connector,shadowGroup".split(","),b,c=6;c--;)b=a[c],this[b]&&(this[b]=this[b].destroy())},getLabelConfig:function(){return{x:this.category,y:this.y,key:this.name||this.category,series:this.series,point:this,percentage:this.percentage,
total:this.total||this.stackTotal}},select:function(a,b){var c=this,d=c.series.chart,a=o(a,!c.selected);c.firePointEvent(a?"select":"unselect",{accumulate:b},function(){c.selected=a;c.setState(a&&"select");b||n(d.getSelectedPoints(),function(a){if(a.selected&&a!==c)a.selected=!1,a.setState(""),a.firePointEvent("unselect")})})},onMouseOver:function(){var a=this.series,b=a.chart,c=b.tooltip,d=b.hoverPoint;if(d&&d!==this)d.onMouseOut();this.firePointEvent("mouseOver");c&&(!c.shared||a.noSharedTooltip)&&
c.refresh(this);this.setState("hover");b.hoverPoint=this},onMouseOut:function(){this.firePointEvent("mouseOut");this.setState();this.series.chart.hoverPoint=null},tooltipFormatter:function(a){var b=this.series,c=b.tooltipOptions,d=a.match(/\{(series|point)\.[a-zA-Z]+\}/g),e=/[{\.}]/,f,g,h,i,j={y:0,open:0,high:0,low:0,close:0,percentage:1,total:1};c.valuePrefix=c.valuePrefix||c.yPrefix;c.valueDecimals=c.valueDecimals||c.yDecimals;c.valueSuffix=c.valueSuffix||c.ySuffix;for(i in d)g=d[i],ra(g)&&g!==
a&&(h=(" "+g).split(e),f={point:this,series:b}[h[1]],h=h[2],f===this&&j.hasOwnProperty(h)?(f=j[h]?h:"value",f=(c[f+"Prefix"]||"")+Xa(this[h],o(c[f+"Decimals"],-1))+(c[f+"Suffix"]||"")):f=f[h],a=a.replace(g,f));return a},update:function(a,b,c){var d=this,e=d.series,f=d.graphic,g,h=e.data,i=h.length,j=e.chart,b=o(b,!0);d.firePointEvent("update",{options:a},function(){d.applyOptions(a);aa(a)&&(e.getAttribs(),f&&f.attr(d.pointAttr[e.state]));for(g=0;ga+1&&b.push(d.slice(a+1,g)),a=g):g===e-1&&b.push(d.slice(a+1,g+1))});this.segments=b},setOptions:function(a){var b=this.chart.options,c=b.plotOptions,d=a.data;a.data=null;c=C(c[this.type],c.series,a);c.data=a.data=d;this.tooltipOptions=C(b.tooltip,c.tooltip);return c},getColor:function(){var a=
this.options,b=this.chart.options.colors,c=this.chart.counters;this.color=a.color||!a.colorByPoint&&b[c.color++]||"gray";c.wrapColor(b.length)},getSymbol:function(){var a=this.options.marker,b=this.chart,c=b.options.symbols,b=b.counters;this.symbol=a.symbol||c[b.symbol++];if(/^url/.test(this.symbol))a.radius=0;b.wrapSymbol(c.length)},drawLegendSymbol:function(a){var b=this.options,c=b.marker,d=a.options.symbolWidth,e=this.chart.renderer,f=this.legendGroup,a=a.baseline,g;if(b.lineWidth){g={"stroke-width":b.lineWidth};
if(b.dashStyle)g.dashstyle=b.dashStyle;this.legendLine=e.path(["M",0,a-4,"L",d,a-4]).attr(g).add(f)}if(c&&c.enabled)b=c.radius,this.legendSymbol=e.symbol(this.symbol,d/2-b,a-4-b,2*b,2*b).attr(this.pointAttr[""]).add(f)},addPoint:function(a,b,c,d){var e=this.data,f=this.graph,g=this.area,h=this.chart,i=this.xData,j=this.yData,k=f&&f.shift||0,l=this.options.data;ua(d,h);if(f&&c)f.shift=k+1;if(g){if(c)g.shift=k+1;g.isArea=!0}b=o(b,!0);d={series:this};this.pointClass.prototype.applyOptions.apply(d,[a]);
i.push(d.x);j.push(this.valueCount===4?[d.open,d.high,d.low,d.close]:d.y);l.push(a);c&&(e[0]&&e[0].remove?e[0].remove(!1):(e.shift(),i.shift(),j.shift(),l.shift()));this.getAttribs();this.isDirtyData=this.isDirty=!0;b&&h.redraw()},setData:function(a,b){var c=this.points,d=this.options,e=this.initialColor,f=this.chart,g=null,h=this.xAxis,i=this.pointClass.prototype;this.xIncrement=null;this.pointRange=h&&h.categories&&1||d.pointRange;if(s(e))f.counters.color=e;var j=[],k=[],l=a?a.length:[],m=this.valueCount;
if(l>(d.turboThreshold||1E3)){for(e=0;g===null&&ek||this.forceCrop))if(a=i.getExtremes(),i=a.min,k=a.max,b[d-1]k)b=[],c=[];else if(b[0]k){for(a=0;a=i){e=x(0,a-1);break}for(;ak){f=a+1;break}b=b.slice(e,f);c=c.slice(e,f);g=!0}for(a=b.length-1;a>0;a--)if(d=b[a]-b[a-1],d>0&&(h===A||d=0&&d<=e;)h[d++]=f}this.tooltipPoints=h}},tooltipHeaderFormatter:function(a){var b=this.tooltipOptions,c=b.xDateFormat,d=this.xAxis,e=d&&d.options.type==="datetime",f;if(e&&!c)for(f in D)if(D[f]>=d.closestPointRange){c=b.dateTimeLabelFormats[f];break}return b.headerFormat.replace("{point.key}",e?db(c,a):a).replace("{series.name}",
this.name).replace("{series.color}",this.color)},onMouseOver:function(){var a=this.chart,b=a.hoverSeries;if(ea||!a.mouseIsDown){if(b&&b!==this)b.onMouseOut();this.options.events.mouseOver&&E(this,"mouseOver");this.setState("hover");a.hoverSeries=this}},onMouseOut:function(){var a=this.options,b=this.chart,c=b.tooltip,d=b.hoverPoint;if(d)d.onMouseOut();this&&a.events.mouseOut&&E(this,"mouseOut");c&&!a.stickyTracking&&!c.shared&&c.hide();this.setState();b.hoverSeries=null},animate:function(a){var b=
this.chart,c=this.clipRect,d=this.options.animation;d&&!aa(d)&&(d={});if(a){if(!c.isAnimating)c.attr("width",0),c.isAnimating=!0}else c.animate({width:b.plotSizeX},d),this.animate=null},drawPoints:function(){var a,b=this.points,c=this.chart,d,e,f,g,h,i,j,k;if(this.options.marker.enabled)for(f=b.length;f--;)if(g=b[f],d=g.plotX,e=g.plotY,k=g.graphic,e!==A&&!isNaN(e))if(a=g.pointAttr[g.selected?"select":""],h=a.r,i=o(g.marker&&g.marker.symbol,this.symbol),j=i.indexOf("url")===0,k)k.animate(u({x:d-h,
y:e-h},k.symbolName?{width:2*h,height:2*h}:{}));else if(h>0||j)g.graphic=c.renderer.symbol(i,d-h,e-h,2*h,2*h).attr(a).add(this.group)},convertAttribs:function(a,b,c,d){var e=this.pointAttrToOptions,f,g,h={},a=a||{},b=b||{},c=c||{},d=d||{};for(f in e)g=e[f],h[f]=o(a[g],b[f],c[f],d[f]);return h},getAttribs:function(){var a=this,b=Z[a.type].marker?a.options.marker:a.options,c=b.states,d=c.hover,e,f=a.color,g={stroke:f,fill:f},h=a.points||[],i=[],j,k=a.pointAttrToOptions,l;a.options.marker?(d.radius=
d.radius||b.radius+2,d.lineWidth=d.lineWidth||b.lineWidth+1):d.color=d.color||pa(d.color||f).brighten(d.brightness).get();i[""]=a.convertAttribs(b,g);n(["hover","select"],function(b){i[b]=a.convertAttribs(c[b],i[""])});a.pointAttr=i;for(f=h.length;f--;){g=h[f];if((b=g.options&&g.options.marker||g.options)&&b.enabled===!1)b.radius=0;e=!1;if(g.options)for(l in k)s(b[k[l]])&&(e=!0);if(e){j=[];c=b.states||{};e=c.hover=c.hover||{};if(!a.options.marker)e.color=pa(e.color||g.options.color).brighten(e.brightness||
d.brightness).get();j[""]=a.convertAttribs(b,i[""]);j.hover=a.convertAttribs(c.hover,i.hover,j[""]);j.select=a.convertAttribs(c.select,i.select,j[""])}else j=i;g.pointAttr=j}},destroy:function(){var a=this,b=a.chart,c=a.clipRect,d=/AppleWebKit\/533/.test(ya),e,f,g=a.data||[],h,i,j;E(a,"destroy");P(a);n(["xAxis","yAxis"],function(b){if(j=a[b])za(j.series,a),j.isDirty=!0});a.legendItem&&a.chart.legend.destroyItem(a);for(f=g.length;f--;)(h=g[f])&&h.destroy&&h.destroy();a.points=null;if(c&&c!==b.clipRect)a.clipRect=
c.destroy();n("area,graph,dataLabelsGroup,group,tracker,trackerGroup".split(","),function(b){a[b]&&(e=d&&b==="group"?"hide":"destroy",a[b][e]())});if(b.hoverSeries===a)b.hoverSeries=null;za(b.series,a);for(i in a)delete a[i]},drawDataLabels:function(){var a=this,b=a.options,c=b.dataLabels;if(c.enabled||a._hasPointLabels){var d,e,f=a.points,g,h,i,j=a.dataLabelsGroup,k=a.chart,l=a.xAxis,l=l?l.left:k.plotLeft,m=a.yAxis,m=m?m.top:k.plotTop,p=k.renderer,q=k.inverted,u=a.type,r=b.stacking,y=u==="column"||
u==="bar",x=c.verticalAlign===null,w=c.y===null,v=p.fontMetrics(c.style.fontSize),H=v.h,J=v.b,K,z;y&&(v={top:J,middle:J-H/2,bottom:-H+J},r?(x&&(c=C(c,{verticalAlign:"middle"})),w&&(c=C(c,{y:v[c.verticalAlign]}))):x?c=C(c,{verticalAlign:"top"}):w&&(c=C(c,{y:v[c.verticalAlign]})));j?j.translate(l,m):j=a.dataLabelsGroup=p.g("data-labels").attr({visibility:a.visible?"visible":"hidden",zIndex:6}).translate(l,m).add();h=c;n(f,function(f){K=f.dataLabel;c=h;(g=f.options)&&g.dataLabels&&(c=C(c,g.dataLabels));
if(z=c.enabled){var l=f.barX&&f.barX+f.barW/2||o(f.plotX,-999),m=o(f.plotY,-999),n=c.y===null?f.y>=b.threshold?-H+J:J:c.y;d=(q?k.plotWidth-m:l)+c.x;e=t((q?k.plotHeight-l:m)+n)}if(K&&a.isCartesian&&(!k.isInsidePlot(d,e)||!z))f.dataLabel=K.destroy();else if(z){var l=c.align,v;i=c.formatter.call(f.getLabelConfig(),c);u==="column"&&(d+={left:-1,right:1}[l]*f.barW/2||0);!r&&q&&f.y<0&&(l="right",d-=10);c.style.color=o(c.color,c.style.color,a.color,"black");if(K)K.attr({text:i}).animate({x:d,y:e});else if(s(i)){l=
{align:l,fill:c.backgroundColor,stroke:c.borderColor,"stroke-width":c.borderWidth,r:c.borderRadius||0,rotation:c.rotation,padding:c.padding,zIndex:1};for(v in l)l[v]===A&&delete l[v];K=f.dataLabel=p[c.rotation?"text":"label"](i,d,e,null,null,null,c.useHTML,!0).attr(l).css(c.style).add(j).shadow(c.shadow)}if(y&&b.stacking&&K)v=f.barX,l=f.barY,m=f.barW,f=f.barH,K.align(c,null,{x:q?k.plotWidth-l-f:v,y:q?k.plotHeight-v-m:l,width:q?f:m,height:q?m:f})}})}},getSegmentPath:function(a){var b=this,c=[];n(a,
function(d,e){b.getPointSpline?c.push.apply(c,b.getPointSpline(a,d,e)):(c.push(e?"L":"M"),e&&b.options.step&&c.push(d.plotX,a[e-1].plotY),c.push(d.plotX,d.plotY))});return c},drawGraph:function(){var a=this,b=a.options,c=a.graph,d=[],e=a.group,f=b.lineColor||a.color,g=b.lineWidth,h=b.dashStyle,i,j=a.chart.renderer,k=[];n(a.segments,function(b){i=a.getSegmentPath(b);b.length>1?d=d.concat(i):k.push(b[0])});a.graphPath=d;a.singlePoints=k;if(c)Fa(c),c.animate({d:d});else if(g){c={stroke:f,"stroke-width":g};
if(h)c.dashstyle=h;a.graph=j.path(d).attr(c).add(e).shadow(b.shadow)}},invertGroups:function(){function a(){var a={width:b.yAxis.len,height:b.xAxis.len};c.attr(a).invert();d&&d.attr(a).invert()}var b=this,c=b.group,d=b.trackerGroup,e=b.chart;I(e,"resize",a);I(b,"destroy",function(){P(e,"resize",a)});a();b.invertGroups=a},createGroup:function(){var a=this.chart;(this.group=a.renderer.g("series")).attr({visibility:this.visible?"visible":"hidden",zIndex:this.options.zIndex}).translate(this.xAxis.left,
this.yAxis.top).add(a.seriesGroup);this.createGroup=Sb},render:function(){var a=this,b=a.chart,c,d=a.options,e=d.clip!==!1,f=d.animation,f=(d=f&&a.animate)?f&&f.duration||500:0,g=a.clipRect,h=b.renderer;if(!g&&(g=a.clipRect=!b.hasRendered&&b.clipRect?b.clipRect:h.clipRect(0,0,b.plotSizeX,b.plotSizeY+1),!b.clipRect))b.clipRect=g;a.createGroup();c=a.group;a.drawDataLabels();d&&a.animate(!0);a.getAttribs();a.drawGraph&&a.drawGraph();a.drawPoints();a.options.enableMouseTracking!==!1&&a.drawTracker();
b.inverted&&a.invertGroups();e&&!a.hasRendered&&(c.clip(g),a.trackerGroup&&a.trackerGroup.clip(b.clipRect));d&&a.animate();setTimeout(function(){g.isAnimating=!1;if((c=a.group)&&g!==b.clipRect&&g.renderer){if(e)c.clip(a.clipRect=b.clipRect);g.destroy()}},f);a.isDirty=a.isDirtyData=!1;a.hasRendered=!0},redraw:function(){var a=this.chart,b=this.isDirtyData,c=this.group;c&&(a.inverted&&c.attr({width:a.plotWidth,height:a.plotHeight}),c.animate({translateX:this.xAxis.left,translateY:this.yAxis.top}));
this.translate();this.setTooltipPoints(!0);this.render();b&&E(this,"updatedData")},setState:function(a){var b=this.options,c=this.graph,d=b.states,b=b.lineWidth,a=a||"";if(this.state!==a)this.state=a,d[a]&&d[a].enabled===!1||(a&&(b=d[a].lineWidth||b+1),c&&!c.dashstyle&&c.attr({"stroke-width":b},a?0:500))},setVisible:function(a,b){var c=this.chart,d=this.legendItem,e=this.group,f=this.tracker,g=this.dataLabelsGroup,h,i=this.points,j=c.options.chart.ignoreHiddenSeries;h=this.visible;h=(this.visible=
a=a===A?!h:a)?"show":"hide";if(e)e[h]();if(f)f[h]();else if(i)for(e=i.length;e--;)if(f=i[e],f.tracker)f.tracker[h]();if(g)g[h]();d&&c.legend.colorizeItem(this,a);this.isDirty=!0;this.options.stacking&&n(c.series,function(a){if(a.options.stacking&&a.visible)a.isDirty=!0});if(j)c.isDirtyBox=!0;b!==!1&&c.redraw();E(this,h)},show:function(){this.setVisible(!0)},hide:function(){this.setVisible(!1)},select:function(a){this.selected=a=a===A?!this.selected:a;if(this.checkbox)this.checkbox.checked=a;E(this,
a?"select":"unselect")},drawTrackerGroup:function(){var a=this.trackerGroup,b=this.chart;if(this.isCartesian){if(!a)this.trackerGroup=a=b.renderer.g().attr({zIndex:this.options.zIndex||1}).add(b.trackerGroup);a.translate(this.xAxis.left,this.yAxis.top)}return a},drawTracker:function(){var a=this,b=a.options,c=b.trackByArea,d=[].concat(c?a.areaPath:a.graphPath),e=d.length,f=a.chart,g=f.renderer,h=f.options.tooltip.snap,i=a.tracker,j=b.cursor,j=j&&{cursor:j},k=a.singlePoints,l=a.drawTrackerGroup(),
m;if(e&&!c)for(m=e+1;m--;)d[m]==="M"&&d.splice(m+1,0,d[m+1]-h,d[m+2],"L"),(m&&d[m]==="M"||m===e)&&d.splice(m,0,"L",d[m-2]+h,d[m-1]);for(m=0;m=0;d--)da&&i>e?(i=x(a,e),k=2*e-i):ig&&k>e?(k=x(g,e),i=2*e-k):kv?g-v:z-(f<=z?v:0));u(c,{barX:h,barY:i,barW:y,barH:j,pointWidth:r});c.shapeType="rect";c.shapeArgs=f=b.renderer.Element.prototype.crisp.call(0,e,h,i,y,j);e%2&&(f.y-=1,f.height+=1);c.trackerArgs=M(j)<3&&C(c.shapeArgs,{height:6,y:i-3})})},getSymbol:function(){},drawLegendSymbol:G.prototype.drawLegendSymbol,drawGraph:function(){},drawPoints:function(){var a=this,b=a.options,c=a.chart.renderer,d,e;n(a.points,function(f){var g=f.plotY;if(g!==A&&!isNaN(g)&&
f.y!==null)d=f.graphic,e=f.shapeArgs,d?(Fa(d),d.animate(C(e))):f.graphic=d=c[f.shapeType](e).attr(f.pointAttr[f.selected?"select":""]).add(a.group).shadow(b.shadow,null,b.stacking&&!b.borderRadius)})},drawTracker:function(){var a=this,b=a.chart,c=b.renderer,d,e,f=+new Date,g=a.options,h=g.cursor,i=h&&{cursor:h},j=a.drawTrackerGroup(),k,l,m;n(a.points,function(h){e=h.tracker;d=h.trackerArgs||h.shapeArgs;l=h.plotY;m=!a.isCartesian||l!==A&&!isNaN(l);delete d.strokeWidth;if(h.y!==null&&m)e?e.attr(d):
h.tracker=c[h.shapeType](d).attr({isTracker:f,fill:tb,visibility:a.visible?"visible":"hidden"}).on(ea?"touchstart":"mouseover",function(c){k=c.relatedTarget||c.fromElement;if(b.hoverSeries!==a&&z(k,"isTracker")!==f)a.onMouseOver();h.onMouseOver()}).on("mouseout",function(b){if(!g.stickyTracking&&(k=b.relatedTarget||b.toElement,z(k,"isTracker")!==f))a.onMouseOut()}).css(i).add(h.group||j)})},animate:function(a){var b=this,c=b.points,d=b.options;if(!a)n(c,function(a){var c=a.graphic,a=a.shapeArgs,g=
b.yAxis,h=d.threshold;c&&(c.attr({height:0,y:s(h)?g.getThreshold(h):g.translate(g.getExtremes().min,0,1,0,1)}),c.animate({height:a.height,y:a.y},d.animation))}),b.animate=null},remove:function(){var a=this,b=a.chart;b.hasRendered&&n(b.series,function(b){if(b.type===a.type)b.isDirty=!0});R.prototype.remove.apply(a,arguments)}});Y.column=ha;Z.bar=C(Z.column,{dataLabels:{align:"left",x:5,y:null,verticalAlign:"middle"}});Ia=ca(ha,{type:"bar",inverted:!0});Y.bar=Ia;Z.scatter=C(T,{lineWidth:0,states:{hover:{lineWidth:0}},
tooltip:{headerFormat:'{series.name} ',pointFormat:"x: {point.x} y: {point.y} "}});Ia=ca(R,{type:"scatter",sorted:!1,translate:function(){var a=this;R.prototype.translate.apply(a);n(a.points,function(b){b.shapeType="circle";b.shapeArgs={x:b.plotX,y:b.plotY,r:a.chart.options.tooltip.snap}})},drawTracker:function(){for(var a=this,b=a.options.cursor,b=b&&{cursor:b},c=a.points,d=c.length,e;d--;)if(e=c[d].graphic)e.element._i=
d;a._hasTracking?a._hasTracking=!0:a.group.attr({isTracker:!0}).on(ea?"touchstart":"mouseover",function(b){a.onMouseOver();if(b.target._i!==A)c[b.target._i].onMouseOver()}).on("mouseout",function(){if(!a.options.stickyTracking)a.onMouseOut()}).css(b)}});Y.scatter=Ia;Z.pie=C(T,{borderColor:"#FFFFFF",borderWidth:1,center:["50%","50%"],colorByPoint:!0,dataLabels:{distance:30,enabled:!0,formatter:function(){return this.point.name},y:5},legendType:"point",marker:null,size:"75%",showInLegend:!1,slicedOffset:10,
states:{hover:{brightness:0.1,shadow:!1}}});T={type:"pie",isCartesian:!1,pointClass:ca(Wa,{init:function(){Wa.prototype.init.apply(this,arguments);var a=this,b;u(a,{visible:a.visible!==!1,name:o(a.name,"Slice")});b=function(){a.slice()};I(a,"select",b);I(a,"unselect",b);return a},setVisible:function(a){var b=this.series.chart,c=this.tracker,d=this.dataLabel,e=this.connector,f=this.shadowGroup,g;g=(this.visible=a=a===A?!this.visible:a)?"show":"hide";this.group[g]();if(c)c[g]();if(d)d[g]();if(e)e[g]();
if(f)f[g]();this.legendItem&&b.legend.colorizeItem(this,a)},slice:function(a,b,c){var d=this.series.chart,e=this.slicedTranslation;ua(c,d);o(b,!0);a=this.sliced=s(a)?a:!this.sliced;a={translateX:a?e[0]:d.plotLeft,translateY:a?e[1]:d.plotTop};this.group.animate(a);this.shadowGroup&&this.shadowGroup.animate(a)}}),pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color"},getColor:function(){this.initialColor=this.chart.counters.color},animate:function(){var a=this;n(a.points,
function(b){var c=b.graphic,b=b.shapeArgs,d=-xa/2;c&&(c.attr({r:0,start:d,end:d}),c.animate({r:b.r,start:b.start,end:b.end},a.options.animation))});a.animate=null},setData:function(a,b){R.prototype.setData.call(this,a,!1);this.processData();this.generatePoints();o(b,!0)&&this.chart.redraw()},getCenter:function(){var a=this.options,b=this.chart,c=b.plotWidth,d=b.plotHeight,a=a.center.concat([a.size,a.innerSize||0]),e=O(c,d),f;return Ea(a,function(a,b){return(f=/%$/.test(a))?[c,d,e,e][b]*w(a)/100:a})},
translate:function(){this.generatePoints();var a=0,b=-0.25,c=this.options,d=c.slicedOffset,e=d+c.borderWidth,f,g=this.chart,h,i,j,k=this.points,l=2*xa,m,p,o,s=c.dataLabels.distance;this.center=f=this.getCenter();this.getX=function(a,b){j=L.asin((a-f[1])/(f[2]/2+s));return f[0]+(b?-1:1)*X(j)*(f[2]/2+s)};n(k,function(b){a+=b.y});n(k,function(c){m=a?c.y/a:0;h=t(b*l*1E3)/1E3;b+=m;i=t(b*l*1E3)/1E3;c.shapeType="arc";c.shapeArgs={x:f[0],y:f[1],r:f[2]/2,innerR:f[3]/2,start:h,end:i};j=(i+h)/2;c.slicedTranslation=
Ea([X(j)*d+g.plotLeft,da(j)*d+g.plotTop],t);p=X(j)*f[2]/2;o=da(j)*f[2]/2;c.tooltipPos=[f[0]+p*0.7,f[1]+o*0.7];c.labelPos=[f[0]+p+X(j)*s,f[1]+o+da(j)*s,f[0]+p+X(j)*e,f[1]+o+da(j)*e,f[0]+p,f[1]+o,s<0?"center":j0,q=[[],[]],s,r,t,u,x=2,v;if(d.enabled){R.prototype.drawDataLabels.apply(this);n(a,function(a){a.dataLabel&&q[a.labelPos[7]0){for(v=m-l-j;v<=m+l+j;v+=a)w.push(v);t=w.length;if(A>t){h=[].concat(z);h.sort(u);for(v=A;v--;)h[v].rank=v;for(v=A;v--;)z[v].rank>=t&&z.splice(v,1);A=z.length}for(v=0;v
0){if(r=C.pop(),B=r.i,r=r.y,s>r&&w[B+1]!==null||s