1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 /** @class 9 Represents a theme, and is also the core theme in which SC looks for 10 other themes. 11 12 If an SC.View has a theme of "ace", it will look in its parent's theme 13 for the theme "ace". If there is no parent--that is, if the view is a 14 frame--it will look in SC.Theme for the named theme. To find a theme, 15 it calls find(themeName) on the theme. 16 17 To be located, themes must be registered either as a root theme (by 18 calling SC.Theme.addTheme) or as a child theme of another theme (by 19 calling theTheme.addTheme). 20 21 All themes are instances. However, new instances based on the current 22 instance can always be created: just call .create(). This method is used 23 by SC.View when you name a theme that doesn't actually exist: it creates 24 a theme based on the parent theme. 25 26 Locating Child Themes 27 ---------------------------- 28 Locating child themes is relatively simple for the most part: it looks in 29 its own "themes" property, which is an object inheriting from its parent's 30 "themes" set, so it includes all parent themes. 31 32 However, it does _not_ include global themes. This is because, when find() 33 is called, it wants to ensure any child theme is specialized. That is, the 34 child theme should include all class names of the base class theme. This only 35 makes sense if the theme really is a child theme of the theme or one of its 36 base classes; if the theme is a global theme, those class names should not 37 be included. 38 39 This makes sense logically as well, because when searching for a render delegate, 40 it will locate it in any base theme that has it, but that doesn't mean 41 class names from the derived theme shouldn't be included. 42 43 @extends SC.Object 44 @since SproutCore 1.1 45 @author Alex Iskander 46 */ 47 SC.Theme = { 48 /** 49 Walks like a duck. 50 */ 51 isTheme: YES, 52 53 /** 54 Class names for the theme. 55 56 These class names include the name of the theme and the names 57 of all parent themes. You can also add your own. 58 */ 59 classNames: [], 60 61 /** 62 @private 63 A helper to extend class names with another set of classnames. The 64 other set of class names can be a hash, an array, a Set, or a space- 65 delimited string. 66 */ 67 _extend_class_names: function(classNames) { 68 // class names may be a CoreSet, array, string, or hash 69 if (classNames) { 70 if (SC.typeOf(classNames) === SC.T_HASH && !classNames.isSet) { 71 for (var className in classNames) { 72 var index = this.classNames.indexOf(className); 73 if (classNames[className] && index < 0) { 74 this.classNames.push(className) 75 } else if (index >= 0) { 76 this.classNames.removeAt(index); 77 } 78 } 79 } else { 80 if (typeof classNames === "string") { 81 //@if(debug) 82 // There is no reason to support classNames as a String, it's just extra cases to have to support and makes for inconsistent code style. 83 SC.warn("Developer Warning: The classNames of a Theme should be an Array."); 84 //@endif 85 classNames = classNames.split(' '); 86 } 87 88 //@if(debug) 89 // There is no reason to support classNames as a Set, it's just extra cases to have to support and makes for inconsistent code style. 90 if (classNames.isSet) { 91 SC.warn("Developer Warning: The classNames of a Theme should be an Array."); 92 } 93 //@endif 94 95 // it must be an array or a CoreSet... 96 classNames.forEach(function (className) { 97 if (!this.classNames.contains(className)) { 98 this.classNames.push(className) 99 } 100 }, this); 101 } 102 } 103 }, 104 105 /** 106 @private 107 Helper method that extends this theme with some extra properties. 108 109 Used during Theme.create(); 110 */ 111 _extend_self: function(ext) { 112 if (ext.classNames) this._extend_class_names(ext.classNames); 113 114 // mixin while enabling sc_super(); 115 var key, value, cur; 116 for (key in ext) { 117 if (key === 'classNames') continue; // already handled. 118 if (!ext.hasOwnProperty(key)) continue; 119 120 value = ext[key]; 121 if (value instanceof Function && !value.base && (value !== (cur=this[key]))) { 122 value.base = cur; 123 } 124 125 this[key] = value; 126 } 127 }, 128 129 /** 130 Creates a new theme based on this one. The name of the new theme will 131 be added to the classNames set. 132 */ 133 create: function() { 134 var result = SC.beget(this); 135 result.baseTheme = this; 136 137 // if we don't beget themes, the same instance would be shared between 138 // all themes. this would be bad: imagine that we have two themes: 139 // "Ace" and "Other." Each one has a "capsule" child theme. If they 140 // didn't have their own child themes hash, the two capsule themes 141 // would conflict. 142 if (this.themes === SC.Theme.themes) { 143 result.themes = {}; 144 } else { 145 result.themes = SC.beget(this.themes); 146 } 147 148 // we also have private ("invisible") child themes; look at invisibleSubtheme 149 // method. 150 result._privateThemes = {}; 151 152 // also, the theme specializes all child themes as they are created 153 // to ensure that all of the class names on this theme are included. 154 result._specializedThemes = {}; 155 156 // we could put this in _extend_self, but we don't want to clone 157 // it for each and every argument passed to create(). 158 result.classNames = SC.clone(this.classNames); 159 160 var args = arguments, len = args.length, idx, mixin; 161 for (idx = 0; idx < len; idx++) { 162 result._extend_self(args[idx]); 163 } 164 165 if (result.name && !result.classNames.contains(result.name)) result.classNames.push(result.name); 166 167 return result; 168 }, 169 170 /** 171 Creates a child theme based on this theme, with the given name, 172 and automatically registers it as a child theme. 173 */ 174 subtheme: function(name) { 175 // extend the theme 176 var t = this.create({ name: name }); 177 178 // add to our set of themes 179 this.addTheme(t); 180 181 // and return the theme class 182 return t; 183 }, 184 185 /** 186 Semi-private, only used by SC.View to create "invisible" subthemes. You 187 should never need to call this directly, nor even worry about. 188 189 Invisible subthemes are only available when find is called _on this theme_; 190 if find() is called on a child theme, it will _not_ locate this theme. 191 192 The reason for "invisible" subthemes is that SC.View will create a subtheme 193 when it finds a theme name that doesn't exist. For example, imagine that you 194 have a parent view with theme "base", and a child view with theme "popup". 195 If no "popup" theme can be found inside "base", SC.View will call 196 base.subtheme. This will create a new theme with the name "popup", 197 derived from "base". Everyone is happy. 198 199 But what happens if you then change the parent theme to "ace"? The view 200 will try again to find "popup", and it will find it-- but it will still be 201 a child theme of "base"; SC.View _needs_ to re-subtheme it, but it won't 202 know it needs to, because it has been found. 203 */ 204 invisibleSubtheme: function(name) { 205 // extend the theme 206 var t = this.create({ name: name }); 207 208 // add to our set of themes 209 this._privateThemes[name] = t; 210 211 // and return the theme class 212 return t; 213 }, 214 215 // 216 // THEME MANAGEMENT 217 // 218 219 themes: {}, 220 221 /** 222 Finds a theme by name within this theme (the theme must have 223 previously been added to this theme or a base theme by using addTheme, or 224 been registered as a root theme). 225 226 If the theme found is not a root theme, this will specialize the theme so 227 that it includes all class names for this theme. 228 */ 229 find: function(themeName) { 230 if (this === SC.Theme) return this.themes[themeName]; 231 var theme; 232 233 // if there is a private theme (invisible subtheme) by that name, use it 234 theme = this._privateThemes[themeName]; 235 if (theme) return theme; 236 237 // if there is a specialized version (the theme extended with our class names) 238 // return that one 239 theme = this._specializedThemes[themeName]; 240 if (theme) return theme; 241 242 // otherwise, we may need to specialize one. 243 theme = this.themes[themeName]; 244 if (theme && !this._specializedThemes[themeName]) { 245 return (this._specializedThemes[themeName] = theme.create({ classNames: this.classNames })); 246 } 247 248 // and finally, if it is a root theme, we do nothing to it. 249 theme = SC.Theme.themes[themeName]; 250 if (theme) return theme; 251 252 return null; 253 }, 254 255 /** 256 Adds a child theme to the theme. This allows the theme to be located 257 by SproutCore views and such later. 258 259 Each theme is registered in the "themes" property by name. Calling 260 find(name) will return the theme with the given name. 261 262 Because the themes property is an object begetted from (based on) any 263 parent theme's "themes" property, if the theme cannot be found in this 264 theme, it will be found in any parent themes. 265 */ 266 addTheme: function(theme) { 267 this.themes[theme.name] = theme; 268 } 269 }; 270 271 // SproutCore _always_ has its base theme. This is not quite 272 // optimal, but the reasoning is because of test running: the 273 // test runner, when running foundation unit tests, cannot load 274 // the theme. As such, foundation must include default versions of 275 // all of its render delegates, and it does so in BaseTheme. All SproutCore 276 // controls have render delegates in BaseTheme. 277 SC.BaseTheme = SC.Theme.create({ 278 name: '' // it is a base class, and doesn't need a class name or such 279 }); 280 281 // however, SproutCore does need a default theme, even if no 282 // actual theme is loaded. 283 SC.Theme.themes['sc-base'] = SC.BaseTheme; 284 SC.defaultTheme = 'sc-base'; 285