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

feat(browser-color): enable browser header coloring #9192

Merged
merged 1 commit into from
Aug 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/app/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ function(SERVICES, COMPONENTS, DEMOS, PAGES,
.primaryPalette('docs-blue')
.accentPalette('docs-red');

$mdThemingProvider
.enableBrowserColor();

angular.forEach(PAGES, function(pages, area) {
angular.forEach(pages, function(page) {
$routeProvider
Expand Down
120 changes: 120 additions & 0 deletions src/core/services/meta/meta.js
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
Copy link
Contributor

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 as charset, http-equiv="Content-Type", and I think name="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 :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

*
* 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 + '\'');
Copy link
Contributor

Choose a reason for hiding this comment

The 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 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just hate double quotes 😅

Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
});
});
102 changes: 102 additions & 0 deletions src/core/services/meta/meta.spec.js
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);
});
});
});
Copy link
Contributor

@ErinCoughlan ErinCoughlan Aug 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing new line

89 changes: 87 additions & 2 deletions src/core/services/theming/theming.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand Down Expand Up @@ -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
*
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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>
*/

/**
Expand Down Expand Up @@ -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
* =======================================
*
Expand Down Expand Up @@ -228,7 +262,7 @@ var themeConfig = {
/**
*
*/
function ThemingProvider($mdColorPalette) {
function ThemingProvider($mdColorPalette, $$mdMetaProvider) {
PALETTES = { };
var THEMES = { };

Expand All @@ -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,
Expand Down Expand Up @@ -282,6 +363,8 @@ function ThemingProvider($mdColorPalette) {
alwaysWatchTheme = alwaysWatch;
},

enableBrowserColor: enableBrowserColor,

Copy link
Contributor

Choose a reason for hiding this comment

The 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 setBrowserColor() method here too. I can see some people just wanting to set it to white/black and it being cumbersome to figure out the proper theme/palette/hue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done by adding:

 setBrowserColor: enableBrowserColor,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but the setBrowserColor() function takes an options object, not a single color, right? So you would still have to pass an options param of theme, palette and hue? That's kinda opposite of what I was suggesting.

Copy link
Member Author

Choose a reason for hiding this comment

The 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?
I'm against it, in that case i'd say to them, use it manually.

Copy link
Contributor

@topherfangio topherfangio Aug 2, 2016

Choose a reason for hiding this comment

The 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 enableBrowserColor() approach, but if they want to set it to something that isn't part of the theme (or maybe any theme), then giving them a way to set it directly would be nice.

What downsides do you see?

$get: ThemingService,
_LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES,
_DARK_DEFAULT_HUES: DARK_DEFAULT_HUES,
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -769,6 +853,7 @@ function generateAllThemes($injector, $mdTheming) {
}

palette[hueName] = {
hex: palette[hueName],
value: rgbValue,
contrast: getContrastColor()
};
Expand Down
Loading