// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2009 Sprout Systems, Inc. and contributors. // Portions ©2008-2009 Apple, Inc. All rights reserved. // License: Licened under MIT license (see license.js) // ========================================================================== sc_require('system/browser'); sc_require('system/event'); sc_require('system/cursor'); sc_require('system/responder') ; sc_require('mixins/string') ; SC.viewKey = SC.guidKey + "_view" ; /** Select a horizontal layout for various views.*/ SC.LAYOUT_HORIZONTAL = 'sc-layout-horizontal'; /** Select a vertical layout for various views.*/ SC.LAYOUT_VERTICAL = 'sc-layout-vertical'; /** @private */ SC._VIEW_DEFAULT_DIMS = 'marginTop marginLeft'.w(); /** Layout properties needed to anchor a view to the top. */ SC.ANCHOR_TOP = { top: 0 }; /** Layout properties needed to anchor a view to the left. */ SC.ANCHOR_LEFT = { left: 0 }; /* Layout properties to anchor a view to the top left */ SC.ANCHOR_TOP_LEFT = { top: 0, left: 0 }; /** Layout properties to anchoe view to the bottom. */ SC.ANCHOR_BOTTOM = { bottom: 0 }; /** Layout properties to anchor a view to the right. */ SC.ANCHOR_RIGHT = { right: 0 } ; /** Layout properties to anchor a view to the bottom right. */ SC.ANCHOR_BOTTOM_RIGHT = { bottom: 0, right: 0 }; /** Layout properties to take up the full width of a parent view. */ SC.FULL_WIDTH = { left: 0, right: 0 }; /** Layout properties to take up the full height of a parent view. */ SC.FULL_HEIGHT = { top: 0, bottom: 0 }; /** Layout properties to center. Note that you must also specify a width and height for this to work. */ SC.ANCHOR_CENTER = { centerX: 0, centerY: 0 }; /** Layout property for width, height */ SC.LAYOUT_AUTO = 'auto'; /** @private - custom array used for child views */ SC.EMPTY_CHILD_VIEWS_ARRAY = []; SC.EMPTY_CHILD_VIEWS_ARRAY.needsClone = YES; /** @class Base class for managing a view. View's provide two functions: 1. They translate state and events into drawing instructions for the web browser and 2. They act as first responders for incoming keyboard, mouse, and touch events. h2. View Initialization When a view is setup, there are several methods you can override that will be called at different times depending on how your view is created. Here is a guide to which method you want to override and when: - *init:* override this method for any general object setup (such as observers, starting timers and animations, etc) that you need to happen everytime the view is created, regardless of whether or not its layer exists yet. - *render:* override this method to generate or update your HTML to reflect the current state of your view. This method is called both when your view is first created and later anytime it needs to be updated. - *didCreateLayer:* the render() method is used to generate new HTML. Override this method to perform any additional setup on the DOM you might need to do after creating the view. For example, if you need to listen for events. - *willDestroyLayer:* if you implement didCreateLayer() to setup event listeners, you should implement this method as well to remove the same just before the DOM for your view is destroyed. - *updateLayer:* Normally, when a view needs to update its content, it will re-render the view using the render() method. If you would like to override this behavior with your own custom updating code, you can replace updateLayer() with your own implementation instead. @extends SC.Responder @extends SC.DelegateSupport @since SproutCore 1.0 */ SC.View = SC.Responder.extend(SC.DelegateSupport, /** @scope SC.View.prototype */ { concatenatedProperties: 'outlets displayProperties layoutProperties classNames renderMixin didCreateLayerMixin willDestroyLayerMixin'.w(), /** The current pane. @property {SC.Pane} */ pane: function() { var view = this ; while (view && !view.isPane) view = view.get('parentView') ; return view ; }.property('parentView').cacheable(), /** The page this view was instantiated from. This is set by the page object during instantiation. @property {SC.Page} */ page: null, /** The current split view this view is embedded in (may be null). @property {SC.SplitView} */ splitView: function() { var view = this ; while (view && !view.isSplitView) view = view.get('parentView') ; return view ; }.property('parentView').cacheable(), /** If the view is currently inserted into the DOM of a parent view, this property will point to the parent of the view. */ parentView: null, /** Optional background color. Will be applied to the view's element if set. This property is intended for one-off views that need a background element. If you plan to create many view instances it is probably better to use CSS. @property {String} */ backgroundColor: null, // .......................................................... // IS ENABLED SUPPORT // /** Set to true when the item is enabled. Note that changing this value will also alter the isVisibleInWindow property for this view and any child views. Note that if you apply the SC.Control mixin, changing this property will also automatically add or remove a 'disabled' CSS class name as well. This property is observable and bindable. @property {Boolean} */ isEnabled: YES, isEnabledBindingDefault: SC.Binding.oneWay().bool(), /** Computed property returns YES if the view and all of its parent views are enabled in the pane. You should use this property when deciding whether to respond to an incoming event or not. This property is not observable. @property {Boolean} */ isEnabledInPane: function() { var ret = this.get('isEnabled'), pv ; if (ret && (pv = this.get('parentView'))) ret = pv.get('isEnabledInPane'); return ret ; }.property('parentView', 'isEnabled'), // .......................................................... // IS VISIBLE IN WINDOW SUPPORT // /** The isVisible property determines if the view is shown in the view hierarchy it is a part of. A view can have isVisible == YES and still have isVisibleInWindow == NO. This occurs, for instance, when a parent view has isVisible == NO. Default is YES. The isVisible property is considered part of the layout and so changing it will trigger a layout update. @property {Boolean} */ isVisible: YES, isVisibleBindingDefault: SC.Binding.bool(), /** YES only if the view and all of its parent views are currently visible in the window. This property is used to optimize certain behaviors in the view. For example, updates to the view layer are not performed if the view until the view becomes visible in the window. */ isVisibleInWindow: NO, /** Recomputes the isVisibleInWindow property based on the visibility of the view and its parent. If the recomputed value differs from the current isVisibleInWindow state, this method will also call recomputIsVisibleInWindow() on its child views as well. As an optional optimization, you can pass the isVisibleInWindow state of the parentView if you already know it. You will not generally need to call or override this method yourself. It is used by the SC.View hierarchy to relay window visibility changes up and down the chain. @property {Boolean} parentViewIsVisible @returns {SC.View} receiver */ recomputeIsVisibleInWindow: function(parentViewIsVisible) { var last = this.get('isVisibleInWindow') ; var cur = this.get('isVisible'), parentView ; // isVisibleInWindow = isVisible && parentView.isVisibleInWindow // this approach only goes up to the parentView if necessary. if (cur) { cur = (parentViewIsVisible === undefined) ? ((parentView=this.get('parentView')) ? parentView.get('isVisibleInWindow') : NO) : parentViewIsVisible ; } // if the state has changed, update it and notify children if (last !== cur) { this.set('isVisibleInWindow', cur) ; var childViews = this.get('childViews'), len = childViews.length, idx; for(idx=0;idx=0) childViews.removeAt(idx); // The DOM will need some fixing up, note this on the view. view.parentViewDidChange() ; // notify views if (this.didRemoveChild) this.didRemoveChild(view); if (view.didRemoveFromParent) view.didRemoveFromParent(this) ; return this ; }, /** Removes all children from the parentView. @returns {SC.View} receiver */ removeAllChildren: function() { var childViews = this.get('childViews'), view ; while (view = childViews.objectAt(childViews.get('length')-1)) { this.removeChild(view) ; } return this ; }, /** Removes the view from its parentView, if one is found. Otherwise does nothing. @returns {SC.View} receiver */ removeFromParent: function() { var parent = this.get('parentView') ; if (parent) parent.removeChild(this) ; return this ; }, /** 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) { // suspend notifications view.beginPropertyChanges(); oldView.beginPropertyChanges(); this.beginPropertyChanges(); this.insertBefore(view,oldView).removeChild(oldView) ; // resume notifications this.endPropertyChanges(); oldView.endPropertyChanges(); view.endPropertyChanges(); return this; }, /** Replaces the current array of child views with the new array of child views. @param {Array} views views you want to add @returns {SC.View} receiver */ replaceAllChildren: function(views) { var len = views.get('length'), idx; this.beginPropertyChanges(); this.destroyLayer().removeAllChildren(); for(idx=0;idx= 0) { this.addObserver(dp[idx], this, this.displayDidChange) ; } // register for drags if (this.get('isDropTarget')) SC.Drag.addDropTarget(this) ; // register scroll views for autoscroll during drags if (this.get('isScrollable')) SC.Drag.addScrollableView(this) ; }, /** Wakes up the view. The default implementation immediately syncs any bindings, which may cause the view to need its display updated. You can override this method to perform any additional setup. Be sure to call sc_super to setup bindings and to call awake on childViews. It is best to awake a view before you add it to the DOM. This way when the DOM is generated, it will have the correct initial values and will not require any additional setup. @returns {void} */ awake: function() { sc_super(); var childViews = this.get('childViews'), len = childViews.length, idx ; for (idx=0; idx layout.maxHeight)) { f.height = layout.maxHeight ; } if (!SC.none(layout.minHeight) && (f.height < layout.minHeight)) { f.height = layout.minHeight ; } if (!SC.none(layout.maxWidth) && (f.width > layout.maxWidth)) { f.width = layout.maxWidth ; } if (!SC.none(layout.minWidth) && (f.width < layout.minWidth)) { f.width = layout.minWidth ; } // make sure width/height are never < 0 if (f.height < 0) f.height = 0 ; if (f.width < 0) f.width = 0 ; return f; }, computeParentDimensions: function(frame) { var ret, pv = this.get('parentView'), pf = (pv) ? pv.get('frame') : null ; if (pf) { ret = { width: pf.width, height: pf.height }; } else { var f = frame ; ret = { width: (f.left || 0) + (f.width || 0) + (f.right || 0), height: (f.top || 0) + (f.height || 0) + (f.bottom || 0) }; } return ret ; }, /** The clipping frame returns the visible portion of the view, taking into account the clippingFrame of the parent view. Keep in mind that the clippingFrame is in the context of the view itself, not it's parent view. Normally this will be calculate based on the intersection of your own clippingFrame and your parentView's clippingFrame. @property {Rect} */ clippingFrame: function() { var pv= this.get('parentView'), f = this.get('frame'), ret = f ; if (pv) { pv = pv.get('clippingFrame') ; ret = SC.intersectRects(pv, f) ; } ret.x -= f.x ; ret.y -= f.y ; return ret ; }.property('parentView', 'frame').cacheable(), /** @private Whenever the clippingFrame changes, this observer will fire, notifying child views that their frames have also changed. */ _sc_view_clippingFrameDidChange: function() { var cvs = this.get('childViews'), len = cvs.length, idx, cv ; for (idx=0; idx View A.beginXXX() // -> View B.beginXXX() // -> View C.begitXXX() // -> View D.beginXXX() // // ...later on, endXXX methods are called in reverse order of beginXXX... // // <- View D.endXXX() // <- View C.endXXX() // <- View B.endXXX() // <- View A.endXXX() // // See the two methods below for an example implementation. /** Call this method when you plan to begin a live resize. This will notify the receiver view and any of its children that are interested that the resize is about to begin. @returns {SC.View} receiver @test in viewDidResize */ beginLiveResize: function() { // call before children have been notified... if (this.willBeginLiveResize) this.willBeginLiveResize() ; // notify children in order var ary = this.get('childViews'), len = ary.length, idx, view ; for (idx=0; idx=0; --idx) { // loop backwards view = ary[idx] ; if (view.endLiveResize) view.endLiveResize() ; } // call *after* all children have been notified... if (this.didEndLiveResize) this.didEndLiveResize() ; return this ; }, /** layoutStyle describes the current styles to be written to your element based on the layout you defined. Both layoutStyle and frame reset when you edit the layout property. Both are read only. Computes the layout style settings needed for the current anchor. @property {Hash} @readOnly */ layoutStyle: function() { var layout = this.get('layout'), ret = {}, pdim = null, error, AUTO = SC.LAYOUT_AUTO; var stLayout = this.get('useStaticLayout'); if (layout.width !== undefined && layout.width === SC.LAYOUT_AUTO && !stLayout) { error= SC.Error.desc("%@.layout() you cannot use width:auto if staticLayout is disabled".fmt(this),"%@".fmt(this),-1); console.error(error.toString()) ; throw error ; } if (layout.height !== undefined && layout.height === SC.LAYOUT_AUTO && !stLayout) { error = SC.Error.desc("%@.layout() you cannot use height:auto if staticLayout is disabled".fmt(this),"%@".fmt(this),-1); console.error(error.toString()) ; throw error ; } // X DIRECTION // handle left aligned and left/right if (!SC.none(layout.left)) { ret.left = Math.floor(layout.left); if (layout.width !== undefined) { if(layout.width === SC.LAYOUT_AUTO) ret.width = SC.LAYOUT_AUTO ; else ret.width = Math.floor(layout.width) ; ret.right = null ; } else { ret.width = null ; ret.right = Math.floor(layout.right || 0) ; } ret.marginLeft = 0 ; // handle right aligned } else if (!SC.none(layout.right)) { ret.right = Math.floor(layout.right) ; ret.marginLeft = 0 ; if (SC.none(layout.width)) { ret.left = 0; ret.width = null; } else { ret.left = null ; if(layout.width === SC.LAYOUT_AUTO) ret.width = SC.LAYOUT_AUTO ; else ret.width = Math.floor(layout.width || 0) ; } // handle centered } else if (!SC.none(layout.centerX)) { ret.left = "50%"; ret.width = Math.floor(layout.width || 0) ; ret.marginLeft = Math.floor(layout.centerX - ret.width/2) ; ret.right = null ; // if width defined, assume top/left of zero } else if (!SC.none(layout.width)) { ret.left = 0; ret.right = null; if(layout.width === SC.LAYOUT_AUTO) ret.width = SC.LAYOUT_AUTO ; else ret.width = Math.floor(layout.width); ret.marginLeft = 0; // fallback, full width. } else { ret.left = 0; ret.right = 0; ret.width = null ; ret.marginLeft= 0; } // handle min/max ret.minWidth = (layout.minWidth === undefined) ? null : layout.minWidth ; ret.maxWidth = (layout.maxWidth === undefined) ? null : layout.maxWidth ; // Y DIRECTION // handle left aligned and left/right if (!SC.none(layout.top)) { ret.top = Math.floor(layout.top); if (layout.height !== undefined) { if(layout.height === SC.LAYOUT_AUTO) ret.height = SC.LAYOUT_AUTO ; else ret.height = Math.floor(layout.height) ; ret.bottom = null ; } else { ret.height = null ; ret.bottom = Math.floor(layout.bottom || 0) ; } ret.marginTop = 0 ; // handle right aligned } else if (!SC.none(layout.bottom)) { ret.marginTop = 0 ; ret.bottom = Math.floor(layout.bottom) ; if (SC.none(layout.height)) { ret.top = 0; ret.height = null ; } else { ret.top = null ; if(layout.height === SC.LAYOUT_AUTO) ret.height = SC.LAYOUT_AUTO ; else ret.height = Math.floor(layout.height || 0) ; } // handle centered } else if (!SC.none(layout.centerY)) { ret.top = "50%"; ret.height = Math.floor(layout.height || 0) ; ret.marginTop = Math.floor(layout.centerY - ret.height/2) ; ret.bottom = null ; } else if (!SC.none(layout.height)) { ret.top = 0; ret.bottom = null; if(layout.height === SC.LAYOUT_AUTO) ret.height = SC.LAYOUT_AUTO ; else ret.height = Math.floor(layout.height || 0) ; ret.marginTop = 0; // fallback, full width. } else { ret.top = 0; ret.bottom = 0; ret.height = null ; ret.marginTop= 0; } // handle min/max ret.minHeight = (layout.minHeight === undefined) ? null : layout.minHeight ; ret.maxHeight = (layout.maxHeight === undefined) ? null : layout.maxHeight ; // if zIndex is set, use it. otherwise let default shine through ret.zIndex = SC.none(layout.zIndex) ? null : layout.zIndex.toString(); // if backgroundPosition is set, use it. // otherwise let default shine through ret.backgroundPosition = SC.none(layout.backgroundPosition) ? null : layout.backgroundPosition.toString() ; // set default values to null to allow built-in CSS to shine through // currently applies only to marginLeft & marginTop var dims = SC._VIEW_DEFAULT_DIMS, loc = dims.length, x; while(--loc >=0) { x = dims[loc]; if (ret[x]===0) ret[x]=null; } // convert any numbers into a number + "px". for(var key in ret) { var value = ret[key]; if (typeof value === SC.T_NUMBER) ret[key] = (value + "px"); } return ret ; }.property().cacheable(), /** The view responsible for laying out this view. The default version returns the current parent view. */ layoutView: function() { return this.get('parentView') ; }.property('parentView').cacheable(), /** This method is called whenever a property changes that invalidates the layout of the view. Changing the layout will do this automatically, but you can add others if you want. @returns {SC.View} receiver */ layoutDidChange: function() { this.beginPropertyChanges() ; if (this.frame) this.notifyPropertyChange('frame') ; this.notifyPropertyChange('layoutStyle') ; this.endPropertyChanges() ; // notify layoutView... var layoutView = this.get('layoutView'); if (layoutView) { layoutView.set('childViewsNeedLayout', YES); layoutView.layoutDidChangeFor(this) ; if (layoutView.get('childViewsNeedLayout')) { layoutView.invokeOnce(layoutView.layoutChildViewsIfNeeded); } } return this ; }.observes('layout'), /** This this property to YES whenever the view needs to layout its child views. Normally this property is set automatically whenever the layout property for a child view changes. @property {Boolean} */ childViewsNeedLayout: NO, /** One of two methods that are invoked whenever one of your childViews layout changes. This method is invoked everytime a child view's layout changes to give you a chance to record the information about the view. Since this method may be called many times during a single run loop, you should keep this method pretty short. The other method called when layout changes, layoutChildViews(), is invoked only once at the end of the run loop. You should do any expensive operations (including changing a childView's actual layer) in this other method. Note that if as a result of running this method you decide that you do not need your layoutChildViews() method run later, you can set the childViewsNeedsLayout property to NO from this method and the layout method will not be called layer. @param {SC.View} childView the view whose layout has changed. @returns {void} */ layoutDidChangeFor: function(childView) { var set = this._needLayoutViews ; if (!set) set = this._needLayoutViews = SC.CoreSet.create(); set.add(childView); }, /** Called your layout method if the view currently needs to layout some child views. @param {Boolean} isVisible if true assume view is visible even if it is not. @returns {SC.View} receiver @test in layoutChildViews */ layoutChildViewsIfNeeded: function(isVisible) { if (!isVisible) isVisible = this.get('isVisibleInWindow'); if (isVisible && this.get('childViewsNeedLayout')) { this.set('childViewsNeedLayout', NO); this.layoutChildViews(); } return this ; }, /** Applies the current layout to the layer. This method is usually only called once per runloop. You can override this method to provide your own layout updating method if you want, though usually the better option is to override the layout method from the parent view. The default implementation of this method simply calls the renderLayout() method on the views that need layout. @returns {void} */ layoutChildViews: function() { var set = this._needLayoutViews, len = set ? set.length : 0, idx; var view, context, layer; for(idx=0;idx=0) { var viewClass = childViews[idx]; loc = childLocs[idx]; if (loc && viewClass && viewClass.loc) viewClass.loc(loc) ; } return this; // done! }, /** Internal method actually updates the localizated attributes on the view class. This is overloaded in design mode to also save the attributes. */ applyLocalizedAttributes: function(loc) { SC.mixin(this.prototype, loc) ; }, views: {} }) ; // ....................................................... // OUTLET BUILDER // /** Generates a computed property that will look up the passed property path the first time you try to get the value. Use this whenever you want to define an outlet that points to another view or object. The root object used for the path will be the receiver. */ SC.outlet = function(path) { return function(key) { return (this[key] = SC.objectForPropertyPath(path, this)) ; }.property(); }; /** @private on unload clear cached divs. */ SC.View.unload = function() { // delete view items this way to ensure the views are cleared. The hash // itself may be owned by multiple view subclasses. var views = SC.View.views; if (views) { for(var key in views) { if (!views.hasOwnProperty(key)) continue ; delete views[key]; } } } ; SC.Event.add(window, 'unload', SC.View, SC.View.unload) ;