-
Notifications
You must be signed in to change notification settings - Fork 3.4k
feat(browser-color): enable browser header coloring #9192
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
/** | ||
* @ngdoc service | ||
* @name $$mdMeta | ||
* @module material.core.meta | ||
* | ||
* @description | ||
* | ||
* A provider and a service that simplifies meta tags access | ||
* | ||
* Note: This is intended only for use with dynamic meta tags such as browser color and title. | ||
* Tags that are only processed when the page is rendered (such as `charset`, and `http-equiv`) | ||
* will not work since `$$mdMeta` adds the tags after the page has already been loaded. | ||
* | ||
* ```js | ||
* app.config(function($$mdMetaProvider) { | ||
* var removeMeta = $$mdMetaProvider.setMeta('meta-name', 'content'); | ||
* var metaValue = $$mdMetaProvider.getMeta('meta-name'); // -> 'content' | ||
* | ||
* removeMeta(); | ||
* }); | ||
* | ||
* app.controller('myController', function($$mdMeta) { | ||
* var removeMeta = $$mdMeta.setMeta('meta-name', 'content'); | ||
* var metaValue = $$mdMeta.getMeta('meta-name'); // -> 'content' | ||
* | ||
* removeMeta(); | ||
* }); | ||
* ``` | ||
* | ||
* @returns {$$mdMeta.$service} | ||
* | ||
*/ | ||
angular.module('material.core.meta', []) | ||
.provider('$$mdMeta', function () { | ||
var head = angular.element(document.head); | ||
var metaElements = {}; | ||
|
||
/** | ||
* Checks if the requested element was written manually and maps it | ||
* | ||
* @param {string} name meta tag 'name' attribute value | ||
* @returns {boolean} returns true if there is an element with the requested name | ||
*/ | ||
function mapExistingElement(name) { | ||
if (metaElements[name]) { | ||
return true; | ||
} | ||
|
||
var element = document.getElementsByName(name)[0]; | ||
|
||
if (!element) { | ||
return false; | ||
} | ||
|
||
metaElements[name] = angular.element(element); | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* @ngdoc method | ||
* @name $$mdMeta#setMeta | ||
* | ||
* @description | ||
* Creates meta element with the 'name' and 'content' attributes, | ||
* if the meta tag is already created than we replace the 'content' value | ||
* | ||
* @param {string} name meta tag 'name' attribute value | ||
* @param {string} content meta tag 'content' attribute value | ||
* @returns {function} remove function | ||
* | ||
*/ | ||
function setMeta(name, content) { | ||
mapExistingElement(name); | ||
|
||
if (!metaElements[name]) { | ||
var newMeta = angular.element('<meta name="' + name + '" content="' + content + '"/>'); | ||
head.append(newMeta); | ||
metaElements[name] = newMeta; | ||
} | ||
else { | ||
metaElements[name].attr('content', content); | ||
} | ||
|
||
return function () { | ||
metaElements[name].attr('content', ''); | ||
metaElements[name].remove(); | ||
delete metaElements[name]; | ||
}; | ||
} | ||
|
||
/** | ||
* @ngdoc method | ||
* @name $$mdMeta#getMeta | ||
* | ||
* @description | ||
* Gets the 'content' attribute value of the wanted meta element | ||
* | ||
* @param {string} name meta tag 'name' attribute value | ||
* @returns {string} content attribute value | ||
*/ | ||
function getMeta(name) { | ||
if (!mapExistingElement(name)) { | ||
throw Error('$$mdMeta: could not find a meta tag with the name \'' + name + '\''); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see a lot of people take this approach with strings of using single quotes and then escaping them... Is there a reason you don't just use double-quotes to wrap the outer string? Totally just my curiosity at play here 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just hate double quotes 😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JS Style Guide also says to always use single quotes. |
||
} | ||
|
||
return metaElements[name].attr('content'); | ||
} | ||
|
||
var module = { | ||
setMeta: setMeta, | ||
getMeta: getMeta | ||
}; | ||
|
||
return angular.extend({}, module, { | ||
$get: function () { | ||
return module; | ||
} | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
describe('$$mdMeta', function() { | ||
var $$mdMeta; | ||
|
||
beforeEach(module('material.core')); | ||
|
||
beforeEach(function() { | ||
inject(function(_$$mdMeta_) { | ||
$$mdMeta = _$$mdMeta_; | ||
}); | ||
}); | ||
|
||
describe('set meta', function () { | ||
beforeEach(function () { | ||
angular.element(document.getElementsByTagName('meta')).remove(); | ||
}); | ||
|
||
it('should create the element and append it to the body', function() { | ||
var name = 'test'; | ||
var content = 'value'; | ||
|
||
expect(document.getElementsByName(name).length).toEqual(0); | ||
|
||
$$mdMeta.setMeta(name, content); | ||
|
||
expect(angular.element(document.getElementsByName(name)[0]).attr('content')).toBe(content); | ||
}); | ||
|
||
it('should update the existing meta tag', function() { | ||
var name = 'test'; | ||
var content = 'value'; | ||
|
||
$$mdMeta.setMeta(name, content); | ||
|
||
expect(angular.element(document.getElementsByName(name)[0]).attr('content')).toBe(content); | ||
|
||
$$mdMeta.setMeta(name, content + '2'); | ||
|
||
expect(angular.element(document.getElementsByName(name)[0]).attr('content')).toBe(content + '2'); | ||
}); | ||
|
||
it('should map existing meta tag', function() { | ||
var name = 'test'; | ||
var content = 'value'; | ||
|
||
var element = angular.element('<meta name="' + name + '" content="' + content + '"/>'); | ||
angular.element(document.head).append(element); | ||
|
||
expect(angular.element(document.getElementsByName(name)[0]).attr('content')).toBe(content); | ||
|
||
$$mdMeta.setMeta(name, content + '2'); | ||
|
||
expect(angular.element(document.getElementsByName(name)[0]).attr('content')).toBe(content + '2'); | ||
}); | ||
|
||
it('should return a remove function', function() { | ||
var name = 'test'; | ||
var content = 'value'; | ||
|
||
var remove = $$mdMeta.setMeta(name, content); | ||
|
||
expect(document.getElementsByName(name).length).toBe(1); | ||
|
||
remove(); | ||
|
||
expect(document.getElementsByName(name).length).toBe(0); | ||
|
||
}); | ||
}); | ||
|
||
describe('get meta', function () { | ||
beforeEach(function () { | ||
angular.element(document.getElementsByTagName('meta')).remove(); | ||
}); | ||
|
||
it('should return the meta content', function() { | ||
var name = 'test'; | ||
var content = 'value'; | ||
|
||
$$mdMeta.setMeta(name, content); | ||
|
||
expect($$mdMeta.getMeta(name)).toBe(content); | ||
}); | ||
|
||
it('should reject unavailable meta tags', function() { | ||
var name = 'test'; | ||
|
||
expect(function () { | ||
$$mdMeta.getMeta(name); | ||
}).toThrowError('$$mdMeta: could not find a meta tag with the name \'' + name + '\''); | ||
}); | ||
|
||
it('should add not mapped meta tag to the hashmap', function() { | ||
var name = 'test'; | ||
var content = 'value'; | ||
|
||
var element = angular.element('<meta name="' + name + '" content="' + content + '"/>'); | ||
angular.element(document.head).append(element); | ||
|
||
expect($$mdMeta.getMeta(name)).toBe(content); | ||
}); | ||
}); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing new line |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ | |
* @description | ||
* Theming | ||
*/ | ||
angular.module('material.core.theming', ['material.core.theming.palette']) | ||
angular.module('material.core.theming', ['material.core.theming.palette', 'material.core.meta']) | ||
.directive('mdTheme', ThemingDirective) | ||
.directive('mdThemable', ThemableDirective) | ||
.directive('mdThemesDisabled', disableThemesDirective ) | ||
|
@@ -93,6 +93,29 @@ function detectDisabledThemes($mdThemingProvider) { | |
* $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 | ||
* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it might be helpful to add some notes about how the theme, palette and hue affect the final color. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
* 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> | ||
*/ | ||
|
||
/** | ||
|
@@ -120,6 +143,17 @@ function detectDisabledThemes($mdThemingProvider) { | |
* 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 | ||
* ======================================= | ||
* | ||
|
@@ -228,7 +262,7 @@ var themeConfig = { | |
/** | ||
* | ||
*/ | ||
function ThemingProvider($mdColorPalette) { | ||
function ThemingProvider($mdColorPalette, $$mdMetaProvider) { | ||
PALETTES = { }; | ||
var THEMES = { }; | ||
|
||
|
@@ -242,6 +276,53 @@ function ThemingProvider($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, | ||
|
@@ -282,6 +363,8 @@ function ThemingProvider($mdColorPalette) { | |
alwaysWatchTheme = alwaysWatch; | ||
}, | ||
|
||
enableBrowserColor: enableBrowserColor, | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it might be helpful to go ahead and provide/document the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done by adding: setBrowserColor: enableBrowserColor, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct me if I'm wrong, but the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh so you we're suggesting that we would enable them the option of setting what ever color they want? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normally, I wouldn't fight you on this and I'd say "ok, cool" and move on...but... I feel like we're being needlessly restrictive by not providing it. Part of the convenience of this service/provider is hiding all of the logic about which meta tags to set for which browsers. Additionally, I don't really see a downside of exposing it. If they want to use the theme colors, then they should definitely use the What downsides do you see? |
||
$get: ThemingService, | ||
_LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES, | ||
_DARK_DEFAULT_HUES: DARK_DEFAULT_HUES, | ||
|
@@ -464,6 +547,7 @@ function ThemingProvider($mdColorPalette) { | |
applyTheme.registered = registered; | ||
applyTheme.defaultTheme = function() { return defaultTheme; }; | ||
applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, themeConfig.nonce); }; | ||
applyTheme.setBrowserColor = enableBrowserColor; | ||
|
||
return applyTheme; | ||
|
||
|
@@ -769,6 +853,7 @@ function generateAllThemes($injector, $mdTheming) { | |
} | ||
|
||
palette[hueName] = { | ||
hex: palette[hueName], | ||
value: rgbValue, | ||
contrast: getContrastColor() | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might make a note that certain
<meta>
tags (such ascharset
,http-equiv="Content-Type"
, and I thinkname="viewport"
) cannot be added dynamically because the browser will have already decided how to render (and probably rendered) the page by the time they get added.I could not find a complete list of the ones that were editable, but this caught me the other day so it's probably not very obvious to most devs :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done