// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2010 Sprout Systems, Inc. and contributors. // Portions ©2008-2010 Apple Inc. All rights reserved. // License: Licensed 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'; /** Default property to disable or enable by default the contextMenu */ SC.CONTEXT_MENU_ENABLED = YES; /** Default property to disable or enable if the focus can jump to the address bar or not. */ SC.TABBING_ONLY_INSIDE_DOCUMENT = YES; /** @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. Views 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. - *didAppendToDocument:* in theory all DOM setup could be done in didCreateLayer() as you already have a DOM element instantiated. However there is cases where the element has to be first appended to the Document because there is either a bug on the browser or you are using plugins which objects are not instantiated until you actually append the element to the DOM. This will allow you to do things like registering DOM events on flash or quicktime objects. @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, /** Activates use of brower's static layout. You can apply this mixin and still use absolute positioning. To activate static positioning, set this property to YES. @property {Boolean} */ useStaticLayout: NO, // .......................................................... // 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'), /** @private Observes the isEnabled property and resigns first responder if set to NO. This will avoid cases where, for example, a disabled text field retains its focus rings. @observes isEnabled */ _sc_view_isEnabledDidChange: function(){ if(!this.get('isEnabled') && this.get('isFirstResponder')){ this.resignFirstResponder(); } }.observes('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, /** By default we don't disable the context menu. Overriding this property can enable/disable the context menu per view. */ isContextMenuEnabled: function() { return SC.CONTEXT_MENU_ENABLED; }.property(), /** 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 previous = this.get('isVisibleInWindow'), current = this.get('isVisible'), parentView; // isVisibleInWindow = isVisible && parentView.isVisibleInWindow // this approach only goes up to the parentView if necessary. if (current) { // If we weren't passed in 'parentViewIsVisible' (we generally aren't; // it's an optimization), then calculate it. if (parentViewIsVisible === undefined) { parentView = this.get('parentView'); parentViewIsVisible = parentView ? parentView.get('isVisibleInWindow') : NO; } current = current && parentViewIsVisible; } // If our visibility has changed, then set the new value and notify our // child views to update their value. if (previous !== current) { this.set('isVisibleInWindow', current); 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 this|nil } return null ; //nothing to do. }, /** This method is invoked by interpretKeyEvents() when you receive a key event matching some plain text. You can use this to actually insert the text into your application, if needed. @param {SC.Event} event @returns {Object} receiver or object that handled event */ insertText: function(chr) { return NO ; }, /** Recursively travels down the view hierarchy looking for a view that implements the key equivalent (returning to YES to indicate it handled the event). You can override this method to handle specific key equivalents yourself. The keystring is a string description of the key combination pressed. The evt is the event itself. If you handle the equivalent, return YES. Otherwise, you should just return sc_super. @param {String} keystring @param {SC.Event} evt @returns {Boolean} */ performKeyEquivalent: function(keystring, evt) { var ret = NO, childViews = this.get('childViews'), len = childViews.length, idx = -1 ; while (!ret && (++idx < len)) { ret = childViews[idx].performKeyEquivalent(keystring, evt) ; } return ret ; }, /** Optionally points to the next key view that should gain focus when tabbing through an interface. If this is not set, then the next key view will be set automatically to the next child. */ nextKeyView: null, /** Computes the next valid key view, possibly returning the receiver or null. This is the next key view that acceptsFirstResponder. @property @type SC.View */ nextValidKeyView: function() { var seen = [], rootView = this.get('pane'), ret = this.get('nextKeyView'); if(!ret) ret = rootView._computeNextValidKeyView(this, seen); if(SC.TABBING_ONLY_INSIDE_DOCUMENT && !ret) { ret = rootView._computeNextValidKeyView(rootView, seen); } return ret ; }.property('nextKeyView'), _computeNextValidKeyView: function(currentView, seen) { var ret = this.get('nextKeyView'), children, i, childLen, child; if(this !== currentView && seen.indexOf(currentView)!=-1 && this.get('acceptsFirstResponder') && this.get('isVisibleInWindow')){ return this; } seen.push(this); // avoid cycles // find next sibling if (!ret) { children = this.get('childViews'); for(i=0, childLen = children.length; i= 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 contentClippingFrame 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 calculated based on the intersection of your own clippingFrame and your parentView's contentClippingFrame. @property {Rect} */ clippingFrame: function() { var f = this.get('frame'), ret = f, pv, cf; if (!f) return null; pv = this.get('parentView'); if (pv) { cf = pv.get('contentClippingFrame'); if (!cf) return f; ret = SC.intersectRects(cf, f); } ret.x -= f.x; ret.y -= f.y; return ret; }.property('parentView', 'frame').cacheable(), /** The clipping frame child views should intersect with. Normally this is the same as the regular clippingFrame. However, you may override this method if you want the child views to actually draw more or less content than is actually visible for some reason. Usually this is only used by the ScrollView to optimize drawing on touch devices. @property {Rect} */ contentClippingFrame: function() { return this.get('clippingFrame'); }.property('clippingFrame').cacheable(), /** @private This method is invoked whenever the clippingFrame changes, notifying each child view that its clippingFrame has 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 ; }, /** Setting wantsAcceleratedLayer to YES will use 3d transforms to move the layer when available. */ wantsAcceleratedLayer: NO, /** Specifies whether 3d transforms can be used to move the layer. */ hasAcceleratedLayer: function(){ return this.get('wantsAcceleratedLayer') && SC.platform.supportsAcceleratedLayers; }.property('wantsAcceleratedLayer').cacheable(), /** 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, dims = SC._VIEW_DEFAULT_DIMS, loc = dims.length, x, value, key, stLayout = this.get('useStaticLayout'), lR = layout.right, lL = layout.left, lT = layout.top, lB = layout.bottom, lW = layout.width, lH = layout.height, lMW = layout.maxWidth, lMH = layout.maxHeight, lcX = layout.centerX, lcY = layout.centerY, hasAcceleratedLayer = this.get('hasAcceleratedLayer'), translateTop = 0, translateLeft = 0; if (lW !== undefined && lW === 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 (lH !== undefined && lH === 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(lL)) { if(SC.isPercentage(lL)) { ret.left = (lL*100)+"%"; //percentage left } else if (hasAcceleratedLayer && SC.empty(lR)) { translateLeft = Math.floor(lL); ret.left = 0; } else { ret.left = Math.floor(lL); //px left } ret.marginLeft = 0 ; if (lW !== undefined) { if(lW === SC.LAYOUT_AUTO) ret.width = SC.LAYOUT_AUTO ; else if(SC.isPercentage(lW)) ret.width = (lW*100)+"%"; //percentage width else ret.width = Math.floor(lW) ; //px width ret.right = null ; } else { ret.width = null ; if(lR && SC.isPercentage(lR)) ret.right = (lR*100)+"%"; //percentage right else ret.right = Math.floor(lR || 0) ; //px right } // handle right aligned } else if (!SC.none(lR)) { if(SC.isPercentage(lR)) { ret.right = Math.floor(lR*100)+"%"; //percentage left }else{ ret.right = Math.floor(lR) ; } ret.marginLeft = 0 ; if (SC.none(lW)) { if (SC.none(lMW)) ret.left = 0; ret.width = null; } else { ret.left = null ; if(lW === SC.LAYOUT_AUTO) ret.width = SC.LAYOUT_AUTO ; else if(lW && SC.isPercentage(lW)) ret.width = (lW*100)+"%" ; //percentage width else ret.width = Math.floor(lW || 0) ; //px width } // handle centered } else if (!SC.none(lcX)) { ret.left = "50%"; if(lW && SC.isPercentage(lW)) ret.width = (lW*100)+"%" ; //percentage width else ret.width = Math.floor(lW || 0) ; if(lW && SC.isPercentage(lW) && (SC.isPercentage(lcX) || SC.isPercentage(lcX*-1))){ ret.marginLeft = Math.floor((lcX - lW/2)*100)+"%" ; }else if(lW && lW >= 1 && !SC.isPercentage(lcX)){ ret.marginLeft = Math.floor(lcX - ret.width/2) ; }else { // This error message happens whenever width is not set. console.warn("You have to set width and centerX usign both percentages or pixels"); ret.marginLeft = "50%"; } ret.right = null ; // if width defined, assume top/left of zero } else if (!SC.none(lW)) { ret.left = 0; ret.right = null; if(lW === SC.LAYOUT_AUTO) ret.width = SC.LAYOUT_AUTO ; else if(SC.isPercentage(lW)) ret.width = (lW*100)+"%"; else ret.width = Math.floor(lW); 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 top aligned and left/right if (!SC.none(lT)) { if(SC.isPercentage(lT)) { ret.top = (lT*100)+"%"; } else if (hasAcceleratedLayer && SC.empty(lB)) { translateTop = Math.floor(lT); ret.top = 0; } else { ret.top = Math.floor(lT); } if (lH !== undefined) { if(lH === SC.LAYOUT_AUTO) ret.height = SC.LAYOUT_AUTO ; else if(SC.isPercentage(lH)) ret.height = (lH*100)+"%" ; else ret.height = Math.floor(lH) ; ret.bottom = null ; } else { ret.height = null ; if(lB && SC.isPercentage(lB)) ret.bottom = (lB*100)+"%" ; else ret.bottom = Math.floor(lB || 0) ; } ret.marginTop = 0 ; // handle bottom aligned } else if (!SC.none(lB)) { ret.marginTop = 0 ; if(SC.isPercentage(lB)) ret.bottom = (lB*100)+"%"; else ret.bottom = Math.floor(lB) ; if (SC.none(lH)) { if (SC.none(lMH)) ret.top = 0; ret.height = null ; } else { ret.top = null ; if(lH === SC.LAYOUT_AUTO) ret.height = SC.LAYOUT_AUTO ; else if(lH && SC.isPercentage(lH)) ret.height = (lH*100)+"%" ; else ret.height = Math.floor(lH || 0) ; } // handle centered } else if (!SC.none(lcY)) { ret.top = "50%"; ret.bottom = null ; if(lH && SC.isPercentage(lH)) ret.height = (lH*100)+ "%" ; else ret.height = Math.floor(lH || 0) ; if(lH && SC.isPercentage(lH) && (SC.isPercentage(lcY) || SC.isPercentage(lcY*-1))){ //height is percentage and lcy too ret.marginTop = Math.floor((lcY - lH/2)*100)+"%" ; }else if(lH && lH >= 1 && !SC.isPercentage(lcY)){ ret.marginTop = Math.floor(lcY - ret.height/2) ; }else { console.warn("You have to set height and centerY to use both percentages or pixels"); ret.marginTop = "50%"; } } else if (!SC.none(lH)) { ret.top = 0; ret.bottom = null; if(lH === SC.LAYOUT_AUTO) ret.height = SC.LAYOUT_AUTO ; else if(lH && SC.isPercentage(lH)) ret.height = (lH*100)+"%" ; else ret.height = Math.floor(lH || 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 while(--loc >=0) { x = dims[loc]; if (ret[x]===0) ret[x]=null; } if (hasAcceleratedLayer) { var transform = 'translateX('+translateLeft+'px) translateY('+translateTop+'px)'; if (SC.platform.supportsCSS3DTransforms) transform += ' translateZ(0px)' ret[SC.platform.domCSSPrefix+'Transform'] = transform; } // convert any numbers into a number + "px". for(key in ret) { 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. Implementation Note: In a traditional setup, we would simply observe 'layout' here, but as described above in the documentation for our custom implementation of propertyDidChange(), this method must always run immediately after 'layout' is updated to avoid the potential for stale (incorrect) cached 'frame' values. @returns {SC.View} receiver */ layoutDidChange: function() { // Did our layout change in a way that could cause us to be resized? If // not, then there's no need to invalidate the frames of our child views. var previousLayout = this._previousLayout, currentLayout = this.get('layout'), didResize = YES, previousWidth, previousHeight, currentWidth, currentHeight; if (previousLayout && previousLayout !== currentLayout) { // This is a simple check to see whether we think the view may have // resized. We could look for a number of cases, but for now we'll // handle only one simple case: if the width and height are both // specified, and they have not changed. previousWidth = previousLayout.width; if (previousWidth !== undefined) { currentWidth = currentLayout.width; if (previousWidth === currentWidth) { previousHeight = previousLayout.height; if (previousLayout !== undefined) { currentHeight = currentLayout.height; if (previousHeight === currentHeight) didResize = NO; } } } } this.beginPropertyChanges() ; this.notifyPropertyChange('layoutStyle') ; if (didResize) { this.viewDidResize(); } else { // Even if we didn't resize, our frame might have changed. // viewDidResize() handles this in the other case. this._viewFrameDidChange(); } 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 ; }, /** 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, i; for (i = 0; i < len; ++i) { set[i].updateLayout(); } set.clear(); // reset & reuse }, /** Invoked by the layoutChildViews method to update the layout on a particular view. This method creates a render context and calls the renderLayout() method, which is probably what you want to override instead of this. You will not usually override this method, but you may call it if you implement layoutChildViews() in a view yourself. @returns {SC.View} receiver @test in layoutChildViews */ updateLayout: function() { var layer = this.get('layer'), context; if (layer) { context = this.renderContext(layer); this.renderLayout(context); context.update(); // If this view uses static layout, then notify if the frame changed. // (viewDidResize will do a comparison) if (this.useStaticLayout) this.viewDidResize(); } layer = null ; return this ; }, /** Default method called by the layout view to actually apply the current layout to the layer. The default implementation simply assigns the current layoutStyle to the layer. This method is also called whenever the layer is first created. @param {SC.RenderContext} the render context @returns {void} @test in layoutChildViews */ renderLayout: function(context, firstTime) { context.addStyle(this.get('layoutStyle')); }, /** walk like a duck */ isView: YES, /** Default method called when a selectstart event is triggered. This event is only supported by IE. Used in sproutcore to disable text selection and IE8 accelerators. The accelerators will be enabled only in text selectable views. In FF and Safari we use the css style 'allow-select'. If you want to enable text selection in certain controls is recommended to override this function to always return YES , instead of setting isTextSelectable to true. For example in textfield you dont want to enable textSelection on the text hint only on the actual text you are entering. You can achieve that by only overriding this method. @param evt {SC.Event} the selectstart event @returns YES if selectable */ selectStart: function(evt) { return this.get('isTextSelectable'); }, /** Used to block the contextMenu per view. @param evt {SC.Event} the contextmenu event @returns YES if the contextmenu can show up */ contextMenu: function(evt) { if(!this.get('isContextMenuEnabled')) evt.stop(); return true; }, /** A boundary set of distances outside which the touch will not be considered "inside" the view anymore. By default, up to 50px on each side. */ touchBoundary: { left: 50, right: 50, top: 50, bottom: 50 }, /** @private A computed property based on frame. */ _touchBoundaryFrame: function (){ return this.get("parentView").convertFrameToView(this.get('frame'), null); }.property("frame", "parentView").cacheable(), /** Returns YES if the provided touch is within the boundary. */ touchIsInBoundary: function(touch) { var f = this.get("_touchBoundaryFrame"), maxX = 0, maxY = 0, boundary = this.get("touchBoundary"); var x = touch.pageX, y = touch.pageY; if (x < f.x) { x = f.x - x; maxX = boundary.left; } else if (x > f.x + f.width) { x = x - (f.x + f.width); maxX = boundary.right; } else { x = 0; maxX = 1; } if (y < f.y) { y = f.y - y; maxY = boundary.top; } else if (y > f.y + f.height) { y = y - (f.y + f.height); maxY = boundary.bottom; } else { y = 0; maxY = 1; } if (x > 100 || y > 100) return NO; return YES; } }); SC.View.mixin(/** @scope SC.View */ { /** @private walk like a duck -- used by SC.Page */ isViewClass: YES, /** This method works just like extend() except that it will also preserve the passed attributes in case you want to use a view builder later, if needed. @param {Hash} attrs Attributes to add to view @returns {Class} SC.View subclass to create @function */ design: function() { if (this.isDesign) return this; // only run design one time var ret = this.extend.apply(this, arguments); ret.isDesign = YES ; if (SC.ViewDesigner) { SC.ViewDesigner.didLoadDesign(ret, this, SC.A(arguments)); } return ret ; }, /** Helper applies the layout to the prototype. */ layout: function(layout) { this.prototype.layout = layout ; return this ; }, /** Convert any layout to a Top, Left, Width, Height layout */ convertLayoutToAnchoredLayout: function(layout, parentFrame){ var ret = {top: 0, left: 0, width: parentFrame.width, height: parentFrame.height}, pFW = parentFrame.width, pFH = parentFrame.height, //shortHand for parentDimensions lR = layout.right, lL = layout.left, lT = layout.top, lB = layout.bottom, lW = layout.width, lH = layout.height, lcX = layout.centerX, lcY = layout.centerY; // X Conversion // handle left aligned and left/right if (!SC.none(lL)) { if(SC.isPercentage(lL)) ret.left = lL*pFW; else ret.left = lL; if (lW !== undefined) { if(lW === SC.LAYOUT_AUTO) ret.width = SC.LAYOUT_AUTO ; else if(SC.isPercentage(lW)) ret.width = lW*pFW ; else ret.width = lW ; } else { if (lR && SC.isPercentage(lR)) ret.width = pFW - ret.left - (lR*pFW); else ret.width = pFW - ret.left - (lR || 0); } // handle right aligned } else if (!SC.none(lR)) { // if no width, calculate it from the parent frame if (SC.none(lW)) { ret.left = 0; if(lR && SC.isPercentage(lR)) ret.width = pFW - (lR*pFW); else ret.width = pFW - (lR || 0); // If has width, calculate the left anchor from the width and right and parent frame } else { if(lW === SC.LAYOUT_AUTO) ret.width = SC.LAYOUT_AUTO ; else { if (SC.isPercentage(lW)) ret.width = lW*pFW; else ret.width = lW; if (SC.isPercentage(lR)) ret.left = pFW - (ret.width + lR); else ret.left = pFW - (ret.width + lR); } } // handle centered } else if (!SC.none(lcX)) { if(lW && SC.isPercentage(lW)) ret.width = (lW*pFW) ; else ret.width = (lW || 0) ; ret.left = ((pFW - ret.width)/2); if (SC.isPercentage(lcX)) ret.left = ret.left + lcX*pFW; else ret.left = ret.left + lcX; // if width defined, assume left of zero } else if (!SC.none(lW)) { ret.left = 0; if(lW === SC.LAYOUT_AUTO) ret.width = SC.LAYOUT_AUTO ; else { if(SC.isPercentage(lW)) ret.width = lW*pFW; else ret.width = lW; } // fallback, full width. } else { ret.left = 0; ret.width = 0; } // handle min/max if (layout.minWidth !== undefined) ret.minWidth = layout.minWidth ; if (layout.maxWidth !== undefined) ret.maxWidth = layout.maxWidth ; // Y Conversion // handle left aligned and top/bottom if (!SC.none(lT)) { if(SC.isPercentage(lT)) ret.top = lT*pFH; else ret.top = lT; if (lH !== undefined) { if(lH === SC.LAYOUT_AUTO) ret.height = SC.LAYOUT_AUTO ; else if (SC.isPercentage(lH)) ret.height = lH*pFH; else ret.height = lH ; } else { ret.height = pFH - ret.top; if(lB && SC.isPercentage(lB)) ret.height = ret.height - (lB*pFH); else ret.height = ret.height - (lB || 0); } // handle bottom aligned } else if (!SC.none(lB)) { // if no height, calculate it from the parent frame if (SC.none(lH)) { ret.top = 0; if (lB && SC.isPercentage(lB)) ret.height = pFH - (lB*pFH); else ret.height = pFH - (lB || 0); // If has height, calculate the top anchor from the height and bottom and parent frame } else { if(lH === SC.LAYOUT_AUTO) ret.height = SC.LAYOUT_AUTO ; else { if (SC.isPercentage(lH)) ret.height = lH*pFH; else ret.height = lH; ret.top = pFH - ret.height; if (SC.isPercentage(lB)) ret.top = ret.top - (lB*pFH); else ret.top = ret.top - lB; } } // handle centered } else if (!SC.none(lcY)) { if(lH && SC.isPercentage(lH)) ret.height = (lH*pFH) ; else ret.height = (lH || 0) ; ret.top = ((pFH - ret.height)/2); if(SC.isPercentage(lcY)) ret.top = ret.top + lcY*pFH; else ret.top = ret.top + lcY; // if height defined, assume top of zero } else if (!SC.none(lH)) { ret.top = 0; if(lH === SC.LAYOUT_AUTO) ret.height = SC.LAYOUT_AUTO ; else if (SC.isPercentage(lH)) ret.height = lH*pFH; else ret.height = lH; // fallback, full height. } else { ret.top = 0; ret.height = 0; } if(ret.top) ret.top = Math.floor(ret.top); if(ret.bottom) ret.bottom = Math.floor(ret.bottom); if(ret.left) ret.left = Math.floor(ret.left); if(ret.right) ret.right = Math.floor(ret.right); if(ret.width !== SC.LAYOUT_AUTO) ret.width = Math.floor(ret.width); if(ret.height !== SC.LAYOUT_AUTO) ret.height = Math.floor(ret.height); // handle min/max if (layout.minHeight !== undefined) ret.minHeight = layout.minHeight ; if (layout.maxHeight !== undefined) ret.maxHeight = layout.maxHeight ; return ret; }, /** For now can only convert Top/Left/Width/Height to a Custom Layout */ convertLayoutToCustomLayout: function(layout, layoutParams, parentFrame){ // TODO: [EG] Create Top/Left/Width/Height to a Custom Layout conversion }, /** Helper applies the classNames to the prototype */ classNames: function(sc) { sc = (this.prototype.classNames || []).concat(sc); this.prototype.classNames = sc; return this ; }, /** Help applies the tagName */ tagName: function(tg) { this.prototype.tagName = tg; return this ; }, /** Helper adds the childView */ childView: function(cv) { var childViews = this.prototype.childViews || []; if (childViews === this.superclass.prototype.childViews) { childViews = childViews.slice(); } childViews.push(cv) ; this.prototype.childViews = childViews; return this ; }, /** Helper adds a binding to a design */ bind: function(keyName, path) { var p = this.prototype, s = this.superclass.prototype; var bindings = p._bindings ; if (!bindings || bindings === s._bindings) { bindings = p._bindings = (bindings || []).slice() ; } keyName = keyName + "Binding"; p[keyName] = path ; bindings.push(keyName); return this ; }, /** Helper sets a generic property on a design. */ prop: function(keyName, value) { this.prototype[keyName] = value; return this ; }, /** Used to construct a localization for a view. The default implementation will simply return the passed attributes. */ localization: function(attrs, rootElement) { // add rootElement if (rootElement) attrs.rootElement = SC.$(rootElement)[0]; return attrs; }, /** Creates a view instance, first finding the DOM element you name and then using that as the root element. You should not use this method very often, but it is sometimes useful if you want to attach to already existing HTML. @param {String|Element} element @param {Hash} attrs @returns {SC.View} instance */ viewFor: function(element, attrs) { var args = SC.$A(arguments); // prepare to edit if (SC.none(element)) { args.shift(); // remove if no element passed } else args[0] = { rootElement: SC.$(element)[0] } ; var ret = this.create.apply(this, arguments) ; args = args[0] = null; return ret ; }, /** Create a new view with the passed attributes hash. If you have the Designer module loaded, this will also create a peer designer if needed. */ create: function() { var C=this, ret = new C(arguments); if (SC.ViewDesigner) { SC.ViewDesigner.didCreateView(ret, SC.$A(arguments)); } return ret ; }, /** Applies the passed localization hash to the component views. Call this method before you call create(). Returns the receiver. Typically you will do something like this: view = SC.View.design({...}).loc(localizationHash).create(); @param {Hash} loc @param rootElement {String} optional rootElement with prepped HTML @returns {SC.View} receiver */ loc: function(loc) { var childLocs = loc.childViews; delete loc.childViews; // clear out child views before applying to attrs this.applyLocalizedAttributes(loc) ; if (SC.ViewDesigner) { SC.ViewDesigner.didLoadLocalization(this, SC.$A(arguments)); } // apply localization recursively to childViews var childViews = this.prototype.childViews, idx = childViews.length, viewClass; while(--idx>=0) { 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, root) { return function(key) { return (this[key] = SC.objectForPropertyPath(path, (root !== undefined) ? root : 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]; } } } ; //unload views for IE, trying to collect memory. if(SC.browser.msie) SC.Event.add(window, 'unload', SC.View, SC.View.unload) ;