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