UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

604 lines (526 loc) 16.3 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2008 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Sebastian Werner (wpbasti) * Andreas Ecker (ecker) ************************************************************************ */ /** * Theme classes contain styling information for certain aspects of the * graphical user interface. * * Supported themes are: colors, decorations, fonts, icons, appearances. * The additional meta theme allows for grouping of the individual * themes. * * For more details, take a look at the * <a href='http://qooxdoo.org/docs/#desktop/gui/theming.md' target='_blank'> * documentation of the theme system in the qooxdoo manual.</a> */ qx.Bootstrap.define("qx.Theme", { statics: { /* --------------------------------------------------------------------------- PUBLIC API --------------------------------------------------------------------------- */ /** * Theme config * * Example: * <pre class='javascript'> * qx.Theme.define("name", * { * aliases : { * "aliasKey" : "resourceFolderOrUri" * }, * extend : otherTheme, * include : [MMixinTheme], * patch : [MMixinTheme], * colors : {}, * decorations : {}, * fonts : {}, * widgets : {}, * appearances : {}, * meta : {}, * boot : function(){} * }); * </pre> * * For more details, take a look at the * <a href='http://qooxdoo.org/docs/#desktop/gui/theming.md' target='_blank'> * documentation of the theme system in the qooxdoo manual.</a> * * @param name {String} name of the mixin * @param config {Map} config structure */ define(name, config) { if (!config) { var config = {}; } config.include = this.__normalizeArray(config.include); config.patch = this.__normalizeArray(config.patch); // Validate incoming data if (qx.core.Environment.get("qx.debug")) { this.__validateConfig(name, config); } // Create alias var theme = { $$type: "Theme", name: name, title: config.title, // Attach toString toString: this.genericToString }; // Remember extend if (config.extend) { theme.supertheme = config.extend; } // Assign to namespace theme.basename = qx.Bootstrap.createNamespace(name, theme); // Convert theme entry from Object to Function (for prototype inheritance) this.__convert(theme, config); this.__initializeAliases(theme, config); // Store class reference in global class registry this.$$registry[name] = theme; // Include mixin themes for (var i = 0, a = config.include, l = a.length; i < l; i++) { this.include(theme, a[i]); } for (var i = 0, a = config.patch, l = a.length; i < l; i++) { this.patch(theme, a[i]); } // Run boot code if (config.boot) { config.boot(); } }, /** * Normalize an object to an array * * @param objectOrArray {Object|Array} Either an object that is to be * normalized to an array, or an array, which is just passed through * * @return {Array} Either an array that has the original object as its * single item, or the original array itself */ __normalizeArray(objectOrArray) { if (!objectOrArray) { return []; } if (qx.Bootstrap.isArray(objectOrArray)) { return objectOrArray; } else { return [objectOrArray]; } }, /** * Initialize alias inheritance * * @param theme {Map} The theme * @param config {Map} config structure */ __initializeAliases(theme, config) { var aliases = config.aliases || {}; if (config.extend && config.extend.aliases) { qx.Bootstrap.objectMergeWith(aliases, config.extend.aliases, false); } theme.aliases = aliases; }, /** * Return a map of all known themes * * @return {Map} known themes */ getAll() { return this.$$registry; }, /** * Returns a theme by name * * @param name {String} theme name to check * @return {Object ? void} theme object */ getByName(name) { return this.$$registry[name]; }, /** * Determine if theme exists * * @param name {String} theme name to check * @return {Boolean} true if theme exists */ isDefined(name) { return this.getByName(name) !== undefined; }, /** * Determine the number of themes which are defined * * @return {Number} the number of classes */ getTotalNumber() { return qx.Bootstrap.objectGetLength(this.$$registry); }, /* --------------------------------------------------------------------------- PRIVATE/INTERNAL API --------------------------------------------------------------------------- */ /** * This method will be attached to all themes to return * a nice identifier for them. * * @internal * @return {String} The interface identifier */ genericToString() { return "[Theme " + this.name + "]"; }, /** * Extract the inheritable key (could be only one) * * @param config {Map} The map from where to extract the key * @return {String} the key which was found */ __extractType(config) { for ( var i = 0, keys = this.__inheritableKeys, l = keys.length; i < l; i++ ) { if (config[keys[i]]) { return keys[i]; } } }, /** * Convert existing entry to a prototype based inheritance function * * @param theme {Theme} newly created theme object * @param config {Map} incoming theme configuration */ __convert(theme, config) { var type = this.__extractType(config); // Use theme key from extended theme if own one is not available if (config.extend && !type) { type = config.extend.type; } // Save theme type theme.type = type || "other"; // Create pseudo class var clazz = function () {}; // Process extend config if (config.extend) { clazz.prototype = new config.extend.$$clazz(); } var target = clazz.prototype; var source = config[type]; // Copy entries to prototype for (var id in source) { target[id] = source[id]; // Appearance themes only: // Convert base flag to class reference (needed for mixin support) if (target[id].base) { if (qx.core.Environment.get("qx.debug")) { if (!config.extend) { throw new Error( "Found base flag in entry '" + id + "' of theme '" + config.name + "'. Base flags are not allowed for themes without a valid super theme!" ); } } target[id].base = config.extend; } } // store pseudo class theme.$$clazz = clazz; // and create instance under the old key theme[type] = new clazz(); }, /** @type {Map} Internal theme registry */ $$registry: {}, /** @type {Array} Keys which support inheritance */ __inheritableKeys: [ "colors", "borders", "decorations", "fonts", "icons", "widgets", "appearances", "meta" ], /** @type {Map} allowed keys in theme definition */ __allowedKeys: qx.core.Environment.select("qx.debug", { true: { title: "string", // String aliases: "object", // Map type: "string", // String extend: "object", // Theme colors: "object", // Map borders: "object", // Map decorations: "object", // Map fonts: "object", // Map icons: "object", // Map widgets: "object", // Map appearances: "object", // Map meta: "object", // Map include: "object", // Array patch: "object", // Array boot: "function" // Function }, default: null }), /** @type {Map} allowed keys inside a meta theme block */ __metaKeys: qx.core.Environment.select("qx.debug", { true: { color: "object", border: "object", decoration: "object", font: "object", icon: "object", appearance: "object", widget: "object" }, default: null }), /** * Validates incoming configuration and checks keys and values * * @signature function(name, config) * @param name {String} The name of the class * @param config {Map} Configuration map * @throws {Error} if the given config is not valid (e.g. wrong key or wrong key value) */ __validateConfig: qx.core.Environment.select("qx.debug", { true(name, config) { var allowed = this.__allowedKeys; for (var key in config) { if (allowed[key] === undefined) { throw new Error( 'The configuration key "' + key + '" in theme "' + name + '" is not allowed!' ); } if (config[key] == null) { throw new Error( 'Invalid key "' + key + '" in theme "' + name + '"! The value is undefined/null!' ); } if (allowed[key] !== null && typeof config[key] !== allowed[key]) { throw new Error( 'Invalid type of key "' + key + '" in theme "' + name + '"! The type of the key must be "' + allowed[key] + '"!' ); } } // Validate maps var maps = [ "colors", "borders", "decorations", "fonts", "icons", "widgets", "appearances", "meta" ]; for (var i = 0, l = maps.length; i < l; i++) { var key = maps[i]; if ( config[key] !== undefined && (config[key] instanceof Array || config[key] instanceof RegExp || config[key] instanceof Date || config[key].classname !== undefined) ) { throw new Error( 'Invalid key "' + key + '" in theme "' + name + '"! The value needs to be a map!' ); } } // Check conflicts (detect number ...) var counter = 0; for (var i = 0, l = maps.length; i < l; i++) { var key = maps[i]; if (config[key]) { counter++; } if (counter > 1) { throw new Error( "You can only define one theme category per file! Invalid theme: " + name ); } } // Validate meta if (config.meta) { var value; for (var key in config.meta) { value = config.meta[key]; if (this.__metaKeys[key] === undefined) { throw new Error( 'The key "' + key + '" is not allowed inside a meta theme block.' ); } if (typeof value !== this.__metaKeys[key]) { throw new Error( 'The type of the key "' + key + '" inside the meta block is wrong.' ); } if ( !( typeof value === "object" && value !== null && value.$$type === "Theme" ) ) { throw new Error( 'The content of a meta theme must reference to other themes. The value for "' + key + '" in theme "' + name + '" is invalid: ' + value ); } } } // Validate extend if (config.extend && config.extend.$$type !== "Theme") { throw new Error( 'Invalid extend in theme "' + name + '": ' + config.extend ); } // Validate include if (config.include) { for (var i = 0, l = config.include.length; i < l; i++) { if ( typeof config.include[i] == "undefined" || config.include[i].$$type !== "Theme" ) { throw new Error( 'Invalid include in theme "' + name + '": ' + config.include[i] ); } } } // Validate patch if (config.patch) { for (var i = 0, l = config.patch.length; i < l; i++) { if ( typeof config.patch[i] === "undefined" || config.patch[i].$$type !== "Theme" ) { throw new Error( 'Invalid patch in theme "' + name + '": ' + config.patch[i] ); } } } }, default() {} }), /** * Include all keys of the given mixin theme into the theme. The mixin may * include keys which are already defined in the target theme. Existing * features of equal name will be overwritten. * * @param theme {Theme} An existing theme which should be modified by including the mixin theme. * @param mixinTheme {Theme} The theme to be included. */ patch(theme, mixinTheme) { this.__checkForInvalidTheme(mixinTheme); var type = this.__extractType(mixinTheme); if (type !== this.__extractType(theme)) { throw new Error( "The mixins '" + theme.name + "' are not compatible '" + mixinTheme.name + "'!" ); } var source = mixinTheme[type]; var target = theme.$$clazz.prototype; for (var key in source) { target[key] = source[key]; } }, /** * Include all keys of the given mixin theme into the theme. If the * mixin includes any keys that are already available in the * class, they will be silently ignored. Use the {@link #patch} method * if you need to overwrite keys in the current class. * * @param theme {Theme} An existing theme which should be modified by including the mixin theme. * @param mixinTheme {Theme} The theme to be included. */ include(theme, mixinTheme) { this.__checkForInvalidTheme(mixinTheme); var type = mixinTheme.type; if (type !== theme.type) { throw new Error( "The mixins '" + theme.name + "' are not compatible '" + mixinTheme.name + "'!" ); } var source = mixinTheme[type]; var target = theme.$$clazz.prototype; for (var key in source) { //Skip keys already present if (target[key] !== undefined) { continue; } target[key] = source[key]; } }, /** * Helper method to check for an invalid theme * * @param mixinTheme {qx.Theme?null} theme to check * @throws {Error} if the theme is not valid */ __checkForInvalidTheme(mixinTheme) { if (typeof mixinTheme === "undefined" || mixinTheme == null) { var errorObj = new Error("Mixin theme is not a valid theme!"); if (qx.core.Environment.get("qx.debug")) { var stackTrace = qx.dev.StackTrace.getStackTraceFromError(errorObj); qx.Bootstrap.error(this, stackTrace); } throw errorObj; } } } });