UNPKG

angular-material-npfixed

Version:

The Angular Material project is an implementation of Material Design in Angular.js. This project provides a set of reusable, well-tested, and accessible Material Design UI components. Angular Material is supported internally at Google by the Angular.js, M

1,119 lines (968 loc) 37.6 kB
(function(angular) { 'use strict'; /** * @ngdoc module * @name material.core.theming * @description * Theming */ angular.module('material.core.theming', ['material.core.theming.palette', 'material.core.meta']) .directive('mdTheme', ThemingDirective) .directive('mdThemable', ThemableDirective) .directive('mdThemesDisabled', disableThemesDirective ) .provider('$mdTheming', ThemingProvider) .config( detectDisabledThemes ) .run(generateAllThemes); /** * Detect if the HTML or the BODY tags has a [md-themes-disabled] attribute * If yes, then immediately disable all theme stylesheet generation and DOM injection */ /** * @ngInject */ function detectDisabledThemes($mdThemingProvider) { var isDisabled = !!document.querySelector('[md-themes-disabled]'); $mdThemingProvider.disableTheming(isDisabled); } /** * @ngdoc service * @name $mdThemingProvider * @module material.core.theming * * @description Provider to configure the `$mdTheming` service. * * ### Default Theme * The `$mdThemingProvider` uses by default the following theme configuration: * * - Primary Palette: `Blue` * - Accent Palette: `Pink` * - Warn Palette: `Deep-Orange` * - Background Palette: `Grey` * * If you don't want to use the `md-theme` directive on the elements itself, you may want to overwrite * the default theme.<br/> * This can be done by using the following markup. * * <hljs lang="js"> * myAppModule.config(function($mdThemingProvider) { * $mdThemingProvider * .theme('default') * .primaryPalette('blue') * .accentPalette('teal') * .warnPalette('red') * .backgroundPalette('grey'); * }); * </hljs> * * ### Dynamic Themes * * By default, if you change a theme at runtime, the `$mdTheming` service will not detect those changes.<br/> * If you have an application, which changes its theme on runtime, you have to enable theme watching. * * <hljs lang="js"> * myAppModule.config(function($mdThemingProvider) { * // Enable theme watching. * $mdThemingProvider.alwaysWatchTheme(true); * }); * </hljs> * * ### Custom Theme Styles * * Sometimes you may want to use your own theme styles for some custom components.<br/> * You are able to register your own styles by using the following markup. * * <hljs lang="js"> * myAppModule.config(function($mdThemingProvider) { * // Register our custom stylesheet into the theming provider. * $mdThemingProvider.registerStyles(STYLESHEET); * }); * </hljs> * * The `registerStyles` method only accepts strings as value, so you're actually not able to load an external * stylesheet file into the `$mdThemingProvider`. * * If it's necessary to load an external stylesheet, we suggest using a bundler, which supports including raw content, * like [raw-loader](https://github.com/webpack/raw-loader) for `webpack`. * * <hljs lang="js"> * myAppModule.config(function($mdThemingProvider) { * // Register your custom stylesheet into the theming provider. * $mdThemingProvider.registerStyles(require('../styles/my-component.theme.css')); * }); * </hljs> * * ### Browser color * * Enables browser header coloring * for more info please visit: * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color * * Options parameter: <br/> * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme. <br/> * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', * 'accent', 'background' and 'warn'. Default is `primary`. <br/> * `hue` - The hue from the selected palette. Default is `800`<br/> * * <hljs lang="js"> * myAppModule.config(function($mdThemingProvider) { * // Enable browser color * $mdThemingProvider.enableBrowserColor({ * theme: 'myTheme', // Default is 'default' * palette: 'accent', // Default is 'primary', any basic material palette and extended palettes are available * hue: '200' // Default is '800' * }); * }); * </hljs> */ /** * @ngdoc method * @name $mdThemingProvider#registerStyles * @param {string} styles The styles to be appended to Angular Material's built in theme css. */ /** * @ngdoc method * @name $mdThemingProvider#setNonce * @param {string} nonceValue The nonce to be added as an attribute to the theme style tags. * Setting a value allows the use of CSP policy without using the unsafe-inline directive. */ /** * @ngdoc method * @name $mdThemingProvider#setDefaultTheme * @param {string} themeName Default theme name to be applied to elements. Default value is `default`. */ /** * @ngdoc method * @name $mdThemingProvider#alwaysWatchTheme * @param {boolean} watch Whether or not to always watch themes for changes and re-apply * classes when they change. Default is `false`. Enabling can reduce performance. */ /** * @ngdoc method * @name $mdThemingProvider#enableBrowserColor * @param {Object=} options Options object for the browser color<br/> * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme. <br/> * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', * 'accent', 'background' and 'warn'. Default is `primary`. <br/> * `hue` - The hue from the selected palette. Default is `800`<br/> * @returns {Function} remove function of the browser color */ /* Some Example Valid Theming Expressions * ======================================= * * Intention group expansion: (valid for primary, accent, warn, background) * * {{primary-100}} - grab shade 100 from the primary palette * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7 * {{primary-100-contrast}} - grab shade 100's contrast color * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1 * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules * * Foreground expansion: Applies rgba to black/white foreground text * * {{foreground-1}} - used for primary text * {{foreground-2}} - used for secondary text/divider * {{foreground-3}} - used for disabled text * {{foreground-4}} - used for dividers * */ // In memory generated CSS rules; registered by theme.name var GENERATED = { }; // In memory storage of defined themes and color palettes (both loaded by CSS, and user specified) var PALETTES; // Text Colors on light and dark backgrounds // @see https://www.google.com/design/spec/style/color.html#color-text-background-colors var DARK_FOREGROUND = { name: 'dark', '1': 'rgba(0,0,0,0.87)', '2': 'rgba(0,0,0,0.54)', '3': 'rgba(0,0,0,0.38)', '4': 'rgba(0,0,0,0.12)' }; var LIGHT_FOREGROUND = { name: 'light', '1': 'rgba(255,255,255,1.0)', '2': 'rgba(255,255,255,0.7)', '3': 'rgba(255,255,255,0.5)', '4': 'rgba(255,255,255,0.12)' }; var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)'; var LIGHT_SHADOW = ''; var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)'); var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87)'); var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)'); var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background']; var DEFAULT_COLOR_TYPE = 'primary'; // A color in a theme will use these hues by default, if not specified by user. var LIGHT_DEFAULT_HUES = { 'accent': { 'default': 'A200', 'hue-1': 'A100', 'hue-2': 'A400', 'hue-3': 'A700' }, 'background': { 'default': '50', 'hue-1': 'A100', 'hue-2': '100', 'hue-3': '300' } }; var DARK_DEFAULT_HUES = { 'background': { 'default': 'A400', 'hue-1': '800', 'hue-2': '900', 'hue-3': 'A200' } }; THEME_COLOR_TYPES.forEach(function(colorType) { // Color types with unspecified default hues will use these default hue values var defaultDefaultHues = { 'default': '500', 'hue-1': '300', 'hue-2': '800', 'hue-3': 'A100' }; if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues; if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues; }); var VALID_HUE_VALUES = [ '50', '100', '200', '300', '400', '500', '600', '700', '800', '900', 'A100', 'A200', 'A400', 'A700' ]; var themeConfig = { disableTheming : false, // Generate our themes at run time; also disable stylesheet DOM injection generateOnDemand : false, // Whether or not themes are to be generated on-demand (vs. eagerly). registeredStyles : [], // Custom styles registered to be used in the theming of custom components. nonce : null // Nonce to be added as an attribute to the generated themes style tags. }; /** * */ function ThemingProvider($mdColorPalette, $$mdMetaProvider) { PALETTES = { }; var THEMES = { }; var themingProvider; var alwaysWatchTheme = false; var defaultTheme = 'default'; // Load JS Defined Palettes angular.extend(PALETTES, $mdColorPalette); // Default theme defined in core.js /** * Adds `theme-color` and `msapplication-navbutton-color` meta tags with the color parameter * @param {string} color Hex value of the wanted browser color * @returns {Function} Remove function of the meta tags */ var setBrowserColor = function (color) { // Chrome, Firefox OS and Opera var removeChrome = $$mdMetaProvider.setMeta('theme-color', color); // Windows Phone var removeWindows = $$mdMetaProvider.setMeta('msapplication-navbutton-color', color); return function () { removeChrome(); removeWindows(); }; }; /** * Enables browser header coloring * for more info please visit: * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color * * The default color is `800` from `primary` palette of the `default` theme * * options are: * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', * 'accent', 'background' and 'warn'. Default is `primary` * `hue` - The hue from the selected palette. Default is `800` * * @param {Object=} options Options object for the browser color * @returns {Function} remove function of the browser color */ var enableBrowserColor = function (options) { options = angular.isObject(options) ? options : {}; var theme = options.theme || 'default'; var hue = options.hue || '800'; var palette = PALETTES[options.palette] || PALETTES[THEMES[theme].colors[options.palette || 'primary'].name]; var color = angular.isObject(palette[hue]) ? palette[hue].hex : palette[hue]; return setBrowserColor(color); }; return themingProvider = { definePalette: definePalette, extendPalette: extendPalette, theme: registerTheme, /** * return a read-only clone of the current theme configuration */ configuration : function() { return angular.extend( { }, themeConfig, { defaultTheme : defaultTheme, alwaysWatchTheme : alwaysWatchTheme, registeredStyles : [].concat(themeConfig.registeredStyles) }); }, /** * Easy way to disable theming without having to use * `.constant("$MD_THEME_CSS","");` This disables * all dynamic theme style sheet generations and injections... */ disableTheming: function(isDisabled) { themeConfig.disableTheming = angular.isUndefined(isDisabled) || !!isDisabled; }, registerStyles: function(styles) { themeConfig.registeredStyles.push(styles); }, setNonce: function(nonceValue) { themeConfig.nonce = nonceValue; }, generateThemesOnDemand: function(onDemand) { themeConfig.generateOnDemand = onDemand; }, setDefaultTheme: function(theme) { defaultTheme = theme; }, alwaysWatchTheme: function(alwaysWatch) { alwaysWatchTheme = alwaysWatch; }, enableBrowserColor: enableBrowserColor, $get: ThemingService, _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES, _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES, _PALETTES: PALETTES, _THEMES: THEMES, _parseRules: parseRules, _rgba: rgba }; // Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... }); function definePalette(name, map) { map = map || {}; PALETTES[name] = checkPaletteValid(name, map); return themingProvider; } // Returns an new object which is a copy of a given palette `name` with variables from // `map` overwritten // Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' }); function extendPalette(name, map) { return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) ); } // Make sure that palette has all required hues function checkPaletteValid(name, map) { var missingColors = VALID_HUE_VALUES.filter(function(field) { return !map[field]; }); if (missingColors.length) { throw new Error("Missing colors %1 in palette %2!" .replace('%1', missingColors.join(', ')) .replace('%2', name)); } return map; } // Register a theme (which is a collection of color palettes to use with various states // ie. warn, accent, primary ) // Optionally inherit from an existing theme // $mdThemingProvider.theme('custom-theme').primaryPalette('red'); function registerTheme(name, inheritFrom) { if (THEMES[name]) return THEMES[name]; inheritFrom = inheritFrom || 'default'; var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom; var theme = new Theme(name); if (parentTheme) { angular.forEach(parentTheme.colors, function(color, colorType) { theme.colors[colorType] = { name: color.name, // Make sure a COPY of the hues is given to the child color, // not the same reference. hues: angular.extend({}, color.hues) }; }); } THEMES[name] = theme; return theme; } function Theme(name) { var self = this; self.name = name; self.colors = {}; self.dark = setDark; setDark(false); function setDark(isDark) { isDark = arguments.length === 0 ? true : !!isDark; // If no change, abort if (isDark === self.isDark) return; self.isDark = isDark; self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND; self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW; // Light and dark themes have different default hues. // Go through each existing color type for this theme, and for every // hue value that is still the default hue value from the previous light/dark setting, // set it to the default hue value from the new light/dark setting. var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES; var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES; angular.forEach(newDefaultHues, function(newDefaults, colorType) { var color = self.colors[colorType]; var oldDefaults = oldDefaultHues[colorType]; if (color) { for (var hueName in color.hues) { if (color.hues[hueName] === oldDefaults[hueName]) { color.hues[hueName] = newDefaults[hueName]; } } } }); return self; } THEME_COLOR_TYPES.forEach(function(colorType) { var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType]; self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) { var color = self.colors[colorType] = { name: paletteName, hues: angular.extend({}, defaultHues, hues) }; Object.keys(color.hues).forEach(function(name) { if (!defaultHues[name]) { throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4" .replace('%1', name) .replace('%2', self.name) .replace('%3', paletteName) .replace('%4', Object.keys(defaultHues).join(', ')) ); } }); Object.keys(color.hues).map(function(key) { return color.hues[key]; }).forEach(function(hueValue) { if (VALID_HUE_VALUES.indexOf(hueValue) == -1) { throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5" .replace('%1', hueValue) .replace('%2', self.name) .replace('%3', colorType) .replace('%4', paletteName) .replace('%5', VALID_HUE_VALUES.join(', ')) ); } }); return self; }; self[colorType + 'Color'] = function() { var args = Array.prototype.slice.call(arguments); console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' + 'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.'); return self[colorType + 'Palette'].apply(self, args); }; }); } /** * @ngdoc service * @name $mdTheming * @module material.core.theming * * @description * * Service that makes an element apply theming related <b>classes</b> to itself. * * <hljs lang="js"> * app.directive('myFancyDirective', function($mdTheming) { * return { * restrict: 'e', * link: function(scope, el, attrs) { * $mdTheming(el); * } * }; * }); * </hljs> * @param {element=} element to apply theming to */ /** * @ngdoc property * @name $mdTheming#THEMES * @description * Property to get all the themes defined * @returns {Object} All the themes defined with their properties */ /** * @ngdoc property * @name $mdTheming#PALETTES * @description * Property to get all the palettes defined * @returns {Object} All the palettes defined with their colors */ /** * @ngdoc method * @name $mdTheming#registered * @description * Determine is specified theme name is a valid, registered theme * @param {string} themeName the theme to check if registered * @returns {boolean} whether the theme is registered or not */ /** * @ngdoc method * @name $mdTheming#defaultTheme * @description * Returns the default theme * @returns {string} The default theme */ /** * @ngdoc method * @name $mdTheming#generateTheme * @description * Lazy generate themes - by default, every theme is generated when defined. * You can disable this in the configuration section using the * `$mdThemingProvider.generateThemesOnDemand(true);` * * The theme name that is passed in must match the name of the theme that was defined as part of the configuration block. * * @param name {string} theme name to generate */ /** * @ngdoc method * @name $mdTheming#setBrowserColor * @description * Sets browser header coloring * for more info please visit: * https://developers.google.com/web/fundamentals/design-and-ui/browser-customization/theme-color * * The default color is `800` from `primary` palette of the `default` theme * * options are:<br/> * `theme` - A defined theme via `$mdThemeProvider` to use the palettes from. Default is `default` theme.<br/> * `palette` - Can be any one of the basic material design palettes, extended defined palettes and 'primary', * 'accent', 'background' and 'warn'. Default is `primary`<br/> * `hue` - The hue from the selected palette. Default is `800` * * @param {Object} options Options object for the browser color * @returns {Function} remove function of the browser color */ /* @ngInject */ function ThemingService($rootScope, $mdUtil, $q, $log) { // Allow us to be invoked via a linking function signature. var applyTheme = function (scope, el) { if (el === undefined) { el = scope; scope = undefined; } if (scope === undefined) { scope = $rootScope; } applyTheme.inherit(el, el); }; Object.defineProperty(applyTheme, 'THEMES', { get: function () { return angular.extend({}, THEMES); } }); Object.defineProperty(applyTheme, 'PALETTES', { get: function () { return angular.extend({}, PALETTES); } }); Object.defineProperty(applyTheme, 'ALWAYS_WATCH', { get: function () { return alwaysWatchTheme; } }); applyTheme.inherit = inheritTheme; applyTheme.registered = registered; applyTheme.defaultTheme = function() { return defaultTheme; }; applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, themeConfig.nonce); }; applyTheme.defineTheme = function(name, options) { options = options || {}; var theme = registerTheme(name); if (options.primary) { theme.primaryPalette(options.primary); } if (options.accent) { theme.accentPalette(options.accent); } if (options.warn) { theme.warnPalette(options.warn); } if (options.background) { theme.backgroundPalette(options.background); } if (options.dark){ theme.dark(); } this.generateTheme(name); return $q.resolve(name); }; applyTheme.setBrowserColor = enableBrowserColor; return applyTheme; /** * Determine is specified theme name is a valid, registered theme */ function registered(themeName) { if (themeName === undefined || themeName === '') return true; return applyTheme.THEMES[themeName] !== undefined; } /** * Get theme name for the element, then update with Theme CSS class */ function inheritTheme (el, parent) { var ctrl = parent.controller('mdTheme') || el.data('$mdThemeController'); updateThemeClass(lookupThemeName()); if (ctrl) { var watchTheme = alwaysWatchTheme || ctrl.$shouldWatch || $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')); var unwatch = ctrl.registerChanges(function (name) { updateThemeClass(name); if (!watchTheme) { unwatch(); } else { el.on('$destroy', unwatch); } }); } /** * Find the theme name from the parent controller or element data */ function lookupThemeName() { // As a few components (dialog) add their controllers later, we should also watch for a controller init. return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme); } /** * Remove old theme class and apply a new one * NOTE: if not a valid theme name, then the current name is not changed */ function updateThemeClass(theme) { if (!theme) return; if (!registered(theme)) { $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' + 'Register it with $mdThemingProvider.theme().'); } var oldTheme = el.data('$mdThemeName'); if (oldTheme) el.removeClass('md-' + oldTheme +'-theme'); el.addClass('md-' + theme + '-theme'); el.data('$mdThemeName', theme); if (ctrl) { el.data('$mdThemeController', ctrl); } } } } } function ThemingDirective($mdTheming, $interpolate, $parse, $mdUtil, $q, $log) { return { priority: 101, // has to be more than 100 to be before interpolation (issue on IE) link: { pre: function(scope, el, attrs) { var registeredCallbacks = []; var startSymbol = $interpolate.startSymbol(); var endSymbol = $interpolate.endSymbol(); var theme = attrs.mdTheme.trim(); var hasInterpolation = theme.substr(0, startSymbol.length) === startSymbol && theme.lastIndexOf(endSymbol) === theme.length - endSymbol.length; var oneTimeOperator = '::'; var oneTimeBind = attrs.mdTheme .split(startSymbol).join('') .split(endSymbol).join('') .trim() .substr(0, oneTimeOperator.length) === oneTimeOperator; var ctrl = { registerChanges: function (cb, context) { if (context) { cb = angular.bind(context, cb); } registeredCallbacks.push(cb); return function () { var index = registeredCallbacks.indexOf(cb); if (index > -1) { registeredCallbacks.splice(index, 1); } }; }, $setTheme: function (theme) { if (!$mdTheming.registered(theme)) { $log.warn('attempted to use unregistered theme \'' + theme + '\''); } ctrl.$mdTheme = theme; // Iterating backwards to support unregistering during iteration // http://stackoverflow.com/a/9882349/890293 // we don't use `reverse()` of array because it mutates the array and we don't want it to get re-indexed for (var i = registeredCallbacks.length; i--;) { registeredCallbacks[i](theme); } }, $shouldWatch: $mdUtil.parseAttributeBoolean(el.attr('md-theme-watch')) || $mdTheming.ALWAYS_WATCH || (hasInterpolation && !oneTimeBind) }; el.data('$mdThemeController', ctrl); var getTheme = function () { var interpolation = $interpolate(attrs.mdTheme)(scope); return $parse(interpolation)(scope) || interpolation; }; var setParsedTheme = function (theme) { if (typeof theme === 'string') { return ctrl.$setTheme(theme); } $q.when( angular.isFunction(theme) ? theme() : theme ) .then(function(name){ ctrl.$setTheme(name); }); }; setParsedTheme(getTheme()); var unwatch = scope.$watch(getTheme, function(theme) { if (theme) { setParsedTheme(theme); if (!ctrl.$shouldWatch) { unwatch(); } } }); } } }; } /** * Special directive that will disable ALL runtime Theme style generation and DOM injection * * <link rel="stylesheet" href="angular-material.min.css"> * <link rel="stylesheet" href="angular-material.themes.css"> * * <body md-themes-disabled> * ... * </body> * * Note: Using md-themes-css directive requires the developer to load external * theme stylesheets; e.g. custom themes from Material-Tools: * * `angular-material.themes.css` * * Another option is to use the ThemingProvider to configure and disable the attribute * conversions; this would obviate the use of the `md-themes-css` directive * */ function disableThemesDirective() { themeConfig.disableTheming = true; // Return a 1x-only, first-match attribute directive return { restrict : 'A', priority : '900' }; } function ThemableDirective($mdTheming) { return $mdTheming; } function parseRules(theme, colorType, rules) { checkValidPalette(theme, colorType); rules = rules.replace(/THEME_NAME/g, theme.name); var generatedRules = []; var color = theme.colors[colorType]; var themeNameRegex = new RegExp('\\.md-' + theme.name + '-theme', 'g'); // Matches '{{ primary-color }}', etc var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g'); var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g; var palette = PALETTES[color.name]; // find and replace simple variables where we use a specific hue, not an entire palette // eg. "{{primary-100}}" //\(' + THEME_COLOR_TYPES.join('\|') + '\)' rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) { if (colorType === 'foreground') { if (hue == 'shadow') { return theme.foregroundShadow; } else { return theme.foregroundPalette[hue] || theme.foregroundPalette['1']; } } // `default` is also accepted as a hue-value, because the background palettes are // using it as a name for the default hue. if (hue.indexOf('hue') === 0 || hue === 'default') { hue = theme.colors[colorType].hues[hue]; } return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity ); }); // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3) angular.forEach(color.hues, function(hueValue, hueName) { var newRule = rules .replace(hueRegex, function(match, _, colorType, hueType, opacity) { return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity); }); if (hueName !== 'default') { newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName); } // Don't apply a selector rule to the default theme, making it easier to override // styles of the base-component if (theme.name == 'default') { var themeRuleRegex = /((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)\.md-default-theme((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g; newRule = newRule.replace(themeRuleRegex, function(match, start, end) { return match + ', ' + start + end; }); } generatedRules.push(newRule); }); return generatedRules; } var rulesByType = {}; // Generate our themes at run time given the state of THEMES and PALETTES function generateAllThemes($injector, $mdTheming) { var head = document.head; var firstChild = head ? head.firstElementChild : null; var themeCss = !themeConfig.disableTheming && $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : ''; // Append our custom registered styles to the theme stylesheet. themeCss += themeConfig.registeredStyles.join(''); if ( !firstChild ) return; if (themeCss.length === 0) return; // no rules, so no point in running this expensive task // Expose contrast colors for palettes to ensure that text is always readable angular.forEach(PALETTES, sanitizePalette); // MD_THEME_CSS is a string generated by the build process that includes all the themable // components as templates // Break the CSS into individual rules var rules = themeCss .split(/\}(?!(\}|'|"|;))/) .filter(function(rule) { return rule && rule.trim().length; }) .map(function(rule) { return rule.trim() + '}'; }); var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g'); THEME_COLOR_TYPES.forEach(function(type) { rulesByType[type] = ''; }); // Sort the rules based on type, allowing us to do color substitution on a per-type basis rules.forEach(function(rule) { var match = rule.match(ruleMatchRegex); // First: test that if the rule has '.md-accent', it goes into the accent set of rules for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) { if (rule.indexOf('.md-' + type) > -1) { return rulesByType[type] += rule; } } // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from // there for (i = 0; type = THEME_COLOR_TYPES[i]; i++) { if (rule.indexOf(type) > -1) { return rulesByType[type] += rule; } } // Default to the primary array return rulesByType[DEFAULT_COLOR_TYPE] += rule; }); // If themes are being generated on-demand, quit here. The user will later manually // call generateTheme to do this on a theme-by-theme basis. if (themeConfig.generateOnDemand) return; angular.forEach($mdTheming.THEMES, function(theme) { if (!GENERATED[theme.name] && !($mdTheming.defaultTheme() !== 'default' && theme.name === 'default')) { generateTheme(theme, theme.name, themeConfig.nonce); } }); // ************************* // Internal functions // ************************* // The user specifies a 'default' contrast color as either light or dark, // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light) function sanitizePalette(palette, name) { var defaultContrast = palette.contrastDefaultColor; var lightColors = palette.contrastLightColors || []; var strongLightColors = palette.contrastStrongLightColors || []; var darkColors = palette.contrastDarkColors || []; // These colors are provided as space-separated lists if (typeof lightColors === 'string') lightColors = lightColors.split(' '); if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' '); if (typeof darkColors === 'string') darkColors = darkColors.split(' '); // Cleanup after ourselves delete palette.contrastDefaultColor; delete palette.contrastLightColors; delete palette.contrastStrongLightColors; delete palette.contrastDarkColors; // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR } angular.forEach(palette, function(hueValue, hueName) { if (angular.isObject(hueValue)) return; // Already converted // Map everything to rgb colors var rgbValue = colorToRgbaArray(hueValue); if (!rgbValue) { throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected." .replace('%1', hueValue) .replace('%2', palette.name) .replace('%3', hueName)); } palette[hueName] = { hex: palette[hueName], value: rgbValue, contrast: getContrastColor() }; function getContrastColor() { if (defaultContrast === 'light') { if (darkColors.indexOf(hueName) > -1) { return DARK_CONTRAST_COLOR; } else { return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR : LIGHT_CONTRAST_COLOR; } } else { if (lightColors.indexOf(hueName) > -1) { return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR : LIGHT_CONTRAST_COLOR; } else { return DARK_CONTRAST_COLOR; } } } }); } } function generateTheme(theme, name, nonce) { var head = document.head; var firstChild = head ? head.firstElementChild : null; if (!GENERATED[name]) { // For each theme, use the color palettes specified for // `primary`, `warn` and `accent` to generate CSS rules. THEME_COLOR_TYPES.forEach(function(colorType) { var styleStrings = parseRules(theme, colorType, rulesByType[colorType]); while (styleStrings.length) { var styleContent = styleStrings.shift(); if (styleContent) { var style = document.createElement('style'); style.setAttribute('md-theme-style', ''); if (nonce) { style.setAttribute('nonce', nonce); } style.appendChild(document.createTextNode(styleContent)); head.insertBefore(style, firstChild); } } }); GENERATED[theme.name] = true; } } function checkValidPalette(theme, colorType) { // If theme attempts to use a palette that doesnt exist, throw error if (!PALETTES[ (theme.colors[colorType] || {}).name ]) { throw new Error( "You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3" .replace('%1', theme.name) .replace('%2', colorType) .replace('%3', Object.keys(PALETTES).join(', ')) ); } } function colorToRgbaArray(clr) { if (angular.isArray(clr) && clr.length == 3) return clr; if (/^rgb/.test(clr)) { return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) { return i == 3 ? parseFloat(value, 10) : parseInt(value, 10); }); } if (clr.charAt(0) == '#') clr = clr.substring(1); if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return; var dig = clr.length / 3; var red = clr.substr(0, dig); var grn = clr.substr(dig, dig); var blu = clr.substr(dig * 2); if (dig === 1) { red += red; grn += grn; blu += blu; } return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)]; } function rgba(rgbArray, opacity) { if ( !rgbArray ) return "rgb('0,0,0')"; if (rgbArray.length == 4) { rgbArray = angular.copy(rgbArray); opacity ? rgbArray.pop() : opacity = rgbArray.pop(); } return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ? 'rgba(' + rgbArray.join(',') + ',' + opacity + ')' : 'rgb(' + rgbArray.join(',') + ')'; } })(window.angular);