Skip to content

Instantly share code, notes, and snippets.

@erichocean
Created October 21, 2008 21:10
Show Gist options
  • Save erichocean/18428 to your computer and use it in GitHub Desktop.
Save erichocean/18428 to your computer and use it in GitHub Desktop.
// ========================================================================
// SproutCore
// copyright 2006-2008 Sprout Systems, Inc.
// ========================================================================
(function() {
var SC = SC.Deprecated;
require('foundation/object') ;
require('foundation/responder') ;
require('foundation/node_descriptor') ;
require('foundation/binding');
require('foundation/path_module');
require('mixins/delegate_support') ;
require('mixins/array') ;
require('mixins/tree') ;
SC.BENCHMARK_OUTLETS = NO ;
SC.BENCHMARK_CONFIGURE_OUTLETS = NO ;
SC.AUTORESIZE_BORDERS = 'borders';
SC.AUTORESIZE_WIDTH = 'width';
SC.AUTORESIZE_RIGHT = 'right';
SC.AUTORESIZE_LEFT = 'left';
SC.AUTORESIZE_HEIGHT = 'height';
SC.AUTORESIZE_TOP = 'top';
SC.AUTORESIZE_BOTTOM = 'bottom';
SC.AUTORESIZE_WIDTH_HEIGHT = 'width+height';
SC.AUTORESIZE_WIDTH_TOP = 'width+top';
SC.AUTORESIZE_WIDTH_BOTTOM = 'width+bottom';
SC.AUTORESIZE_RIGHT_HEIGHT = 'right+height';
SC.AUTORESIZE_RIGHT_TOP = 'right+top';
SC.AUTORESIZE_RIGHT_BOTTOM = 'right+bottom';
SC.AUTORESIZE_LEFT_TOP = 'left+top';
SC.AUTORESIZE_LEFT_BOTTOM = 'left+bottom';
SC.AUTORESIZE_LEFT_HEIGHT = 'left+height';
/**
@class
A view is the root class you use to manage the web page DOM in your
application. You can use views to render visible content on your page,
provide animations, and to capture and respond to events.
You can use SC.View directly to manage DOM elements or you can extend one
of the many subclasses provided by SproutCore. This documentation describes
the general concepts you need to understand when working with views, though
most often you will want to work with one of the subclasses instead.
h2. Working with DOM Elements
h2. Handling Events
@extends SC.Responder
@extends SC.PathModule
@extends SC.DelegateSupport
@since SproutCore 1.0
*/
SC.View = SC.Responder.extend( SC.PathModule, SC.DelegateSupport, SC.Tree,
/** @scope SC.View.prototype */ {
isAwake: false,
awake: function() {
if ( !this.isAwake ) {
var outlets = this.outlets;
// ...and then appends outlets to the view tree...
for ( var i = 0, e = outlets.length; i < e; ++i ) {
var key = outlets[i];
var view = this[key];
if (view) this.insertBefore(view, null);
}
// ...and then calls awake() recursively...
for ( var i = 0, e = outlets.length; i < e; ++i ) {
var key = outlets[i];
var view = this[key];
if (view) view.awake();
}
// ... and then configures any bindings.
// for ( NSString *key in _bindings ) {
// SCBinding *binding = [self bindTo: [key substringWithRange: NSMakeRange( 0, [key length] - 7)] from: [_bindings objectForKey: key]];
// [bindings addObject: binding];
// }
// sc_super();
// _outlets = nil; // don't need to keep this around anymore
// _bindings = nil; // don't need to keep this around anymore
this.isAwake = true;
}
},
// ..........................................
// VIEW API
//
// The methods in this section are used to manage actual views. You can
// basically interact with child elements in two ways. One using an API
// similar to the DOM API. Alternatively, you can treat the view like an
// array and use standard iterators.
//
/**
Insert the view into the the receiver's childNodes array.
The view will be added to the childNodes array before the beforeView. If
beforeView is null, then the view will be added to the end of the array.
This will also add the view's rootElement DOM node to the receivers
containerElement DOM node as a child.
If the specified view already belongs to another parent, it will be
removed from that view first.
@param view {SC.View} the view to insert as a child node.
@param beforeView {SC.View} view to insert before, or null to insert at
end
@returns {SC.View} the receiver
*/
insertBefore: function(view, beforeView) {
this._insertBefore(view,beforeView,true);
},
/** @private */
_insertBefore: function(view, beforeView, updateDom) {
// verify that beforeView is a child.
if (beforeView) {
if (beforeView.parentNode != this) throw "insertBefore() beforeView must belong to the receiver" ;
if (beforeView == view) throw "insertBefore() views cannot be the same";
}
if (view.parentNode) view.removeFromParent() ;
this.willAddChild(this, beforeView) ;
view.willAddToParent(this, beforeView) ;
// patch in the view.
if (beforeView) {
view.set('previousSibling', beforeView.previousSibling) ;
view.set('nextSibling', beforeView) ;
beforeView.set('previousSibling', view) ;
} else {
view.set('previousSibling', this.lastChild) ;
view.set('nextSibling', null) ;
this.set('lastChild', view) ;
}
if (view.previousSibling) view.previousSibling.set('nextSibling',view);
if (view.previousSibling == null) this.set('firstChild',view) ;
view.set('parentNode', this) ;
// Update DOM. -- ANIMATE
// Note that this code is not called when outlets are first configured.
// The assumption is that the created view already belongs to the
// document somwhere.
if (updateDom) {
var beforeElement = (beforeView) ? beforeView.rootElement : null;
(this.containerElement || this.rootElement).insertBefore(view.rootElement,beforeElement);
// regenerate the childNodes array.
this._rebuildChildNodes();
}
// update cached states.
view._updateIsVisibleInWindow() ;
view._flushInternalCaches() ;
view._invalidateClippingFrame() ;
// call notices.
view.didAddToParent(this, beforeView) ;
this.didAddChild(view, beforeView) ;
try{
return this ;
}finally{
if(beforeElement)
beforeElement=null;
}
},
/**
Remove the view from the receiver's childNodes array.
This will also remove the view's DOM element from the recievers DOM.
@param view {SC.View} the view to remove
@returns {SC.View} the receiver
*/
removeChild: function(view) {
if (!view) return ;
if (view.parentNode != this) throw "removeChild: view must belong to parent";
view.willRemoveFromParent() ;
this.willRemoveChild(view) ;
// unpatch.
if (view.previousSibling) {
view.previousSibling.set('nextSibling', view.nextSibling);
} else this.set('firstChild', view.nextSibling) ;
if (view.nextSibling) {
view.nextSibling.set('previousSibling', view.previousSibling) ;
} else this.set('lastChild', view.previousSibling) ;
// Update DOM -- ANIMATE
var el = (this.containerElement || this.rootElement);
if (el && (view.rootElement.parentNode == el) && (el != document)) {
el.removeChild(view.rootElement);
}
// regenerate the childNodes array.
this._rebuildChildNodes();
view.set('nextSibling', null);
view.set('previousSibling', null);
view.set('parentNode', null) ;
// update parent state.
view._updateIsVisibleInWindow() ;
view._flushInternalCaches();
view._invalidateClippingFrame() ;
view.didRemoveFromParent(this) ;
this.didRemoveChild(view);
try{
return this;
}finally{
el=null;
}
},
/**
Replace the oldView with the specified view in the receivers childNodes
array. This will also replace the DOM node of the oldView with the DOM
node of the new view in the receivers DOM.
If the specified view already belongs to another parent, it will be
removed from that view first.
@param view {SC.View} the view to insert in the DOM
@param view {SC.View} the view to remove from the DOM.
@returns {SC.View} the receiver
*/
replaceChild: function(view, oldView) {
this.insertBefore(view,oldView) ; this.removeChild(oldView) ;
return this;
},
/**
Removes the receiver from its parentNode. If the receiver does not belong
to a parentNode, this method does nothing.
@returns {null}
*/
removeFromParent: function() {
if (this.parentNode) this.parentNode.removeChild(this) ;
return null ;
},
/**
Works just like removeFromParent but also removes the view from internal
caches and sets the rootElement to null so that the view and its DOM can
be garbage collected.
SproutCore includes special gaurds that ensure views and their related
DOM elements will be garbage collected whenever your web page unloads.
However, if you create and destroy views frequently while your application
is running, you should call this method when views are no longer needed
to ensure they will be garbage collected even while your application is
still running.
@returns {null}
*/
destroy: function() {
this.removeFromParent() ;
delete SC.View._view[SC.guidFor(this)];
return null ;
},
/**
Appends the specified view to the end of the receivers childNodes array.
This is equivalent to calling insertBefore(view, null);
@param view {SC.View} the view to insert
@returns {SC.View} the receiver
*/
appendChild: function(view) {
this.insertBefore(view,null) ;
return this ;
},
/**
The array of views that are direct children of the receiver view. The DOM
elements managed by the views are also directl children of the
containerElement for the receiver.
@field
@type Array
*/
childNodes: [],
/**
The first child view in the childNodes array. If the view does not have
any children, this property will be null.
@field
@type SC.View
*/
firstChild: null,
/**
The last child view in the childNodes array. If the view does not have any children,
this property will be null.
@field
@type SC.View
*/
lastChild: null,
/**
The next sibling view in the childNodes array of the receivers parentNode.
If the receiver is the last view in the array or if the receiver does not
belong to a parent view this property will be null.
@field
@type SC.View
*/
nextSibling: null,
/**
The previous sibling view in the childNodes array of the receivers
parentNode. If the receiver is the first view in the array or if the
receiver does not belong to a parent view this property will be null.
@field
@type SC.View
*/
previousSibling: null,
/**
The parent view this view belongs to. If the receiver does not belong to a parent view
then this property is null.
@field
@type SC.View
*/
parentNode: null,
/**
The pane this view belongs to. The pane is the root of the responder
chain that this view belongs to. Typically a view's pane will be the
SC.window object. However, if you have added the view to a dialog, panel,
popup or other pane, this property will point to that pane instead.
If the view does not belong to a parentNode or if the view is not
onscreen, this property will be null.
@field
@type SC.View
*/
pane: function()
{
var view = this;
while(view = view.get('parentNode'))
{
if (view.get('isPane') ) break;
}
return view;
}.property(),
/**
Removes all child views from the receiver.
@returns {void}
*/
clear: function() {
while(this.firstChild) this.removeChild(this.firstChild) ;
},
/**
This method is called on the view just before it is added to a new parent
view.
You can override this method to do any setup you need on your view or to
reset any cached values that are impacted by being added to a view. The
default implementation does nothing.
@param parent {SC.View} the new parent
@paran beforeView {SC.View} the view in the parent's childNodes array that
will follow this view once it is added. If the view is being added to
the end of the array, this will be null.
@returns {void}
*/
willAddToParent: function(parent, beforeView) {},
/**
This method is called on the view just after it is added to a new parent
view.
You can override this method to do any setup you need on your view or to
reset any cached values that are impacted by being added to a view. The
default implementation does nothing.
@param parent {SC.View} the new parent
@paran beforeView {SC.View} the view in the parent's childNodes array that
will follow this view once it is added. If the view is being added to
the end of the array, this will be null.
@returns {void}
*/
didAddToParent: function(parent, beforeView) {},
/**
This method is called on the view just before it is removed from a parent
view.
You can override this method to clear out any values that depend on the
view belonging to the current parentNode. The default implementation does
nothing.
@returns {void}
*/
willRemoveFromParent: function() {},
/**
This method is called on the view just after it is removed from a parent
view.
You can override this method to clear out any values that depend on the
view belonging to the current parentNode. The default implementation does
nothing.
@param oldParent {SC.View} the old parent view
@returns {void}
*/
didRemoveFromParent: function(oldParent) {},
/**
This method is called just before a new child view is added to the
receiver's childNodes array. You can use this to prepare for any layout
or other cleanup you might need to do.
The default implementation does nothing.
@param child {SC.View} the view to be added
@param beforeView {SC.View} and existing child view that will follow the
child view in the array once it is added. If adding to the end of the
array, this param will be null.
@returns {void}
*/
willAddChild: function(child, beforeView) {},
/**
This method is called just after a new child view is added to the
receiver's childNodes array. You can use this to prepare for any layout
or other cleanup you might need to do.
The default implementation does nothing.
@param child {SC.View} the view that was added
@param beforeView {SC.View} and existing child view that will follow the
child view in the array once it is added. If adding to the end of the
array, this param will be null.
@returns {void}
*/
didAddChild: function(child, beforeView) {},
/**
This method is called just before a child view is removed from the
receiver's childNodes array. You can use this to prepare for any layout
or other cleanup you might need to do.
The default implementation does nothing.
@param child {SC.View} the view to be removed
@returns {void}
*/
willRemoveChild: function(child) {},
/**
This method is called just after a child view is removed from the
receiver's childNodes array. You can use this to prepare for any layout
or other cleanup you might need to do.
The default implementation does nothing.
@param child {SC.View} the view that was removed
@returns {void}
*/
didRemoveChild: function(child) {},
nextKeyView: null,
previousKeyView: null,
nextValidKeyView: function()
{
var view = this;
while (view = view.get('nextKeyView'))
{
if (view.get('isVisible') && view.get('acceptsFirstResponder')) {
return view;
}
}
return null;
},
previousValidKeyView: function()
{
var view = this;
while (view = view.get('previousKeyView'))
{
if (view.get('isVisible') && view.get('acceptsFirstResponder')) {
return view;
}
}
return null;
},
/** @private
Invoked whenever the child hierarchy changes and any internally cached
values might need to be recalculated.
*/
_flushInternalCaches: function() {
// only flush cache for parent if this item was cached since the top level
// cached can only be populated if this one is populated also...
if ((this._needsClippingFrame != null) || (this._needsFrameChanges != null)) {
this._needsClippingFrame = this._needsFrameChanges = null ;
if (this.parentNode) this.parentNode._flushInternalCaches() ;
}
},
// ..........................................
// SC.Responder implementation
//
nextResponder: function()
{
return this.parentNode;
}.property('parentNode'),
// recursively travels down the view hierarchy looking for a view that returns true to performKeyEquivalent
performKeyEquivalent: function(keystring, evt)
{
var child = this.get('firstChild');
while (child)
{
if (child.performKeyEquivalent(keystring, evt)) return true;
child = child.get('nextSibling');
}
return false;
},
// ..........................................
// ELEMENT API
//
/**
An array of currently applied classNames.
@field
@type {Array}
@param value {Array} Array of class names to apply to the element
*/
classNames: function(key, value) {
if (value !== undefined) {
value = Array.from(value) ;
if (this.rootElement) this.rootElement.className = value.join(' ') ;
this._classNames = value.slice() ;
}
if (!this._classNames) {
var classNames = this.rootElement.className;
this._classNames = (classNames && classNames.length > 0) ? classNames.split(' ') : [] ;
}
return this._classNames ;
}.property(),
/**
Detects the presence of the class name on the root element.
@param className {String} the class name
@returns {Boolean} YES if class name is currently applied, NO otherwise
*/
hasClassName: function(className) {
return (this._classNames || this.get('classNames')).indexOf(className) >= 0 ;
},
/**
Adds the class name to the element.
@param className {String} the class name to add.
@returns {String} the class name
*/
addClassName: function(className) {
if (this.hasClassName(className)) return ; // nothing to do
var classNames = this._classNames || this.get('classNames') ;
classNames.push(className) ;
this.set('classNames', classNames) ;
return className ;
},
/**
Removes the specified class name from the element.
@param className {String} the class name to remove
@returns {String} the class name
*/
removeClassName: function(className) {
if (!this.hasClassName(className)) return ; // nothing to do
var classNames = this._classNames || this.get('classNames') ;
classNames = this._classNames = classNames.without(className) ;
this.set('classNames', classNames) ;
return className ;
},
/**
Adds or removes the class name according to flag.
This is a simple way to add or remove a class from the root element.
@param className {String} the class name
@param flag {Boolean} YES to add class name, NO to remove it.
@returns {String} The class Name.
*/
setClassName: function(className, flag) {
return (!!flag) ? this.addClassName(className) : this.removeClassName(className);
},
/**
Toggles the presence of the class name.
If the specified CSS class is applied, it will be removed. If it is not
present, it will be added. Note that if this changes the potential
layout of the view, you must wrap calls to this in viewFrameDidChange()
and viewFrameWillChange().
@param className {String} the class name
@returns {Boolean} YES if classname is now applied
*/
toggleClassName: function(className) {
return this.setClassName(className, !this.hasClassName(className)) ;
},
/**
Retrieves the current value of the named CSS style.
This method is designed to work cross platform and uses the current
computed style, which is the combination of all applied CSS class names
and inline styles.
@param style {String} the style key.
@returns {Object} the style value or null if not-applied/auto
*/
getStyle: function(style) {
var element = this.rootElement ;
if (!this._computedStyle) {
this._computedStyle = document.defaultView.getComputedStyle(element, null) ;
}
//if (style == 'float') style = 'cssFloat' ;
style = (style === 'float') ? 'cssFloat' : style.camelize() ;
var value = element.style[style];
if (!value) {
value = this._computedStyle ? this._computedStyle[style] : null ;
}
if (style === 'opacity') {
value = value ? parseFloat(value) : 1.0;
}
if (value === 'auto') value = null ;
return value ;
},
/**
Sets the passed hash of CSS styles and values on the element. You should
pass your properties pre-camelized.
@param styles {Hash} hash of keys and values
@param camelized {Boolean} optional bool set to NO if you did not camelize.
@returns {Boolean} YES if set succeeded.
*/
setStyle: function(styles, camelized) {
return Element.setStyle(this.rootElement, styles, camelized) ;
},
/**
Updates the HTML of an element.
This method takes care of nasties like processing scripts and inserting
HTML into a table. It is also somewhat slow. If you control the HTML
being inserted and you are not working with table elements, you should use
the innerHTML property instead. If you are setting content generated by
users, this method can insert the content safely.
@param html {String} the html to insert.
*/
update: function(html) {
Element.update((this.containerElement || this.rootElement),html) ;
this.propertyDidChange('innerHTML') ;
},
/**
Retrieves the value for an attribute on the DOM element
@param attrName {String} the attribute name
@returns {String} attribute value
*/
getAttribute: function(attrName) {
return Element.readAttribute(this.rootElement,attrName) ;
},
/**
Sets an attribute on the root DOM element.
@param attrName {String} the attribute name
@param value {String} the new attribute value
@returns {String} the set attribute name
*/
setAttribute: function(attrName, value) {
this.rootElement.setAttribute(attrName, value) ;
},
/**
Returns true if the named attributes is defined on the views root element.
@param attrName {String} the attribute name
@returns {Boolean} YES if attribute is present.
*/
hasAttribute: function(attrName) {
return Element.hasAttribute(this.rootElement, attrName) ;
},
// ..........................................
// STYLE API
//
// These properties can be used to directly manipulate various CSS
// styles on the view. These properties are required for animation
// support. Values are typically assumed to be in px.
/**
SC.View's unknown property is used to implement a large class of
properties beginning with the the world "style". You can get or set
any of these properties to edit individual CSS style properties.
*/
unknownProperty: function(key, value) {
if (key && key.match && key.match(/^style/)) {
key = key.slice(5,key.length).replace(/^./, function(x) {
return x.toLowerCase();
});
var ret = null ;
// handle dimensional properties
if (key.match(/height$|width$|top$|bottom$|left$|right$/i)) {
if (value !== undefined) {
this.viewFrameWillChange() ;
var props = {} ;
props[key] = (value) ? value + 'px' : 'auto' ;
this.setStyle(props) ;
this.viewFrameDidChange() ;
}
ret = this.getStyle(key) ;
ret = (ret === 'auto') ? null : Math.round(parseFloat(ret)) ;
// all other properties just pass through (and do not change frame)
} else {
if (value !== undefined) {
var props = {} ;
props[key] = value ;
this.setStyle(props) ;
}
ret = this.getStyle(key) ;
}
return ret;
} else return sc_super();
},
// ..........................................
// DOM API
//
// The methods in this section give you some low-level control over how the
// view interacts with the DOM. You do not normally need to work with this.
/**
This is the DOM element actually managed by this view. This will be set
by the view when it is created. You should rarely need to access this
property directly. When you do access it, you should only do so from
within methods you write on your SC.View subclasses, never from outside
the view.
Unlike most properties, you do not need to use get()/set() to access this
property. It is not currently safe to edit this property once the view
has been createde.
@field
@type {Element}
*/
rootElement: null,
/**
Normally when you add child views to your view, their DOM elements will
be set as direct children of the root element. However you can
choose instead to designate an alertnative child node using this
property. Set this to a selector string to begin with. The first time
it is accessed, the view will convert it to an actual element. It is not
currently safe to edit this property once the view has been created.
Like rootElement, you should only access this property from within
methods you write on an SC.View subclass, never from outside the view.
Unlike most properties, it is not necessary to use get()/set().
@field
@type {Element}
*/
containerElement: null,
// ..........................................
// VIEW LAYOUT
//
// The following methods can be used to implement automatic resizing.
// The frame and bounds provides a simple way for you to compute the
// location and size of your views. You can then use the automatic
// resizing.
/**
Returns true if the view or any of its contained views implement the
clippingFrameDidChange method.
If this property returns false, then notifications about changes to the
clippingFrame will probably not be called on the receiver. Normally if
you do not need to worry about this property since implementing the
clippingFrameDidChange() method will change its value and cause your
method to be invoked.
This property is automatically updated whenever you add or remove a child
view.
*/
needsClippingFrame: function() {
if (this._needsClippingFrame == null) {
var ret = this.clippingFrameDidChange != SC.View.prototype.clippingFrameDidChange;
var view = this.get('firstChild') ;
while(!ret && view) {
ret = view.get('needsClippingFrame') ;
view = view.get('nextSibling') ;
}
this._needsClippingFrame = ret ;
}
return this._needsClippingFrame ;
}.property(),
/**
Returns true if the view or any of its contained views implements any
resize methods.
If this property returns false, changes to your frame view may not be
relayed to child methods. This may mean that your various frame
properties could become stale unless you call refreshFrames() first.
If you want you make sure your frames are up to date, see hasManualLayout.
This property is automatically updated whenever you add or remove a child
view. It returns true if you implement any of the resize methods or if
hasManualLayout is true.
*/
needsFrameChanges: function() {
if (this._needsFrameChanges == null) {
var ret = this.get('needsClippingFrame') || this.get('hasManualLayout') ;
var view = this.get('firstChild') ;
while(!ret && view) {
ret = view.get('needsFrameChanges') ;
view = view.get('nextSibling') ;
}
this._needsFrameChanges = ret ;
}
return this._needsFrameChanges ;
}.property(),
/**
Returns true if the receiver manages the layout for itself or its
children.
Normally this property returns true automatically if you implement
resizeChildrenWithOldSize() or resizeWithOldParentSize() or
clippingFrameDidChange().
If you do not implement these methods but need to make sure your frame is
always up-to-date anyway, set this property to true.
*/
hasManualLayout: function() {
return (this.resizeChildrenWithOldSize != SC.View.prototype.resizeChildrenWithOldSize) ||
(this.resizeWithOldParentSize != SC.View.prototype.resizeWithOldParentSize) ||
(this.clippingFrameDidChange != SC.View.prototype.clippingFrameDidChange) ;
}.property(),
/**
Convert a point _from_ the offset parent of the passed view to the current
view.
This is a useful utility for converting points in the coordinate system of
another view to the coordinate system of the receiver. Pass null for
targetView to convert a point from a window offset. This is the inverse
of convertFrameToView().
Note that if your view is not visible on the screen, this may not work.
@param {Point} f The point or frame to convert
@param {SC.View} targetView The view to convert from. Pass null to convert from window coordinates.
@returns {Point} The converted point or frame
*/
convertFrameFromView: function(f, targetView) {
// first, convert to root level offset.
var thisOffset = SC.viewportOffset(this.get('offsetParent')) ;
var thatOffset = (targetView) ? SC.viewportOffset(targetView.get('offsetParent')) : SC.ZERO_POINT;
// now get adjustment.
var adjustX = thatOffset.x - thisOffset.x ;
var adjustY = thatOffset.y - thisOffset.y ;
return { x: (f.x + adjustX), y: (f.y + adjustY), width: f.width, height: f.height };
},
/**
Convert a point _to_ the offset parent of the passed view from the current
view.
This is a useful utility for converting points in the coordinate system of
the receiver to the coordinate system of another view. Pass null for
targetView to convert a point to a window offset. This is the inverse of
convertFrameFromView().
Note that if your view is not visible on the screen, this may not work.
@param {Point} f The point or frame to convert
@param {SC.View} targetView The view to convert to. Pass null to convert to window coordinates.
@returns {Point} The converted point or frame
*/
convertFrameToView: function(f, sourceView) {
// first, convert to root level offset.
var thisOffset = SC.viewportOffset(this.get('offsetParent')) ;
var thatOffset = (sourceView) ? SC.viewportOffset(sourceView.get('offsetParent')) : SC.ZERO_POINT ;
// now get adjustment.
var adjustX = thisOffset.x - thatOffset.x ;
var adjustY = thisOffset.y - thatOffset.y ;
return { x: (f.x + adjustX), y: (f.y + adjustY), width: f.width, height: f.height };
},
/**
This property returns a DOM ELEMENT that is the offset parent for
this view's frame coordinates. Depending on your CSS, this parent
may or may not match with the parent view.
@example
offsetView = $view(this.get('offsetParent')) ;
@field
@type {Element}
*/
offsetParent: function() {
// handle simple cases.
var el = this.rootElement ;
if (!el || el === document.body) return el;
if (el.offsetParent) return el.offsetParent ;
// in some cases, we can't find the offset parent so we walk up the
// chain until an element is found with a position other than
// 'static'
//
// Note that IE places DOM elements not in the main body inside of a
// document-fragment root. We need to treat document-fragments (i.e.
// nodeType === 11) as null values
var ret = null ;
while(!ret && (el = el.parentNode) && (el.nodeType !== 11) && (el !== document.body)) {
if (Element.getStyle(el, 'position') !== 'static') ret = el;
}
if (!ret && (el === document.body)) ret = el ;
return ret ;
}.property(),
/**
The inner bounds for the content shown inside of this frame. Reflects
scroll position and other properties.
The inner frame returns the actual available frame for child elements,
less any borders or scroll bars.
This value can change when:
- the receiver's frame changes
- the receiver's child views change, adding or removing scrollbars
- You can the CSS or applied style that effects the borders or scrollbar visibility
*/
innerFrame: function(key, value) {
var f ;
if (this._innerFrame == null) {
// get the base frame
// The _collectInnerFrame function is set at the bottom of this file
// based on the browser type.
var el = this.rootElement ;
f = this._collectFrame(SC.View._collectInnerFrame) ;
// bizarely for FireFox if your offsetParent has a border, then it can
// impact the offset
if (SC.Platform.Firefox) {
var parent = el.offsetParent ;
var overflow = (parent) ? Element.getStyle(parent, 'overflow') : 'visible' ;
if (overflow && overflow !== 'visible') {
var left = parseInt(Element.getStyle(parent, 'borderLeftWidth'),0) || 0 ;
var top = parseInt(Element.getStyle(parent, 'borderTopWidth'),0) || 0 ;
f.x += left; f.y += top ;
}
}
// fix the x & y with the clientTop/clientLeft
var clientLeft, clientTop ;
if (SC.Platform.IE) {
if (!el.width) {
clientLeft = parseInt(this.getStyle('border-left-width'),0) || 0 ;
} else clientLeft = el.clientLeft ;
if (!el.height) {
clientTop = parseInt(this.getStyle('border-top-width'),0) || 0 ;
} else clientTop = el.clientTop ;
f.x += clientLeft; f.y += clientTop;
}
else {
if (el.clientLeft == null) {
clientLeft = parseInt(this.getStyle('border-left-width'),0) || 0 ;
} else clientLeft = el.clientLeft ;
if (el.clientTop == null) {
clientTop = parseInt(this.getStyle('border-top-width'),0) || 0 ;
} else clientTop = el.clientTop ;
f.x += clientLeft; f.y += clientTop;
}
// cache this frame if using manual layout mode
this._innerFrame = SC.cloneRect(f);
} else f = SC.cloneRect(this._innerFrame) ;
// console.log('returning x:%@ y:%@ w:%@ h:%@'.fmt(f.x, f.y, f.width, f.height));
return f ;
}.property('frame'),
layout: null, // the default layout
// controls how the frame method positions the view (16 different possibilities)
autoresize: SC.AUTORESIZE_BORDERS,
preferredSize: { width: 150, height: 100 }, // the default preferred size
// preferredLayout: fuction() {
// var parent = this.parentNode;
// while (parent) this.parentNode;
//
// }.property();
page: function(key, value) {
if (value !== undefined) {
this._page = value;
}
else {
var page = this._page;
if (!page && this.parentNode ) page = this.parentNode.get('page');
return page;
}
}.property(),
parentLayout: function() {
if ( !this._parentLayout || this.didChangeFor('parentNode', 'page')) {
var parentLayout = this.parentNode ? this.parentNode.get('layout') : null;
if (!parentLayout) {
var page = this.get('page');
if (page) parentLayout = page.get('frame');
}
this._parentLayout = parentLayout;
}
return this._parentLayout;
}.property('parentNode', 'page'),
layoutSubviews: function() {
var ary = this.get('childNodes');
for (var i = 0, e = ary.length; i < e; ++i ) {
var child = ary[i];
child.set('frame', child.get('layout'));
child.layoutSubviews();
}
},
isView: true,
/**
The outside bounds of your view, offset top/left from its offsetParent
The frame rect is the area actually occupied by a view including any
borders or padding, but excluding margins.
The frame is calculated and cached the first time you get it. Afer that,
the frame cache should automatically update when you make changes that
will effect the view frames unless you change the frame indirectly, such
as through changing CSS classes or by-passing the view to edit the DOM.
If you make a change like this, be sure to wrap the code that makes this
change with calls to viewFrameWillChange() and viewFrameDidChange() on the
highest-level view that will be impacted by the change. Calling this
method will automatically update child frames as well.
When you set the frame property, it will update the left, top, height,
and width CSS attributes on the element. Since the height and width in
the frame rect includes borders and padding, the view will automatically
adjust the height and width CSS it sets to account for this.
If you would prefer to edit the CSS attributes for the frame directly
instead, you can do so by using the styleTop, styleLeft, styleRight,
styleBottom, styleWidth, and styleHeight properties on the view. These
properties will update the CSS attributes and call viewFrameDidChange()/
viewFrameWillChange().
@field
*/
frame: function(key, value) {
if (value !== undefined) {
this._frame = value;
if ( value && this.didChangeFor('autoresize', 'layout') ) {
var style = { position: 'absolute', padding: '0px', border: '0px', display: 'block' };
var autoresize = this.get('autoresize');
var f = this.get('layout') || value; // value used for computed frames
var pf = this.get('parentLayout');
if (!pf) {
console.log("SC.View#set('frame', %@) called in parent-less view.".fmt($I(value)));
return; // not visible, nothing to do...
}
// okay, actually change the frame...
this.viewFrameWillChange();
console.log('updating positioning for ' + this);
console.log('autoresize: ' + autoresize);
console.log('frame is %@'.fmt($I(f)));
console.log('parentFrame is %@'.fmt($I(pf)));
switch (autoresize) {
case SC.AUTORESIZE_WIDTH: // fixed left and right, variable top and bottom
style.left = f.x + 'px';
style.right = (pf.width - (f.x + f.width)) + 'px';
style.height = f.height + 'px';
style.top = '50%';
style.marginTop = (0 - f.height/2) + 'px';
break;
case SC.AUTORESIZE_RIGHT: // fixed width and left, variable top and bottom
style.width = f.width + 'px';
style.left = f.x + 'px';
style.top = '50%';
style.marginTop = (0 - f.height/2) + 'px';
break;
case SC.AUTORESIZE_LEFT: // fixed width and right, variable top and bottom
style.width = f.width + 'px';
style.right = (pf.width - (f.x + f.width)) + 'px';
style.top = '50%';
style.marginTop = (0 - f.height/2) + 'px';
break;
case SC.AUTORESIZE_HEIGHT: // variable left and right, fixed top and bottom
style.width = f.width + 'px';
style.left = '50%';
style.marginLeft = (0 - f.width/2) + 'px';
style.top = f.y + 'px';
style.bottom = (pf.height - (f.y + f.height)) + 'px';;
break;
case SC.AUTORESIZE_TOP: // variable left and right, fixed height and bottom
style.width = f.width + 'px';
style.left = '50%';
style.marginLeft = (0 - f.width/2) + 'px';
style.height = f.height + 'px';
style.bottom = (pf.height - (f.y + f.height)) + 'px';;
break;
case SC.AUTORESIZE_BOTTOM: // variable left and right, fixed height and top
style.width = f.width + 'px';
style.left = '50%';
style.marginLeft = (0 - f.width/2) + 'px';
style.height = f.height + 'px';
style.top = f.y + 'px';
break;
case SC.AUTORESIZE_WIDTH_HEIGHT: // fixed left, right, top and bottom
style.left = f.x + 'px';
style.right = (pf.width - (f.x + f.width)) + 'px';
style.top = f.y + 'px';
style.bottom = (pf.height - (f.y + f.height)) + 'px';;
break;
case SC.AUTORESIZE_WIDTH_TOP: // fixed left and right, fixed height and bottom
style.left = f.x + 'px';
style.right = (pf.width - (f.x + f.width)) + 'px';
style.height = f.height + 'px';
style.bottom = (pf.height - (f.y + f.height)) + 'px';;
break;
case SC.AUTORESIZE_WIDTH_BOTTOM: // fixed left and right, fixed height and top
style.left = f.x + 'px';
style.right = (pf.width - (f.x + f.width)) + 'px';
style.height = f.height + 'px';
style.top = f.y + 'px';
break;
case SC.AUTORESIZE_RIGHT_HEIGHT: // fixed width and left, fixed top and bottom
style.width = f.width + 'px';
style.left = f.x + 'px';
style.top = f.y + 'px';
style.bottom = (pf.height - (f.y + f.height)) + 'px';;
break;
case SC.AUTORESIZE_RIGHT_TOP: // fixed width and left, fixed height and bottom
style.width = f.width + 'px';
style.left = f.x + 'px';
style.height = f.height + 'px';
style.bottom = (pf.height - (f.y + f.height)) + 'px';;
break;
case SC.AUTORESIZE_RIGHT_BOTTOM: // fixed width and left, fixed height and top
style.width = f.width + 'px';
style.left = f.x + 'px';
style.height = f.height + 'px';
style.top = f.y + 'px';
break;
case SC.AUTORESIZE_LEFT_TOP: // fixed width and right, fixed height and bottom
style.width = f.width + 'px';
style.right = (pf.width - (f.x + f.width)) + 'px';
style.height = f.height + 'px';
style.bottom = (pf.height - (f.y + f.height)) + 'px';;
break;
case SC.AUTORESIZE_LEFT_BOTTOM: // fixed width and right, fixed height and top
style.width = f.width + 'px';
style.right = (pf.width - (f.x + f.width)) + 'px';
style.height = f.height + 'px';
style.top = f.y + 'px';
break;
case SC.AUTORESIZE_LEFT_HEIGHT: // fixed width and right, fixed top and bottom
style.width = f.width + 'px';
style.right = (pf.width - (f.x + f.width)) + 'px';
style.top = f.y + 'px';
style.bottom = (pf.height - (f.y + f.height)) + 'px';;
break;
case SC.AUTORESIZE_BORDERS: // fixed width and height, variable top, bottom, left, and right
style.width = f.width + 'px';
style.left = '50%';
style.marginLeft = (0 - f.width/2) + 'px';
style.height = f.height + 'px';
style.top = '50%';
style.marginTop = (0 - f.height/2) + 'px';
break;
default:
console.log('missing or invalid autoresize value; using SC.AUTORESIZE_BORDERS');
style.width = f.width + 'px';
style.left = '50%';
style.marginLeft = (0 - f.width/2) + 'px';
style.height = f.height + 'px';
style.top = '50%';
style.marginTop = (0 - f.height/2) + 'px';
}
console.log('style is %@'.fmt($I(style)));
// now apply style change and clear the cached frame
this.setStyle(style);
// notify for a resize only.
this.viewFrameDidChange();
}
}
else {
var f = this._frame;
return f ? SC.cloneRect(f) : { x: 0, y: 0, width: 100, height: 150 };
}
}.property('autoresize', 'layout'),
/**
The current frame size.
This property will actually return the same value as the frame property,
however setting this property will set only the frame size and ignore any
origin you might pass.
@field
*/
size: function(key, value) {
if (value !== undefined) {
this.set('frame',{ width: value.width, height: value.height }) ;
}
return this.get('frame') ;
}.property('frame'),
/**
The current frame origin.
This property will actually return the same value as the frame property,
however setting this property will set only the frame origin and ignore
any size you might pass.
@field
*/
origin: function(key, value) {
if (value !== undefined) {
this.set('frame',{ x: value.x, y: value.y }) ;
}
return this.get('frame') ;
}.property('frame'),
/**
Call this method before you make a change that will impact the frame of
the view such as changing the border thickness or adding/removing a CSS
style.
Once you finish making your changes, be sure to call viewFrameDidChange()
as well. This will deliver any relevant resizing and other notifications.
It is safe to nest multiple calls to this method.
This method is called automatically anytime you set the frame.
@returns {void}
*/
viewFrameWillChange: function() {
if (this._frameChangeLevel++ <= 0) {
this._frameChangeLevel = 1 ;
// save frame information if view has manual layout.
if (this.get('needsFrameChanges')) {
this._cachedFrames = this.getEach('innerFrame', 'clippingFrame', 'frame') ;
} else this._cachedFrames = null ;
this.beginPropertyChanges(); // suspend change notifications
}
},
/**
Call this method just after you finish making changes that will impace the
frame of the view such as changing the border thickness or adding/removing
a CSS style.
It is safe to next multiple calls to this method. This method is called
automatically anytime you set the frame.
@returns {void}
*/
viewFrameDidChange: function(force) {
// clear the frame caches
this.recacheFrames() ;
// if this is a top-level call then also deliver notifications as needed.
if (--this._frameChangeLevel <= 0) {
this._frameChangeLevel = 0 ;
if (this._cachedFrames) {
var newFrames = this.getEach('innerFrame', 'clippingFrame') ;
// notify if clippingFrame has changed and clippingFrameDidChange is
// implemented.
var nf = newFrames[1]; var of = this._cachedFrames[1] ;
if (force || (nf.width != of.width) || (nf.height != of.height)) {
this._invalidateClippingFrame() ;
}
// notify children if the size of the innerFrame has changed.
var nf = newFrames[0]; var of = this._cachedFrames[0] ;
if (force || (nf.width != of.width) || (nf.height != of.height)) {
this.resizeChildrenWithOldSize(this._cachedFrames.last()) ;
}
// clear parent scrollFrame if needed
var parent = this.parentNode ;
while (parent && parent != SC.window) {
if (parent._scrollFrame) parent._scrollFrame = null ;
parent = parent.parentNode ;
}
this.notifyPropertyChange('frame') ; // trigger notifications.
}
// allow notifications again
this.endPropertyChanges() ;
}
},
/**
Clears any cached frames so the next get will recompute them.
This method does not notify any observers of changes to the frames. It
should only be used when you need to make sure your frame info is up to
date but you do not expect anything to have happened that frame observers
would be interested in.
*/
recacheFrames: function() {
this._innerFrame = this._frame = this._clippingFrame = this._scrollFrame = null ;
},
/**
Set to true if you expect this view to have scrollable content.
Normally views do not monitor their onscroll event. If you set this
property to true, however, the view will observe its onscroll event and
update its scrollFrame and clippedFrame.
This will also register the view as a scrollable area that can be
auto-scrolled during a drag/drop event.
*/
isScrollable: false,
/**
The frame used to control scrolling of content.
x,y => offset from the innerFrame root.
width,height => total size of the frame
If the frame does not have scrollable content, then the size will be equal
to the innerFrame size.
This frame changes when:
- the receiver's innerFrame changes
- the scroll location is changed programatically
- the size of child views changes
- the user scrolls the view
@field
*/
scrollFrame: function(key, value) {
// if value was passed, update the scroll x,y only.
if (value != undefined) {
var el = this.rootElement ;
if (value.x != null) el.scrollLeft = 0-value.x ;
if (value.y != null) el.scrollTop = 0-value.y ;
this._scrollFrame = null ;
this._invalidateClippingFrame() ;
}
// build frame. We can use a cached version but only
var f;
if (this._scrollFrame == null) {
var el = this.rootElement ;
var func;
if (SC.isIE()) {
func = function() {
var borderTopWidth = 0;
var borderBottomWidth = 0;
var borderLeftWidth = 0;
var borderRightWidth = 0;
var overflow = el.currentStyle.overflow;
if ( overflow != 'hidden' && overflow != 'auto' ) {
borderTopWidth = parseInt(el.currentStyle.borderTopWidth, 0) || 0 ;
borderBottomWidth = parseInt(el.currentStyle.borderBottomWidth, 0) || 0 ;
borderLeftWidth = parseInt(el.currentStyle.borderLeftWidth, 0) || 0 ;
borderRightWidth = parseInt(el.currentStyle.borderRightWidth, 0) || 0 ;
}
return {
x: 0 - el.scrollLeft,
y: 0 - el.scrollTop,
width: el.scrollWidth + borderLeftWidth + borderRightWidth,
height: Math.max(el.scrollHeight, el.clientHeight) + borderTopWidth + borderBottomWidth
};
};
}
else {
func = function() {
return {
x: 0 - el.scrollLeft,
y: 0 - el.scrollTop,
width: el.scrollWidth,
height: el.scrollHeight
};
};
}
f = this._collectFrame(func);
// cache this frame if using manual layout mode
this._scrollFrame = SC.cloneRect(f);
} else f = SC.cloneRect(this._scrollFrame) ;
// finally return the frame.
return f ;
}.property('frame'),
/**
The visible portion of the view.
Returns the subset of the receivers frame that is actually visible on
screen. This frame is automatically updated whenever one of the following
changes:
- A parent view is resized
- A parent view's scrollFrame changes.
- The receiver is moved or resized
- The receiver or a parent view is added to or removed from the window.
@field
*/
clippingFrame: function() {
var f ;
if (this._clippingFrame == null) {
//if (this instanceof SC.SplitView) debugger ;
// my clipping frame is usually my frame
f = this.get('frame') ;
// scope to my parents clipping frame.
if (this.parentNode) {
// use only the visible portion of the parent's innerFrame.
var parent = this.parentNode ;
var prect = SC.intersectRects(parent.get('clippingFrame'), parent.get('innerFrame'));
// convert the local view's coordinates
prect = this.convertFrameFromView(prect, parent) ;
// if parent is scrollable, then adjust by scroll frame also.
if (this.parentNode.get('isScrollable')) {
var scrollFrame = this.get('scrollFrame') ;
prect.x -= scrollFrame.x ;
prect.y -= scrollFrame.y ;
}
// blend with current frame
f = SC.intersectRects(f, prect) ;
} else {
f.width = f.height = 0 ;
}
this._clippingFrame = SC.cloneRect(f) ;
} else f = SC.cloneRect(this._clippingFrame) ;
return f ;
}.property('frame', 'scrollFrame'),
/**
Called whenever the receivers clippingFrame has changed. You can override
this method to perform partial rendering or other clippingFrame-dependent
actions.
The default implementation does nothing (and may not even be called do to
optimizations). Note that this is the preferred way to respond to changes
in the clippingFrame of using an observer since this method is gauranteed
to happen in the correct order. You can use observers and bindings as
well if you wish to handle anything that need not be handled
synchronously.
*/
clippingFrameDidChange: function() {
},
/**
Called whenever the view's innerFrame size changes. You can override this
method to perform your own layout of your child views.
If you do not override this method, the view will assume you are using
CSS to layout your child views. As an optimization the view may not
always call this method if it determines that you have not overridden it.
This default version simply calls resizeWithOldParentSize() on all of its
children.
@param oldSize {Size} The old frame size of the view.
@returns {void}
*/
resizeChildrenWithOldSize: function(oldSize) {
var child = this.get('firstChild') ;
while(child) {
child.resizeWithOldParentSize(oldSize) ;
child = child.get('nextSibling') ;
}
},
/**
Called whenever the parent's innerFrame size has changed. You can
override this method to change how your view responds to this change.
If you do not override this method, the view will assume you are using CSS
to control your layout and it will simply relay the change information to
your child views. As an optmization, the view may not always call this
method if it determines that you have not overridden it.
@param oldSize {Size} The old frame size of the parent view.
*/
resizeWithOldParentSize: function(oldSize) {
this.viewFrameWillChange() ;
this.viewFrameDidChange(YES) ;
},
/** @private
Handler for the onscroll event. Hooked in on init if isScrollable is
true. Notify children that their clipping frame has changed.
*/
_onscroll: function() {
this._scrollFrame = null ;
this.notifyPropertyChange('scrollFrame') ;
SC.Benchmark.start('%@.onscroll'.fmt(this)) ;
this._invalidateClippingFrame() ;
SC.Benchmark.end('%@.onscroll'.fmt(this)) ;
},
_frameChangeLevel: 0,
/** @private
Used internally to collect client offset and location info. If the
element is not in the main window or hidden, it will be added temporarily
and then the passed function will be called.
*/
_collectFrame: function(func) {
var el = this.rootElement ;
// if not visible in window, move parent node into window and get
// dim and offset. If the element has no parentNode, then just move
// the element in.
var isVisibleInWindow = this.get('isVisibleInWindow') ;
if (!isVisibleInWindow) {
var pn = el.parentNode || el ;
if (pn === SC.window.rootElement) pn = el ;
var pnParent = pn.parentNode ; // cache former parent node
var pnSib = pn.nextSibling ; // cache next sibling
SC.window.rootElement.insertBefore(pn, null) ;
}
// if view is not displayed, temporarily display it also
var display = this.getStyle('display') ;
var isHidden = !(display != 'none' && display != null) ;
// All *Width and *Height properties give 0 on elements with display none,
// so enable the element temporarily
if (isHidden) {
var els = this.rootElement.style;
var originalVisibility = els.visibility;
var originalPosition = els.position;
var originalDisplay = els.display;
els.visibility = 'hidden';
els.position = 'absolute';
els.display = 'block';
}
var ret = func.call(this) ;
if (isHidden) {
els.display = originalDisplay;
els.position = originalPosition;
els.visibility = originalVisibility;
}
if (!isVisibleInWindow) {
if (pnParent) {
pnParent.insertBefore(pn, pnSib) ;
} else {
if(pn.parentNode)
SC.window.rootElement.removeChild(pn) ;
}
}
return ret;
},
/** @private
Called whenever some aspect of the receiver's frames have changed that
probably has invalidated the child views clippingFrames. Events that cause
this include:
- change to the innerFrame size
- change to the scrollFrame
- change to the clippingFrame
For performance reasons, this only passes onto children if they or a decendent
implements the clippingFrameDidChange method.
*/
_invalidateChildrenClippingFrames: function() {
var view = this.get('firstChild') ;
while(view) {
view._invalidateClippingFrame() ;
view = view.get('nextSibling') ;
}
},
/** @private
Called by a parentNode whenever the clippingFrame needs to be recalculated.
*/
_invalidateClippingFrame: function() {
if (this.get('needsClippingFrame')) {
this._clippingFrame = null ;
this.clippingFrameDidChange() ;
this.notifyPropertyChange('clippingFrame') ;
this._invalidateChildrenClippingFrames() ;
}
},
// ..........................................
// PROPERTIES
//
/**
Used to show or hide the view.
If this property is set to NO, then the DOM element will be hidden using
display:none. You will often want to bind this property to some setting
in your application to make various parts of your app visible as needed.
If you have animation enabled, then changing this property will actually
trigger the animation to bring the view in or out.
The default binding format is SC.Binding.Bool
@field
@type {Boolean}
*/
isVisible: true,
/** @private */
isVisibleBindingDefault: SC.Binding.Bool,
/**
(Read Only) The current display visibility of the view.
Usually, this property will mirror the current state of the isVisible
property. However, if your view animates its visibility in and out, then
this will not become false until the animation completes.
@type {Boolean}
*/
displayIsVisible: true,
/**
true when the view is actually visible in the DOM window.
This property is set to true only when the view is (a) in the main DOM
hierarchy and (b) all parent nodes are visible and (c) the receiver node
is visible.
@type {Boolean}
@field
*/
isVisibleInWindow: NO,
/**
If true, the tooltip will be localized. Also used by some subclasses.
@type {Boolean}
@field
*/
localize: false,
/**
Applied to the title attribute of the rootElement DOM if set.
If localize is true, then the toolTip will be localized first.
@type {String}
@field
*/
toolTip: '',
/**
The HTML you want to use when creating a new element.
You can specify the HTML as a string of text, using the NodeDescriptor, or
by pointing directly to an element.
Note that as an optimization, SC.View will actually convert the value of
this property to an actual DOM structure the first time you create a view
and then clone the DOM structure for future views.
This means that in general you should only set the value of emptyElement
when you create a view subclass. Changing this property value at other
times will often have no effect.
@field
@type {String}
*/
emptyElement: "<div></div>",
/**
If true, view will display in a lightbox when you show it.
@field
@type {Boolean}
*/
isPanel: false,
/**
If true, the view should be modal when shown as a panel.
@field
@type {Boolean}
*/
isModal: true,
/**
Enable visible animation by default.
*/
isAnimationEnabled: true,
/**
General support for animation. Just call this method and it will build
and play an animation starting from the current state. The second param
is optional. It should either be a hash of animator options or an
animator object returned by a previous call to transitionTo().
*/
transitionTo: function(target,animator,opts) {
var animatorOptions = opts || {} ;
// Create or reset the animator.
if (animator && !animator._isAnimator) {
var finalStyle = animator ;
if (!this.get("isAnimationEnabled")) {
animatorOptions = Object.clone(animatorOptions) ;
animatorOptions.duration = 1;
}
if (animatorOptions.duration) {
animatorOptions.duration = parseInt(animatorOptions.duration,0) ;
}
animator = Animator.apply(this.rootElement, finalStyle, animatorOptions);
animator._isAnimator = true ;
}
// trigger animation
if (animator) {
animator.jumpTo(animator.state) ;
animator.seekTo(target) ;
}
return animator ;
},
/**
The contents of the view as HTML. You can use this property to both
retrieve the content and to change it. Use this property instead of
manually changing the content of your view as this property works around
certain cross-browser bugs.
@field
*/
innerHTML: function(key, value) {
if (value !== undefined) {
// Clear the text node.
this._textNode = null ;
// Safari2 has a bad habit of sometimes not actually changing its
// innerHTML. This will make sure the innerHTML get's changed properly.
if (SC.isSafari() && !SC.isSafari3()) {
var el = (this.containerElement || this.rootElement) ; var reps = 0 ;
var f = function() {
el.innerHTML = '' ; el.innerHTML = value ;
if ((reps++ < 5) && (value.length>0) && (el.innerHTML == '')) {
f.invokeLater() ;
}
};
f();
} else (this.containerElement || this.rootElement).innerHTML = value;
} else value = (this.containerElement || this.rootElement).innerHTML ;
return value ;
}.property(),
/**
The contents of the view as plain text. You can use this property to
both retrieve the content and to change it. Use this property instead of
the innerHTML property when you want to set plain text only as this
property is much faster.
@field
*/
innerText: function(key, value) {
if (value !== undefined) {
if (value == null) value = '' ;
// add a textNode if necessary
if (this._textNode == null) {
this._textNode = document.createTextNode(value) ;
var el = this.rootElement || this.containerElement ;
while(el.firstChild) el.removeChild(el.firstChild) ;
el.appendChild(this._textNode) ;
} else this._textNode.data = value ;
}
return (this._textNode) ? this._textNode.data : this.innerHTML().unescapeHTML() ;
}.property(),
// ..........................................
// SUPPORT METHODS
//
init: function() {
arguments.callee.base.call(this) ;
// configure them outlets.
if (SC.BENCHMARK_CONFIGURE_OUTLETS) SC.Benchmark.start('SC.View.configureOutlets') ;
this.configureOutlets() ;
if (SC.BENCHMARK_CONFIGURE_OUTLETS) SC.Benchmark.end('SC.View.configureOutlets') ;
var toolTip = this.get('toolTip') ;
if(toolTip && (toolTip != '')) this._updateToolTipObserver();
// if container element is a string, convert it to an actual DOM element.
if (this.containerElement && ($type(this.containerElement) === T_STRING)) {
this.containerElement = this.$sel(this.containerElement);
}
// register as a drop target and scrollable.
if (this.get('isDropTarget')) SC.Drag.addDropTarget(this) ;
if (this.get('isScrollable')) SC.Drag.addScrollableView(this) ;
// add scrollable handler
if (this.isScrollable) this.rootElement.onscroll = SC.View._onscroll ;
// setup isVisibleInWindow ;
this.isVisibleInWindow = (this.parentNode) ? this.parentNode.get('isVisibleInWindow') : NO;
// set frame
// var layout = this.get('layout');
// if ( !layout ) {
// var preferredSize = this.get('preferredSize') || { width: 150, height: 100 };
// layout = { x: 0, y: 0, width: preferredSize.width, height: preferredSize.height };
// }
// this.set('frame', layout );
},
// this method looks through your outlets array and will try to
// reconfigure any missing ones.
configureOutlets: function() {
if (!this.outlets || (this.outlets.length <= 0)) return ;
// lookup outlets as selector paths or execute the function if there
// is one.
this.beginPropertyChanges(); // bundle changes
for(var oloc=0;oloc < this.outlets.length;oloc++) {
var view = this.outlet(this.outlets[oloc]) ;
}
this.endPropertyChanges() ;
},
// ..........................................
// VISIBILITY METHODS
//
// Calling this method will show the view. Don't call this method
// directly but instead set the isVisible property to true. You can
// override this method to provide your own show capabilities.
show: function() {
Element.show(this.rootElement) ;
this.removeClassName('hidden') ;
this.set('displayIsVisible',true) ;
},
// This is the primitive method for hiding a view. It will be called when
// isVisible is set to false after an animation runs or immediate if no
// animation is defined.
hide: function() {
Element.hide(this.rootElement) ;
this.addClassName('hidden') ;
this.set('displayIsVisible', false) ;
},
// ..........................................
// DEPRECATED. DO NOT USE
//
// deprecated. Included only for compatibility.
animateVisible: function(key, value) {
if (value !== undefined) return this.set('isAnimationEnabled',value) ;
return this.get('isAnimationEnabled');
}.property('isAnimationEnabled'),
// ..........................................
// PRIVATE METHODS
//
// this will set the rootElement, cleaning up any old element.
_attachRootElement: function(el) {
if (this.rootElement) this.rootElement._configured = null ;
this.rootElement = el ;
el._configured = this._guid ;
},
// This method is called internally after you add or remove a child view.
// It will rebuild the childNodes array to reflect all children.
_rebuildChildNodes: function() {
var ret = [] ; var view = this.firstChild;
while(view) { ret.push(view); view = view.nextSibling; }
this.set('childNodes', ret) ;
},
_toolTipObserver: function() {
var toolTip = this.get('toolTip') ;
if (this.get('localize')) toolTip = toolTip.loc() ;
this.rootElement.title = toolTip ;
}.observes("toolTip"),
_isVisibleObserver: function() {
var flag = this.get('isVisible') ;
if ((this._isVisible === undefined) || (flag != this._isVisible)) {
this._isVisible = flag ;
if (flag) {
this._show() ;
} else this._hide() ;
// update parent state.
this._updateIsVisibleInWindow() ;
}
}.observes('isVisible'),
_updateIsVisibleInWindow: function(parentNodeState) {
if (parentNodeState === undefined) {
var parentNode = this.get('parentNode') ;
parentNodeState = (parentNode) ? parentNode.get('isVisibleInWindow') : false ;
}
var visible = parentNodeState && this.get('isVisible') ;
// if state changes, update and notify children.
if (visible != this.get('isVisibleInWindow')) {
this.set('isVisibleInWindow', visible) ;
this.recacheFrames() ;
var child = this.get('firstChild') ;
while(child) {
child._updateIsVisibleInWindow(visible) ;
child = child.get('nextSibling') ;
}
}
},
// Calling this method will show the view. Don't call this method
// directly but instead set the isVisible property to true. You can
// override this method to provide your own show capabilities.
_show: function(anchorView, triggerEvent) {
// compatibility
if (this.showView) return this.showView() ;
// if this is a type of pane, call the pane manager.
var paneType = this.get('paneType') ;
if (this.get('isPanel')) paneType = SC.PANEL_PANE; // compatibility
if (paneType) {
if (anchorView === undefined) anchorView = null ;
if (triggerEvent === undefined) triggerEvent = null ;
SC.PaneManager.manager().showPaneView(this, paneType, anchorView, triggerEvent) ;
this.set('displayIsVisible', true) ;
// if an animation is defined and animations are configured, use that.
// the displayIsVisible property will be set to true when the animation
// completes.
} else if (this.visibleAnimation && this.get('isAnimationEnabled')) {
this._transitionVisibleTo(1.0) ;
// at this point the animation has been reset to the beginng. Run the
// core show() method immediately so the animation will be visible.
this.show() ;
// otherwise, just change over visible settings.
} else {
this._visibleAnimator = null ;
this.show() ;
}
return this ;
},
_hide: function() {
// compatibility
if (this.hideView) return this.hideView() ;
// if this is a type of pane, call the pane manager.
var isPane = (!!this.get('paneType')) || this.get('isPanel') ;
if (isPane) {
SC.PaneManager.manager().hidePaneView(this) ;
this.set('displayIsVisible', false) ;
// if an animation is defined and animations are configured, use that.
// the displayIsVisible property will be set to false when the animation
// completes.
} else if (this.visibleAnimation && this.get('isAnimationEnabled')) {
this._transitionVisibleTo(0.0) ;
// otherwise, just change over visible settings.
} else {
this._visibleAnimator = null;
this.hide();
}
return this ;
},
_transitionVisibleTo: function(target) {
var a ;
// if an animator already exists, just transition to the new state.
if (this._visibleAnimator) {
this.transitionTo(target,this._visibleAnimator);
// otherwise, build the animator from the options passed. Patch in our
// own onComplete handler.
} else {
var opts = this.visibleAnimation ;
var style = [opts.hidden,opts.visible] ;
opts.onComplete =
this._animateVisibleDidComplete.bind(this,opts.onComplete) ;
this._visibleAnimator = this.transitionTo(target,style,opts);
}
},
// This is called when the animation completes. Finish cleaning up the
// visibility section.
_animateVisibleDidComplete: function(chainFunc) {
if (!this.get('isVisible')) this.hide() ;
if (chainFunc) chainFunc(this) ;
},
_firstResponderObserver: function(target, key, value) {
this.setClassName('focus',value) ;
}.observes('isFirstResponder'),
_dropTargetObserver: function() {
if (this.get('isDropTarget')) {
SC.Drag.addDropTarget(this) ;
} else SC.Drag.removeDropTarget(this) ;
}.observes('isDropTarget'),
// .............................................
// SPECIAL TYPES OF VIEWS
//
// This will show the pane as a popup or picker (depending on the paneType
// you have set.) This works just like setting isVisible to true, except
// that it also passes the anchorView and triggerEvent you pass in.
popup: function(anchorView, triggerEvent) {
// this will bypass the normal observer machinery, calling the private
// _show method ourselves. To avoid triggering _show twice, we patch up
// the internal _isVisible property.
this._isVisible = true ;
this._show(anchorView, triggerEvent) ;
this.set('isVisible', true);
},
// This can be used to manually add observers to the rootElement for the
// methods in the passed map. You generally don't want to do this since we
// handle event propgation through the responder chain.
configureObserverMethods: function(methodMap) {
for(var name in methodMap) {
if (!methodMap.hasOwnProperty(name)) continue ;
if (this[name]) {
var method = this[name].bindAsEventListener(this);
Event.observe(this.rootElement,methodMap[name],method) ;
}
}
},
//
// SC.Tree support
//
/**
@property
Returns the parent of this view.
Required by SC.Tree.
@returns {SC.View} Can be null.
*/
parent: function() {
return this.parentNode;
}.property('parentNode'),
/**
@property
Returns the childen of this view, as an SC.Array-compatible object.
Required by SC.Tree.
@returns {SC.Array}
*/
children: function() {
if (!this._viewChildren) this._viewChildren = SC._ViewChildren.create({ view: this });
return this._viewChildren;
}, // don't need to observe childNodes; SC._ViewChildren does it for us
/**
@property
Returns the number of children.
Required by SC.Tree.
@returns {Number}
*/
childCount: function() {
return this.childNodes.length;
}.property('childNodes')
// toString: function() {
// var el = this.rootElement ;
// var tagName = (!!el.tagName) ? el.tagName.toLowerCase() : 'document' ;
//
// var className = el.className ;
// className = (className && className.length>0) ? 'class=%@'.fmt(className) : null;
//
// var idName = el.id ;
// idName = (idName && idName.length>0) ? 'id=%@'.fmt(idName) : null;
//
// return "%@:%@<%@>".fmt(this._type, this._guid, [tagName,idName, className].compact().join(' ')) ;
// }
}) ;
//
// This is a private, internal class returned by SC.View.children(). It dips
// directly into the internals of SC.View to do its work.
//
// Note, this will still relay changes not made using this object.
//
// This cannot be implemented by SC.View itself, due to method naming collisions.
//
SC._ViewChildren = SC.Object.extend( SC.Array, {
view: null,
length: function() {
if (!view) return 0;
return view.childNodes.length;
}, // don't need to observe anything
childNodesObserver: function() {2
this.arrayContentDidChange();
}.observes('view.childNodes'),
//
// SC.Array support
//
/**
SC.Array primitive implementation.
@param {Number} idx
Starting index in the children to replace. If idx >= length, then append to
the end of the array.
@param {Number} amt
Number of children that should be removed, starting at *idx*.
@param {Array} objects
An array of zero or more childvews that should be inserted into children at
*idx*
*/
replace: function(idx, amt, objects) {
var insertBeforeView = null;
var len;
if (!view) return;
// TODO: replacing an array of views could be *way* faster
view.beginPropertyChanges();
if (idx >= 0) {
for (len = 0; amt > len; len++ ) {
// this.view.childNodes.length changes each iteration
if (idx < this.view.childNodes.length) this.view.removeChild(this.view.childNodes[idx]);
else break; // nothing more to remove
}
}
if (0 <= idx && idx+1 < this.view.childNodes.length) insertBeforeView = this.view.childNodes[idx+1];
if (objects) len = objects.length;
while (--len >= 0) {
this.view.insertBefore(objects[len], insertBeforeView);
insertBeforeView = objects[len];
}
view.endPropertyChanges();
// this.arrayContentDidChange() will be triggered by childNodesObserver if needed
},
/**
SC.Array primitive implementation.
This works on the view's *children*.
@param {Number} idx
The index of the item to return. If idx exceeds the current length, returns null.
*/
objectAt: function(idx)
{
if (!_view) return undefined;
if (idx < 0) return undefined ;
if (idx >= this._view.childNodes.length) return null;
return this._view.childNodes[idx];
}
});
// Class Methods
SC.View.mixin({
// this is the global registry of views. It's used to map elements back
// to the views that own them.
_view: {},
findViewForElement: function(el) {
var guid = el._configured ;
return (guid) ? SC.View._view[guid] : null ;
},
// ..........................................
// SETUP
//
// This works much like create except that it works on the passed in
// element instead of trying to find something new. If you pass null for
// the first parameter, then a new element will be created with the html
// you set in content.
viewFor: function(el,config) {
if (el) el = $(el) ;
var r = SC.idt.active ; var vStart ;
if (r) SC.idt.v_count++;
if (r) vStart = new Date().getTime() ;
// find or build the element.
if (!el) {
var emptyElement = this.prototype._cachedEmptyElement || this.prototype.emptyElement;
// if the emptyElement is a string not starting with '<', treat it like
// an id and find it in the doc. If an element is found, cache it for
// future use.
var isString = typeof(emptyElement) == 'string' ;
if (isString && (emptyElement.slice(0,1) != '<')) {
var el = $sel(emptyElement) ;
if (el) {
this.prototype.emptyElement = emptyElement = el ;
isString = false ;
}
}
// if still a string, then use it to create HTML. Save the generated
// element so that we can avoid doing this over again.
if (isString) {
SC._ViewCreator.innerHTML = emptyElement ;
el = $(SC._ViewCreator.firstChild) ;
SC.NodeCache.appendChild(el) ;
this.prototype._cachedEmptyElement = el.cloneNode(true) ;
} else if (typeof(emptyElement) == "object") {
if (emptyElement.tagName) {
el = emptyElement.cloneNode(true) ;
} else el = SC.NodeDescriptor.create(emptyElement) ;
}
}
if (r) SC.idt.vc_t += (new Date().getTime()) - vStart ;
// configure only once.
if (el && el._configured) return SC.View.findViewForElement(el);
// Now that we have found an element, instantiate the view.
var args = SC.$A(arguments) ; args[0] = { rootElement: el } ;
if (r) vStart = new Date().getTime();
var ret = new this(args,this) ; // create instance.
if (r) SC.idt.v_t += (new Date().getTime()) - vStart;
el._configured = ret._guid ;
// return the view.
SC.View._view[ret._guid] = ret ;
return ret ;
},
// create in the view work is like viewFor but with 'null' for el
create: function(configs) {
var args = SC.$A(arguments) ;
args.unshift(null) ;
return this.viewFor.apply(this,args) ;
},
// extend works just like a normal extend except that we need to delete the cached empty
// element.
extend: function(configs) {
var ret = SC.Object.extend.apply(this, arguments) ;
ret.prototype._cachedEmptyElement = null ;
return ret ;
},
/**
Creates a new subclass, add to the receiver any passed properties
or methods, and configure it as an outlet, which will cause an
instance of the subclass to be created during awake().
@params {Hash} props the methods of properties you want to add
@returns {Class} A new object class
*/
outlet: function(props) {
if (SC.BENCHMARK_OBJECTS) SC.Benchmark.start('SC.Object.outlet') ;
var viewClass = this.extend(props) ; // save the view class
var func = function() { return viewClass.viewFor(null) ; } ;
func.isOutlet = true ;
if (SC.BENCHMARK_OBJECTS) SC.Benchmark.end('SC.Object.outlet') ;
return func ;
},
/**
Defines a view as an outlet. This will return an function that
can be executed at a later time to actually create itself as an outlet.
*/
outletFor: function(path) {
var viewClass = this ; // save the view class
var func = function() {
if (SC.BENCHMARK_OUTLETS) SC.Benchmark.start("OUTLET(%@)".format(path)) ;
// if no path was passed, then create the view from scratch
if (path == null) {
var ret = viewClass.viewFor(null) ;
// otherwise, try to find the HTML element identified by the path.
// If the element cannot be found in the caller (the owner view), then
// search the entire document.
} else {
var ret = (this.$$sel) ? this.$$sel(path) : $$sel(path) ;
// if some HTML has been found, then loop through and create views for each
// one. Be sure to setup the proper parent view.
if (ret) {
var owner = this ; var views = [] ;
for(var loc=0;loc<ret.length;loc++) {
// create the new view instance
var view = viewClass.viewFor(ret[loc], { owner: owner }) ;
// if successful, then we need to determine the new parentNode.
// then walk up the DOM tree to find the first parent element
// managed by a view (including this).
//
// If a matching view is not found, but the view IS in a DOM
// somewhere then make the view a child of either SC.page or
// SC.window.
//
// Add the view to the list of child views also.
//
if (view && view.rootElement && view.rootElement.parentNode) {
var node = view.rootElement.parentNode;
var parentView = null ;
// go up the chain. stop when we find a parent view, or the rootElement
// for SC.page.
while(node && !parentView) {
switch(node) {
case this.rootElement:
parentView = this;
break ;
case SC.page.rootElement:
parentView = SC.page ;
break;
case SC.window.rootElement:
parentView = SC.window ;
break;
default:
node = node.parentNode ;
}
}
// if a parentView was found, then add to parentView.
if (parentView) {
parentView._insertBefore(view,null,false) ;
parentView._rebuildChildNodes() ; // this is not done with _insertBefore.
view._updateIsVisibleInWindow();
}
// view is not in a DOM. nothing to do.
}
// add to return array
views[views.length] = view ;
}
ret = views ;
ret = (ret.length == 0) ? null : ((ret.length == 1) ? ret[0] : ret);
}
}
if (SC.BENCHMARK_OUTLETS) SC.Benchmark.end("OUTLET(%@)".format(path)) ;
return ret ;
} ;
func.isOutlet = true ;
return func ;
},
automaticOutletFor: function() {
var ret = this.outletFor.apply(this, arguments) ;
ret.autoconfiguredOutlet = YES ;
return ret ;
}
}) ;
// IE Specfic Overrides
if (SC.Platform.IE) {
SC.View.prototype.getStyle = function(style) {
var element = this.rootElement ;
// collect value
style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
var value = element.style[style];
if (!value && element.currentStyle) value = element.currentStyle[style];
// handle opacity
if (style === 'opacity') {
if (value = (this.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) {
if (value[1]) value = parseFloat(value[1]) / 100;
}
value = 1.0;
}
// handle auto
if (value === 'auto') {
switch(style) {
case 'width':
if (this.getStyle('display') === 'none') {
value = null ;
} else if (element.currentStyle) {
var paddingLeft = parseInt(element.currentStyle.paddingLeft,0)||0;
var paddingRight = parseInt(element.currentStyle.paddingRight,0)||0;
var borderLeftWidth = parseInt(element.currentStyle.borderLeftWidth, 0) || 0 ;
var borderRightWidth = parseInt(element.currentStyle.borderRightWidth, 0) || 0 ;
value = (element.offsetWidth - paddingLeft - paddingRight - borderLeftWidth - borderRightWidth) + 'px' ;
}
break ;
case 'height':
if (this.getStyle('display') === 'none') {
value = null ;
} else if (element.currentStyle) {
var paddingTop = parseInt(element.currentStyle.paddingTop,0)||0;
var paddingBottom = parseInt(element.currentStyle.paddingBottom,0)||0;
var borderTopWidth = parseInt(element.currentStyle.borderTopWidth, 0) || 0 ;
var borderBottomWidth = parseInt(element.currentStyle.borderBottomWidth, 0) || 0 ;
value = (element.offsetHeight - paddingTop - paddingBottom - borderTopWidth - borderBottomWidth) + 'px' ;
}
break ;
default:
value = null ;
}
}
return value;
};
// Called from innerFrame to actually collect the values for the innerFrame.
// Normally the value we want for the width/height is stored in clientWidth/
// height but in IE this is only good if the element hasLayout. In this
// case always use the scrollWidth/Height.
SC.View._collectInnerFrame = function() {
var el = this.rootElement ;
var hasLayout = (el.currentStyle) ? el.currentStyle.hasLayout : false ;
var borderTopWidth = parseInt(el.currentStyle.borderTopWidth, 0) || 0 ;
var borderBottomWidth = parseInt(el.currentStyle.borderBottomWidth, 0) || 0 ;
var scrollHeight = el.offsetHeight - borderTopWidth - borderBottomWidth ;
if (el.clientWidth > el.scrollWidth) scrollHeight - 15 ;
return {
x: el.offsetLeft,
y: el.offsetTop,
width: (hasLayout) ? Math.min(el.scrollWidth, el.clientWidth) : el.scrollWidth,
height: (hasLayout) ? Math.min(scrollHeight, el.clientHeight) : scrollHeight
};
} ;
} else {
// Called from innerFrame to actually collect the values for the innerFrame.
// This method should return the smaller of the scrollWidth/height (which
// will be set if the element is scrollable), or the clientWdith/height
// (which is set if the element is not scrollable).
SC.View._collectInnerFrame = function() {
var el = this.rootElement ;
return {
x: el.offsetLeft,
y: el.offsetTop,
width: Math.min(el.scrollWidth, el.clientWidth),
height: Math.min(el.scrollHeight, el.clientHeight)
};
} ;
}
// this handler goes through the guid to avoid any potential memory leaks
SC.View._onscroll = function(evt) { $view(this)._onscroll(evt); } ;
SC.View.WIDTH_PADDING_STYLES = ['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth'];
SC.View.HEIGHT_PADDING_STYLES = ['paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth'];
SC.View.SCROLL_WIDTH_PADDING_STYLES = ['borderLeftWidth', 'borderRightWidth'];
SC.View.SCROLL_HEIGHT_PADDING_STYLES = ['borderTopWidth', 'borderBottomWidth'];
SC.View.elementFor = SC.View.viewFor ; // Old Sprout Compatibility.
// This div is used to create nodes. It should normally remain empty.
SC._ViewCreator = document.createElement('div') ;
// This div can be used to hold elements you don't want on the page right now.
SC.NodeCache = document.createElement('div') ;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment