Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit ced4e0c

Browse files
EladBezalelThomasBurleson
authored andcommitted
feat(colors): directive and service to use any color from any palette on any element
Provide directive and service that allows custom components to be easily colored using Material Theme colors. - directive and documentation - service and documentation - tests - smart watch option, when the directive recognize at least one of it's values needs interpolation it starts to watch it - mdColorsWatch - when set to false the directive doesn't watch for changes - when using `background` property the directive will seamlessly set the foreground color according the palette hue - $mdColorUtil - aggregate color related utils - fixed theming scoping issue - THEME was defined globally which made it leak and changed for other modules Fixes #1269. Closes #7791
1 parent e7f866d commit ced4e0c

File tree

15 files changed

+926
-51
lines changed

15 files changed

+926
-51
lines changed

src/components/colors/colors.js

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
(function () {
2+
"use strict";
3+
4+
/**
5+
* Use a RegExp to check if the `md-colors="<expression>"` is static string
6+
* or one that should be observed and dynamically interpolated.
7+
*/
8+
var STATIC_COLOR_EXPRESSION = /^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?('|")[a-zA-Z0-9-.]*('|"))+\s*}$/;
9+
var COLOR_PALETTES = undefined;
10+
11+
/**
12+
* @ngdoc module
13+
* @name material.components.colors
14+
*
15+
* @description
16+
* Define $mdColors service and a `md-colors=""` attribute directive
17+
*/
18+
angular
19+
.module('material.components.colors', ['material.core'])
20+
.directive('mdColors', MdColorsDirective)
21+
.service('$mdColors', MdColorsService);
22+
23+
/**
24+
* @ngdoc service
25+
* @name $mdColors
26+
* @module material.core.theming.colors
27+
*
28+
* @description
29+
* With only defining themes, one couldn't get non ngMaterial elements colored with Material colors,
30+
* `$mdColors` service is used by the md-color directive to convert the 1..n color expressions to RGBA values and will apply
31+
* those values to element as CSS property values.
32+
*
33+
* @usage
34+
* <hljs lang="html">
35+
* <div md-colors="{background: 'myTheme-accent-900-0.43'}">
36+
* <div md-colors="{color: 'red-A100', border-color: 'primary-600'}">
37+
* <span>Color demo</span>
38+
* </div>
39+
* </div>
40+
* </hljs>
41+
*
42+
*/
43+
function MdColorsService($mdTheming, $mdColorPalette, $mdUtil, $parse) {
44+
COLOR_PALETTES = COLOR_PALETTES || Object.keys($mdColorPalette);
45+
46+
// Publish service instance
47+
return {
48+
applyThemeColors: applyThemeColors,
49+
getThemeColor: getThemeColor
50+
};
51+
52+
// ********************************************
53+
// Internal Methods
54+
// ********************************************
55+
56+
/**
57+
* Convert the color expression into an object with scope-interpolated values
58+
* Then calculate the rgba() values based on the theme color parts
59+
*/
60+
function applyThemeColors(element, scope, colorExpression) {
61+
// Json.parse() does not work because the keys are not quoted;
62+
// use $parse to convert to a hash map
63+
var themeColors = $parse(colorExpression)(scope);
64+
65+
// Assign the calculate RGBA color values directly as inline CSS
66+
element.css(interpolateColors(themeColors));
67+
}
68+
69+
/**
70+
* Public api to get parsed color from expression
71+
*
72+
*/
73+
function getThemeColor(expression) {
74+
var color = extractColorOptions(expression);
75+
76+
return parseColor(color);
77+
}
78+
79+
/**
80+
* Return the parsed color
81+
* @param color hashmap of color definitions
82+
* @param contrast whether use contrast color for foreground
83+
* @returns rgba color string
84+
*/
85+
function parseColor(color, contrast) {
86+
contrast = contrast || false;
87+
var rgbValues = $mdColorPalette[color.palette][color.hue];
88+
89+
rgbValues = contrast ? rgbValues.contrast : rgbValues.value;
90+
91+
return $mdUtil.supplant('rgba( {0}, {1}, {2}, {3} )',
92+
[rgbValues[0], rgbValues[1], rgbValues[2], rgbValues[3] || color.opacity]
93+
);
94+
}
95+
96+
/**
97+
* Convert the color expression into an object with scope-interpolated values
98+
* Then calculate the rgba() values based on the theme color parts
99+
*
100+
* @results Hashmap of CSS properties with associated `rgba( )` string vales
101+
*
102+
*
103+
*/
104+
function interpolateColors(themeColors) {
105+
var rgbColors = {};
106+
107+
var hasColorProperty = themeColors.hasOwnProperty('color');
108+
109+
angular.forEach(themeColors, function (value, key) {
110+
var color = extractColorOptions(value);
111+
112+
rgbColors[key] = parseColor(color);
113+
114+
if (key === 'background' && !hasColorProperty) {
115+
rgbColors['color'] = parseColor(color, true);
116+
}
117+
});
118+
119+
return rgbColors;
120+
}
121+
122+
/**
123+
* For the evaluated expression, extract the color parts into a hash map
124+
*/
125+
function extractColorOptions(expression) {
126+
var parts = expression.split('-'),
127+
hasTheme = angular.isDefined($mdTheming.THEMES[parts[0]]),
128+
theme = hasTheme ? parts.splice(0, 1)[0] : 'default';
129+
130+
var defaultHue = parts[0] !== 'accent' ? 500 : 'A200';
131+
132+
return {
133+
theme: theme,
134+
palette: extractPalette(parts, theme),
135+
hue: parts[1] || defaultHue,
136+
opacity: parts[2] || 1
137+
};
138+
}
139+
140+
/**
141+
* Calculate the theme palette name
142+
*/
143+
function extractPalette(parts, theme) {
144+
// If the next section is one of the palettes we assume it's a two word palette
145+
// Two word palette can be also written in camelCase, forming camelCase to dash-case
146+
147+
var isTwoWord = parts.length > 1 && COLOR_PALETTES.indexOf(parts[1]) !== -1;
148+
var palette = parts[0].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
149+
150+
if (isTwoWord) palette = parts[0] + '-' + parts.splice(1, 1);
151+
152+
if (COLOR_PALETTES.indexOf(palette) === -1) {
153+
// If the palette is not in the palette list it's one of primary/accent/warn/background
154+
var scheme = $mdTheming.THEMES[theme].colors[palette];
155+
if (!scheme) {
156+
throw new Error($mdUtil.supplant('mdColors: couldn\'t find \'{palette}\' in the palettes.', {palette: palette}));
157+
}
158+
palette = scheme.name;
159+
}
160+
161+
return palette;
162+
}
163+
}
164+
165+
/**
166+
* @ngdoc directive
167+
* @name mdColors
168+
* @module material.components.colors
169+
*
170+
* @restrict A
171+
*
172+
* @description
173+
* `mdColors` directive will apply the theme-based color expression as RGBA CSS style values.
174+
*
175+
* The format will be similar to our color defining in the scss files:
176+
*
177+
* ## `[?theme]-[palette]-[?hue]-[?opacity]`
178+
* - [theme] - default value is the `default` theme
179+
* - [palette] - can be either palette name or primary/accent/warn/background
180+
* - [hue] - default is 500
181+
* - [opacity] - default is 1
182+
*
183+
* > `?` indicates optional parameter
184+
*
185+
* @usage
186+
* <hljs lang="html">
187+
* <div md-colors="{background: 'myTheme-accent-900-0.43'}">
188+
* <div md-colors="{color: 'red-A100', border-color: 'primary-600'}">
189+
* <span>Color demo</span>
190+
* </div>
191+
* </div>
192+
* </hljs>
193+
*
194+
* `mdColors` directive will automatically watch for changes in the expression if it recognizes an interpolation
195+
* expression or a function. For performance options, you can use `::` prefix to the `md-colors` expression
196+
* to indicate a one-time data binding.
197+
* <hljs lang="html">
198+
* <md-card md-colors="::{background: '{{theme}}-primary-700'}">
199+
* </md-card>
200+
* </hljs>
201+
*
202+
*/
203+
function MdColorsDirective($mdColors, $mdUtil, $log) {
204+
return {
205+
restrict: 'A',
206+
compile: function (tElem, tAttrs) {
207+
var shouldWatch = shouldColorsWatch();
208+
209+
return function (scope, element, attrs) {
210+
var colorExpression = function () {
211+
return attrs.mdColors;
212+
};
213+
214+
try {
215+
if (shouldWatch) {
216+
scope.$watch(colorExpression, angular.bind(this,
217+
$mdColors.applyThemeColors, element, scope
218+
));
219+
}
220+
else {
221+
$mdColors.applyThemeColors(element, scope, colorExpression());
222+
}
223+
224+
}
225+
catch (e) {
226+
$log.error(e.message);
227+
}
228+
229+
};
230+
231+
function shouldColorsWatch() {
232+
// Simulate 1x binding and mark mdColorsWatch == false
233+
var rawColorExpression = tAttrs.mdColors;
234+
var bindOnce = rawColorExpression.indexOf('::') > -1;
235+
var isStatic = bindOnce ? true : STATIC_COLOR_EXPRESSION.test(tAttrs.mdColors);
236+
237+
// Remove it for the postLink...
238+
tAttrs.mdColors = rawColorExpression.replace('::','');
239+
240+
var hasWatchAttr = angular.isDefined(tAttrs.mdColorsWatch);
241+
242+
return (bindOnce || isStatic) ? false :
243+
hasWatchAttr ? $mdUtil.parseAttributeBoolean(tAttrs.mdColorsWatch) : true;
244+
}
245+
}
246+
};
247+
248+
}
249+
250+
251+
})();

0 commit comments

Comments
 (0)