/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/*
This is a limited shim for ShadowDOM css styling.
https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#styles
The intention here is to support only the styling features which can be
relatively simply implemented. The goal is to allow users to avoid the
most obvious pitfalls and do so without compromising performance significantly.
For ShadowDOM styling that's not covered here, a set of best practices
can be provided that should allow users to accomplish more complex styling.
The following is a list of specific ShadowDOM styling features and a brief
discussion of the approach used to shim.
Shimmed features:
* :host, :host-context: ShadowDOM allows styling of the shadowRoot's host
element using the :host rule. To shim this feature, the :host styles are
reformatted and prefixed with a given scope name and promoted to a
document level stylesheet.
For example, given a scope name of .foo, a rule like this:
:host {
background: red;
}
}
becomes:
.foo {
background: red;
}
* encapsultion: Styles defined within ShadowDOM, apply only to
dom inside the ShadowDOM. Polymer uses one of two techniques to imlement
this feature.
By default, rules are prefixed with the host element tag name
as a descendant selector. This ensures styling does not leak out of the 'top'
of the element's ShadowDOM. For example,
div {
font-weight: bold;
}
becomes:
x-foo div {
font-weight: bold;
}
becomes:
Alternatively, if WebComponents.ShadowCSS.strictStyling is set to true then
selectors are scoped by adding an attribute selector suffix to each
simple selector that contains the host element tag name. Each element
in the element's ShadowDOM template is also given the scope attribute.
Thus, these rules match only elements that have the scope attribute.
For example, given a scope name of x-foo, a rule like this:
div {
font-weight: bold;
}
becomes:
div[x-foo] {
font-weight: bold;
}
Note that elements that are dynamically added to a scope must have the scope
selector added to them manually.
* upper/lower bound encapsulation: Styles which are defined outside a
shadowRoot should not cross the ShadowDOM boundary and should not apply
inside a shadowRoot.
This styling behavior is not emulated. Some possible ways to do this that
were rejected due to complexity and/or performance concerns include: (1) reset
every possible property for every possible selector for a given scope name;
(2) re-implement css in javascript.
As an alternative, users should make sure to use selectors
specific to the scope in which they are working.
* ::distributed: This behavior is not emulated. It's often not necessary
to style the contents of a specific insertion point and instead, descendants
of the host element can be styled selectively. Users can also create an
extra node around an insertion point and style that node's contents
via descendent selectors. For example, with a shadowRoot like this:
could become:
Note the use of @polyfill in the comment above a ShadowDOM specific style
declaration. This is a directive to the styling shim to use the selector
in comments in lieu of the next selector when running under polyfill.
*/
(function(scope) {
var ShadowCSS = {
strictStyling: false,
registry: {},
// Shim styles for a given root associated with a name and extendsName
// 1. cache root styles by name
// 2. optionally tag root nodes with scope name
// 3. shim polyfill directives /* @polyfill */ and /* @polyfill-rule */
// 4. shim :host and scoping
shimStyling: function(root, name, extendsName) {
var scopeStyles = this.prepareRoot(root, name, extendsName);
var typeExtension = this.isTypeExtension(extendsName);
var scopeSelector = this.makeScopeSelector(name, typeExtension);
// use caching to make working with styles nodes easier and to facilitate
// lookup of extendee
var cssText = stylesToCssText(scopeStyles, true);
cssText = this.scopeCssText(cssText, scopeSelector);
// cache shimmed css on root for user extensibility
if (root) {
root.shimmedStyle = cssText;
}
// add style to document
this.addCssToDocument(cssText, name);
},
/*
* Shim a style element with the given selector. Returns cssText that can
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
*/
shimStyle: function(style, selector) {
return this.shimCssText(style.textContent, selector);
},
/*
* Shim some cssText with the given selector. Returns cssText that can
* be included in the document via WebComponents.ShadowCSS.addCssToDocument(css).
*/
shimCssText: function(cssText, selector) {
cssText = this.insertDirectives(cssText);
return this.scopeCssText(cssText, selector);
},
makeScopeSelector: function(name, typeExtension) {
if (name) {
return typeExtension ? '[is=' + name + ']' : name;
}
return '';
},
isTypeExtension: function(extendsName) {
return extendsName && extendsName.indexOf('-') < 0;
},
prepareRoot: function(root, name, extendsName) {
var def = this.registerRoot(root, name, extendsName);
this.replaceTextInStyles(def.rootStyles, this.insertDirectives);
// remove existing style elements
this.removeStyles(root, def.rootStyles);
// apply strict attr
if (this.strictStyling) {
this.applyScopeToContent(root, name);
}
return def.scopeStyles;
},
removeStyles: function(root, styles) {
for (var i=0, l=styles.length, s; (i .bar { }
*
* to
*
* scopeName.foo > .bar
*/
convertColonHost: function(cssText) {
return this.convertColonRule(cssText, cssColonHostRe,
this.colonHostPartReplacer);
},
/*
* convert a rule like :host-context(.foo) > .bar { }
*
* to
*
* scopeName.foo > .bar, .foo scopeName > .bar { }
*
* and
*
* :host-context(.foo:host) .bar { ... }
*
* to
*
* scopeName.foo .bar { ... }
*/
convertColonHostContext: function(cssText) {
return this.convertColonRule(cssText, cssColonHostContextRe,
this.colonHostContextPartReplacer);
},
convertColonRule: function(cssText, regExp, partReplacer) {
// p1 = :host, p2 = contents of (), p3 rest of rule
return cssText.replace(regExp, function(m, p1, p2, p3) {
p1 = polyfillHostNoCombinator;
if (p2) {
var parts = p2.split(','), r = [];
for (var i=0, l=parts.length, p; (i .zot becomes .foo[name].bar[name] > .zot[name]
applyStrictSelectorScope: function(selector, scopeSelector) {
scopeSelector = scopeSelector.replace(/\[is=([^\]]*)\]/g, '$1');
var splits = [' ', '>', '+', '~'],
scoped = selector,
attrName = '[' + scopeSelector + ']';
splits.forEach(function(sep) {
var parts = scoped.split(sep);
scoped = parts.map(function(p) {
// remove :host since it should be unnecessary
var t = p.trim().replace(polyfillHostRe, '');
if (t && (splits.indexOf(t) < 0) && (t.indexOf(attrName) < 0)) {
p = t.replace(/([^:]*)(:*)(.*)/, '$1' + attrName + '$2$3');
}
return p;
}).join(sep);
});
return scoped;
},
insertPolyfillHostInCssText: function(selector) {
return selector.replace(colonHostContextRe, polyfillHostContext).replace(
colonHostRe, polyfillHost);
},
propertiesFromRule: function(rule) {
var cssText = rule.style.cssText;
// TODO(sorvell): Safari cssom incorrectly removes quotes from the content
// property. (https://bugs.webkit.org/show_bug.cgi?id=118045)
// don't replace attr rules
if (rule.style.content && !rule.style.content.match(/['"]+|attr/)) {
cssText = cssText.replace(/content:[^;]*;/g, 'content: \'' +
rule.style.content + '\';');
}
// TODO(sorvell): we can workaround this issue here, but we need a list
// of troublesome properties to fix https://github.com/Polymer/platform/issues/53
//
// inherit rules can be omitted from cssText
// TODO(sorvell): remove when Blink bug is fixed:
// https://code.google.com/p/chromium/issues/detail?id=358273
var style = rule.style;
for (var i in style) {
if (style[i] === 'initial') {
cssText += i + ': initial; ';
}
}
return cssText;
},
replaceTextInStyles: function(styles, action) {
if (styles && action) {
if (!(styles instanceof Array)) {
styles = [styles];
}
Array.prototype.forEach.call(styles, function(s) {
s.textContent = action.call(this, s.textContent);
}, this);
}
},
addCssToDocument: function(cssText, name) {
if (cssText.match('@import')) {
addOwnSheet(cssText, name);
} else {
addCssToDocument(cssText);
}
}
};
var selectorRe = /([^{]*)({[\s\S]*?})/gim,
cssCommentRe = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim,
// TODO(sorvell): remove either content or comment
cssCommentNextSelectorRe = /\/\*\s*@polyfill ([^*]*\*+([^/*][^*]*\*+)*\/)([^{]*?){/gim,
cssContentNextSelectorRe = /polyfill-next-selector[^}]*content\:[\s]*?['"](.*?)['"][;\s]*}([^{]*?){/gim,
// TODO(sorvell): remove either content or comment
cssCommentRuleRe = /\/\*\s@polyfill-rule([^*]*\*+([^/*][^*]*\*+)*)\//gim,
cssContentRuleRe = /(polyfill-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim,
// TODO(sorvell): remove either content or comment
cssCommentUnscopedRuleRe = /\/\*\s@polyfill-unscoped-rule([^*]*\*+([^/*][^*]*\*+)*)\//gim,
cssContentUnscopedRuleRe = /(polyfill-unscoped-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim,
cssPseudoRe = /::(x-[^\s{,(]*)/gim,
cssPartRe = /::part\(([^)]*)\)/gim,
// note: :host pre-processed to -shadowcsshost.
polyfillHost = '-shadowcsshost',
// note: :host-context pre-processed to -shadowcsshostcontext.
polyfillHostContext = '-shadowcsscontext',
parenSuffix = ')(?:\\((' +
'(?:\\([^)(]*\\)|[^)(]*)+?' +
')\\))?([^,{]*)';
var cssColonHostRe = new RegExp('(' + polyfillHost + parenSuffix, 'gim'),
cssColonHostContextRe = new RegExp('(' + polyfillHostContext + parenSuffix, 'gim'),
selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$',
colonHostRe = /\:host/gim,
colonHostContextRe = /\:host-context/gim,
/* host name without combinator */
polyfillHostNoCombinator = polyfillHost + '-no-combinator',
polyfillHostRe = new RegExp(polyfillHost, 'gim'),
polyfillHostContextRe = new RegExp(polyfillHostContext, 'gim'),
shadowDOMSelectorsRe = [
/>>>/g,
/::shadow/g,
/::content/g,
// Deprecated selectors
/\/deep\//g, // former >>>
/\/shadow\//g, // former ::shadow
/\/shadow-deep\//g, // former /deep/
/\^\^/g, // former /shadow/
/\^(?!=)/g // former /shadow-deep/
];
function stylesToCssText(styles, preserveComments) {
var cssText = '';
Array.prototype.forEach.call(styles, function(s) {
cssText += s.textContent + '\n\n';
});
// strip comments for easier processing
if (!preserveComments) {
cssText = cssText.replace(cssCommentRe, '');
}
return cssText;
}
function cssTextToStyle(cssText) {
var style = document.createElement('style');
style.textContent = cssText;
return style;
}
function cssToRules(cssText) {
var style = cssTextToStyle(cssText);
document.head.appendChild(style);
var rules = [];
if (style.sheet) {
// TODO(sorvell): Firefox throws when accessing the rules of a stylesheet
// with an @import
// https://bugzilla.mozilla.org/show_bug.cgi?id=625013
try {
rules = style.sheet.cssRules;
} catch(e) {
//
}
} else {
console.warn('sheet not found', style);
}
style.parentNode.removeChild(style);
return rules;
}
var frame = document.createElement('iframe');
frame.style.display = 'none';
function initFrame() {
frame.initialized = true;
document.body.appendChild(frame);
var doc = frame.contentDocument;
var base = doc.createElement('base');
base.href = document.baseURI;
doc.head.appendChild(base);
}
function inFrame(fn) {
if (!frame.initialized) {
initFrame();
}
document.body.appendChild(frame);
fn(frame.contentDocument);
document.body.removeChild(frame);
}
// TODO(sorvell): use an iframe if the cssText contains an @import to workaround
// https://code.google.com/p/chromium/issues/detail?id=345114
var isChrome = navigator.userAgent.match('Chrome');
function withCssRules(cssText, callback) {
if (!callback) {
return;
}
var rules;
if (cssText.match('@import') && isChrome) {
var style = cssTextToStyle(cssText);
inFrame(function(doc) {
doc.head.appendChild(style.impl);
rules = Array.prototype.slice.call(style.sheet.cssRules, 0);
callback(rules);
});
} else {
rules = cssToRules(cssText);
callback(rules);
}
}
function rulesToCss(cssRules) {
for (var i=0, css=[]; i < cssRules.length; i++) {
css.push(cssRules[i].cssText);
}
return css.join('\n\n');
}
function addCssToDocument(cssText) {
if (cssText) {
getSheet().appendChild(document.createTextNode(cssText));
}
}
function addOwnSheet(cssText, name) {
var style = cssTextToStyle(cssText);
style.setAttribute(name, '');
style.setAttribute(SHIMMED_ATTRIBUTE, '');
document.head.appendChild(style);
}
var SHIM_ATTRIBUTE = 'shim-shadowdom';
var SHIMMED_ATTRIBUTE = 'shim-shadowdom-css';
var NO_SHIM_ATTRIBUTE = 'no-shim';
var sheet;
function getSheet() {
if (!sheet) {
sheet = document.createElement("style");
sheet.setAttribute(SHIMMED_ATTRIBUTE, '');
sheet[SHIMMED_ATTRIBUTE] = true;
}
return sheet;
}
// add polyfill stylesheet to document
if (window.ShadowDOMPolyfill) {
addCssToDocument('style { display: none !important; }\n');
var doc = ShadowDOMPolyfill.wrap(document);
var head = doc.querySelector('head');
head.insertBefore(getSheet(), head.childNodes[0]);
// TODO(sorvell): monkey-patching HTMLImports is abusive;
// consider a better solution.
document.addEventListener('DOMContentLoaded', function() {
var urlResolver = scope.urlResolver;
if (window.HTMLImports && !HTMLImports.useNative) {
var SHIM_SHEET_SELECTOR = 'link[rel=stylesheet]' +
'[' + SHIM_ATTRIBUTE + ']';
var SHIM_STYLE_SELECTOR = 'style[' + SHIM_ATTRIBUTE + ']';
HTMLImports.importer.documentPreloadSelectors += ',' + SHIM_SHEET_SELECTOR;
HTMLImports.importer.importsPreloadSelectors += ',' + SHIM_SHEET_SELECTOR;
HTMLImports.parser.documentSelectors = [
HTMLImports.parser.documentSelectors,
SHIM_SHEET_SELECTOR,
SHIM_STYLE_SELECTOR
].join(',');
var originalParseGeneric = HTMLImports.parser.parseGeneric;
HTMLImports.parser.parseGeneric = function(elt) {
if (elt[SHIMMED_ATTRIBUTE]) {
return;
}
var style = elt.__importElement || elt;
if (!style.hasAttribute(SHIM_ATTRIBUTE)) {
originalParseGeneric.call(this, elt);
return;
}
if (elt.__resource) {
style = elt.ownerDocument.createElement('style');
style.textContent = elt.__resource;
}
// relay on HTMLImports for path fixup
HTMLImports.path.resolveUrlsInStyle(style, elt.href);
style.textContent = ShadowCSS.shimStyle(style);
style.removeAttribute(SHIM_ATTRIBUTE, '');
style.setAttribute(SHIMMED_ATTRIBUTE, '');
style[SHIMMED_ATTRIBUTE] = true;
// place in document
if (style.parentNode !== head) {
// replace links in head
if (elt.parentNode === head) {
head.replaceChild(style, elt);
} else {
this.addElementToDocument(style);
}
}
style.__importParsed = true;
this.markParsingComplete(elt);
this.parseNext();
}
var hasResource = HTMLImports.parser.hasResource;
HTMLImports.parser.hasResource = function(node) {
if (node.localName === 'link' && node.rel === 'stylesheet' &&
node.hasAttribute(SHIM_ATTRIBUTE)) {
return (node.__resource);
} else {
return hasResource.call(this, node);
}
}
}
});
}
// exports
scope.ShadowCSS = ShadowCSS;
})(window.WebComponents);