1 sc_require("views/view"); 2 3 SC.View.reopen( 4 /** @scope SC.View.prototype */ { 5 6 // .......................................................... 7 // THEME SUPPORT 8 // 9 10 /** 11 Names which theme this view should use; the theme named by this property 12 will be set to the view's 'theme' property. 13 14 Themes are identified by their name. In addition to looking for the 15 theme globally, SproutCore will look for the theme inside 'baseTheme', 16 which is almost always the parent view's theme. 17 18 If null (the default), the view will set its 'theme' property to 19 be equal to 'baseTheme'. 20 21 Example: themeName: 'ace' 22 23 @type String 24 */ 25 themeName: null, 26 27 /** 28 Selects which theme to use as a 'base theme'. If null, the 'baseTheme' 29 property will be set to the parent's theme. If there is no parent, the theme 30 named by SC.defaultTheme is used. 31 32 This property is private for the time being. 33 34 @private 35 @type String 36 */ 37 baseThemeName: null, 38 39 /** 40 The SC.Theme instance which this view should use to render. 41 42 Note: the actual code for this function is in _themeProperty for backwards-compatibility: 43 some older views specify a string value for 'theme', which would override this property, 44 breaking it. 45 46 @property {SC.Theme} 47 */ 48 theme: function() { 49 var base = this.get('baseTheme'), themeName = this.get('themeName'); 50 51 // find theme, if possible 52 if (themeName) { 53 // Note: theme instance "find" function will search every parent 54 // _except_ global (which is not a parent) 55 var theme; 56 if (base) { 57 theme = base.find(themeName); 58 if (theme) { return theme; } 59 } 60 61 theme = SC.Theme.find(themeName); 62 if (theme) { return theme; } 63 64 // Create a new invisible subtheme. This will cause the themeName to 65 // be applied as a class name. 66 return base.invisibleSubtheme(themeName); 67 } 68 69 // can't find anything, return base. 70 return base; 71 }.property('baseTheme', 'themeName').cacheable(), 72 73 /** 74 Detects when the theme changes. Replaces the layer if necessary. 75 76 Also, because 77 */ 78 _sc_view_themeDidChange: function() { 79 var curTheme = this.get('theme'); 80 if (this._lastTheme === curTheme) { return; } 81 this._lastTheme = curTheme; 82 83 // invalidate child view base themes, if present 84 var childViews = this.childViews, len = childViews.length, idx; 85 for (idx = 0; idx < len; idx++) { 86 childViews[idx].notifyPropertyChange('baseTheme'); 87 } 88 89 if (this.get('layer')) { this.replaceLayer(); } 90 }.observes('theme'), 91 92 /** 93 The SC.Theme instance in which the 'theme' property should look for the theme 94 named by 'themeName'. 95 96 For example, if 'baseTheme' is SC.AceTheme, and 'themeName' is 'popover', 97 it will look to see if SC.AceTheme has a child theme named 'popover', 98 and _then_, if it is not found, look globally. 99 100 @private 101 @property {SC.Theme} 102 */ 103 baseTheme: function() { 104 var parent; 105 var baseThemeName = this.get('baseThemeName'); 106 if (baseThemeName) { 107 return SC.Theme.find(baseThemeName); 108 } else { 109 parent = this.get('parentView'); 110 var theme = parent && parent.get('theme'); 111 return theme || SC.Theme.find(SC.defaultTheme); 112 } 113 }.property('baseThemeName', 'parentView').cacheable(), 114 115 /** 116 The object to which rendering and updating the HTML representation of this 117 view should be delegated. 118 119 By default, views are responsible for creating their own HTML 120 representation. In some cases, however, you may want to create an object 121 that is responsible for rendering all views of a certain type. For example, 122 you may want rendering of SC.ButtonView to be controlled by an object that 123 is specific to the current theme. 124 125 By setting a render delegate, the render and update methods will be called 126 on that object instead of the view itself. 127 128 For your convenience, the view will provide its displayProperties to the 129 RenderDelegate. In some cases, you may have a conflict between the RenderDelegate's 130 API and your view's. For instance, you may have a 'value' property that is 131 any number, but the render delegate expects a percentage. Make a 'displayValue' 132 property, add _it_ to displayProperties instead of 'value', and the Render Delegate 133 will automatically use that when it wants to find 'value.' 134 135 You can also set the render delegate by using the 'renderDelegateName' property. 136 137 @type Object 138 */ 139 renderDelegate: function(key, value) { 140 if (value) { this._setRenderDelegate = value; } 141 if (this._setRenderDelegate) { return this._setRenderDelegate; } 142 143 // If this view does not have a render delegate but has 144 // renderDelegateName set, try to retrieve the render delegate from the 145 // theme. 146 var renderDelegateName = this.get('renderDelegateName'), renderDelegate; 147 148 if (renderDelegateName) { 149 renderDelegate = this.get('theme')[renderDelegateName]; 150 if (!renderDelegate) { 151 throw new Error("%@: Unable to locate render delegate \"%@\" in theme.".fmt(this, renderDelegateName)); 152 } 153 154 return renderDelegate; 155 } 156 157 return null; 158 }.property('renderDelegateName', 'theme'), 159 160 /** 161 The name of the property of the current theme that contains the render 162 delegate to use for this view. 163 164 By default, views are responsible for creating their own HTML 165 representation. You can tell the view to instead delegate rendering to the 166 theme by setting this property to the name of the corresponding property 167 of the theme. 168 169 For example, to tell the view that it should render using the 170 SC.ButtonView render delegate, set this property to 171 'buttonRenderDelegate'. When the view is created, it will retrieve the 172 buttonRenderDelegate property from its theme and set the renderDelegate 173 property to that object. 174 */ 175 renderDelegateName: null, 176 177 /** 178 [RO] Pass this object as the data source for render delegates. This proxy object 179 for the view relays requests for properties like 'title' to 'displayTitle' 180 as necessary. 181 182 If you ever communicate with your view's render delegate, you should pass this 183 object as the data source. 184 185 The proxy that forwards RenderDelegate requests for properties to the view, 186 handling display*, keeps track of the delegate's state, etc. 187 */ 188 renderDelegateProxy: function() { 189 return SC.View._RenderDelegateProxy.createForView(this); 190 }.property('renderDelegate').cacheable(), 191 192 /** 193 Invoked whenever your view needs to create its HTML representation. 194 195 You will normally override this method in your subclassed views to 196 provide whatever drawing functionality you will need in order to 197 render your content. 198 199 This method is usually only called once per view. After that, the update 200 method will be called to allow you to update the existing HTML 201 representation. 202 203 204 The default implementation of this method calls renderChildViews(). 205 206 For backwards compatibility, this method will also call the appropriate 207 method on a render delegate object, if your view has one. 208 209 @param {SC.RenderContext} context the render context 210 @returns {void} 211 */ 212 render: function(context, firstTime) { 213 var renderDelegate = this.get('renderDelegate'); 214 215 if (renderDelegate) { 216 if (firstTime) { 217 renderDelegate.render(this.get('renderDelegateProxy'), context); 218 } else { 219 renderDelegate.update(this.get('renderDelegateProxy'), context.$()); 220 } 221 } 222 }, 223 224 /** 225 Invokes a method on the render delegate, if one is present and it implements 226 that method. 227 228 @param {String} method The name of the method to call. 229 @param arg One or more arguments. 230 */ 231 invokeRenderDelegateMethod: function(method, args) { 232 var renderDelegate = this.get('renderDelegate'); 233 if (!renderDelegate) return undefined; 234 235 if (SC.typeOf(renderDelegate[method]) !== SC.T_FUNCTION) return undefined; 236 237 args = SC.$A(arguments); 238 args.shift(); 239 args.unshift(this.get('renderDelegateProxy')); 240 return renderDelegate[method].apply(renderDelegate, args); 241 } 242 }); 243 244 /** 245 @class 246 @private 247 View Render Delegate Proxies are tool SC.Views use to: 248 249 - look up 'display*' ('displayTitle' instead of 'title') to help deal with 250 differences between the render delegate's API and the view's. 251 252 RenderDelegateProxies are fully valid data sources for render delegates. They 253 act as proxies to the view, interpreting the .get and .didChangeFor commands 254 based on the view's displayProperties. 255 256 This tool is not useful outside of SC.View itself, and as such, is private. 257 */ 258 SC.View._RenderDelegateProxy = { 259 260 //@if(debug) 261 // for testing: 262 isViewRenderDelegateProxy: YES, 263 //@endif 264 265 /** 266 Creates a View Render Delegate Proxy for the specified view. 267 268 Implementation note: this creates a hash of the view's displayProperties 269 array so that the proxy may quickly determine whether a property is a 270 displayProperty or not. This could cause issues if the view's displayProperties 271 array is modified after instantiation. 272 273 @param {SC.View} view The view this proxy should proxy to. 274 @returns SC.View._RenderDelegateProxy 275 */ 276 createForView: function(view) { 277 var ret = SC.beget(this); 278 279 // set up displayProperty lookup for performance 280 var dp = view.get('displayProperties'), lookup = {}; 281 for (var idx = 0, len = dp.length; idx < len; idx++) { 282 lookup[dp[idx]] = YES; 283 } 284 285 // also allow the few special properties through 286 lookup.theme = YES; 287 288 ret._displayPropertiesLookup = lookup; 289 ret.renderState = {}; 290 291 ret._view = view; 292 return ret; 293 }, 294 295 296 /** 297 Provides the render delegate with any property it needs. 298 299 This first looks up whether the property exists in the view's 300 displayProperties, and whether it exists prefixed with 'display'; 301 for instance, if the render delegate asks for 'title', this will 302 look for 'displayTitle' in the view's displayProperties array. 303 304 @param {String} property The name of the property the render delegate needs. 305 @returns The value. 306 */ 307 get: function(property) { 308 if (this[property] !== undefined) { return this[property]; } 309 310 var displayProperty = 'display' + property.capitalize(); 311 312 if (this._displayPropertiesLookup[displayProperty]) { 313 return this._view.get(displayProperty); 314 } else { 315 return this._view.get(property); 316 } 317 }, 318 319 /** 320 Checks if any of the specified properties have changed. 321 322 For each property passed, this first determines whether to use the 323 'display' prefix. Then, it calls view.didChangeFor with context and that 324 property name. 325 */ 326 didChangeFor: function(context) { 327 var len = arguments.length, idx; 328 for (idx = 1; idx < len; idx++) { 329 var property = arguments[idx], 330 displayProperty = 'display' + property.capitalize(); 331 332 if (this._displayPropertiesLookup[displayProperty]) { 333 if (this._view.didChangeFor(context, displayProperty)) { return YES; } 334 } else { 335 if (this._view.didChangeFor(context, property)) { return YES; } 336 } 337 } 338 339 return NO; 340 } 341 }; 342 343 /** 344 Generates a computed property that will look up the specified property from 345 the view's render delegate, if present. You may specify a default value to 346 return if there is no such property or is no render delegate. 347 348 The generated property is read+write, so it may be overridden. 349 350 @param {String} propertyName The name of the property to get from the render delegate.. 351 @param {Value} def The default value to use if the property is not present. 352 */ 353 SC.propertyFromRenderDelegate = function(propertyName, def) { 354 return function(key, value) { 355 // first, handle set() case 356 if (value !== undefined) { 357 this['_set_rd_' + key] = value; 358 } 359 360 // use any value set manually via set() -- two lines ago. 361 var ret = this['_set_rd_' + key]; 362 if (ret !== undefined) return ret; 363 364 // finally, try to get it from the render delegate 365 var renderDelegate = this.get('renderDelegate'); 366 if (renderDelegate && renderDelegate.get) { 367 var proxy = this.get('renderDelegateProxy'); 368 ret = renderDelegate.getPropertyFor(proxy, propertyName); 369 } 370 371 if (ret !== undefined) return ret; 372 373 return def; 374 }.property('renderDelegate').cacheable(); 375 }; 376 377 378