blob: 230a35420757f4bbd80f6c418edc41f60d30e95f [file] [log] [blame]
/**
@license
Copyright (c) 2017 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
*/
'use strict';
import {nativeShadow, nativeCssVariables, cssBuild} from './style-settings.js';
import {parse, stringify, types, StyleNode} from './css-parse.js'; // eslint-disable-line no-unused-vars
import {MEDIA_MATCH} from './common-regex.js';
import {processUnscopedStyle, isUnscopedStyle} from './unscoped-style-handler.js';
/**
* @param {string|StyleNode} rules
* @param {function(StyleNode)=} callback
* @return {string}
*/
export function toCssText (rules, callback) {
if (!rules) {
return '';
}
if (typeof rules === 'string') {
rules = parse(rules);
}
if (callback) {
forEachRule(rules, callback);
}
return stringify(rules, nativeCssVariables);
}
/**
* @param {HTMLStyleElement} style
* @return {StyleNode}
*/
export function rulesForStyle(style) {
if (!style['__cssRules'] && style.textContent) {
style['__cssRules'] = parse(style.textContent);
}
return style['__cssRules'] || null;
}
// Tests if a rule is a keyframes selector, which looks almost exactly
// like a normal selector but is not (it has nothing to do with scoping
// for example).
/**
* @param {StyleNode} rule
* @return {boolean}
*/
export function isKeyframesSelector(rule) {
return Boolean(rule['parent']) &&
rule['parent']['type'] === types.KEYFRAMES_RULE;
}
/**
* @param {StyleNode} node
* @param {Function=} styleRuleCallback
* @param {Function=} keyframesRuleCallback
* @param {boolean=} onlyActiveRules
*/
export function forEachRule(node, styleRuleCallback, keyframesRuleCallback, onlyActiveRules) {
if (!node) {
return;
}
let skipRules = false;
let type = node['type'];
if (onlyActiveRules) {
if (type === types.MEDIA_RULE) {
let matchMedia = node['selector'].match(MEDIA_MATCH);
if (matchMedia) {
// if rule is a non matching @media rule, skip subrules
if (!window.matchMedia(matchMedia[1]).matches) {
skipRules = true;
}
}
}
}
if (type === types.STYLE_RULE) {
styleRuleCallback(node);
} else if (keyframesRuleCallback &&
type === types.KEYFRAMES_RULE) {
keyframesRuleCallback(node);
} else if (type === types.MIXIN_RULE) {
skipRules = true;
}
let r$ = node['rules'];
if (r$ && !skipRules) {
for (let i=0, l=r$.length, r; (i<l) && (r=r$[i]); i++) {
forEachRule(r, styleRuleCallback, keyframesRuleCallback, onlyActiveRules);
}
}
}
// add a string of cssText to the document.
/**
* @param {string} cssText
* @param {string} moniker
* @param {Node} target
* @param {Node} contextNode
* @return {HTMLStyleElement}
*/
export function applyCss(cssText, moniker, target, contextNode) {
let style = createScopeStyle(cssText, moniker);
applyStyle(style, target, contextNode);
return style;
}
/**
* @param {string} cssText
* @param {string} moniker
* @return {HTMLStyleElement}
*/
export function createScopeStyle(cssText, moniker) {
let style = /** @type {HTMLStyleElement} */(document.createElement('style'));
if (moniker) {
style.setAttribute('scope', moniker);
}
style.textContent = cssText;
return style;
}
/**
* Track the position of the last added style for placing placeholders
* @type {Node}
*/
let lastHeadApplyNode = null;
// insert a comment node as a styling position placeholder.
/**
* @param {string} moniker
* @return {!Comment}
*/
export function applyStylePlaceHolder(moniker) {
let placeHolder = document.createComment(' Shady DOM styles for ' +
moniker + ' ');
let after = lastHeadApplyNode ?
lastHeadApplyNode['nextSibling'] : null;
let scope = document.head;
scope.insertBefore(placeHolder, after || scope.firstChild);
lastHeadApplyNode = placeHolder;
return placeHolder;
}
/**
* @param {HTMLStyleElement} style
* @param {?Node} target
* @param {?Node} contextNode
*/
export function applyStyle(style, target, contextNode) {
target = target || document.head;
let after = (contextNode && contextNode.nextSibling) ||
target.firstChild;
target.insertBefore(style, after);
if (!lastHeadApplyNode) {
lastHeadApplyNode = style;
} else {
// only update lastHeadApplyNode if the new style is inserted after the old lastHeadApplyNode
let position = style.compareDocumentPosition(lastHeadApplyNode);
if (position === Node.DOCUMENT_POSITION_PRECEDING) {
lastHeadApplyNode = style;
}
}
}
/**
* @param {string} buildType
* @return {boolean}
*/
export function isTargetedBuild(buildType) {
return nativeShadow ? buildType === 'shadow' : buildType === 'shady';
}
/**
* Walk from text[start] matching parens and
* returns position of the outer end paren
* @param {string} text
* @param {number} start
* @return {number}
*/
export function findMatchingParen(text, start) {
let level = 0;
for (let i=start, l=text.length; i < l; i++) {
if (text[i] === '(') {
level++;
} else if (text[i] === ')') {
if (--level === 0) {
return i;
}
}
}
return -1;
}
/**
* @param {string} str
* @param {function(string, string, string, string)} callback
*/
export function processVariableAndFallback(str, callback) {
// find 'var('
let start = str.indexOf('var(');
if (start === -1) {
// no var?, everything is prefix
return callback(str, '', '', '');
}
//${prefix}var(${inner})${suffix}
let end = findMatchingParen(str, start + 3);
let inner = str.substring(start + 4, end);
let prefix = str.substring(0, start);
// suffix may have other variables
let suffix = processVariableAndFallback(str.substring(end + 1), callback);
let comma = inner.indexOf(',');
// value and fallback args should be trimmed to match in property lookup
if (comma === -1) {
// variable, no fallback
return callback(prefix, inner.trim(), '', suffix);
}
// var(${value},${fallback})
let value = inner.substring(0, comma).trim();
let fallback = inner.substring(comma + 1).trim();
return callback(prefix, value, fallback, suffix);
}
/**
* @param {Element} element
* @param {string} value
*/
export function setElementClassRaw(element, value) {
// use native setAttribute provided by ShadyDOM when setAttribute is patched
if (nativeShadow) {
element.setAttribute('class', value);
} else {
window['ShadyDOM']['nativeMethods']['setAttribute'].call(element, 'class', value);
}
}
export const wrap = window['ShadyDOM'] && window['ShadyDOM']['wrap'] || ((node) => node);
/**
* @param {Element | {is: string, extends: string}} element
* @return {{is: string, typeExtension: string}}
*/
export function getIsExtends(element) {
let localName = element['localName'];
let is = '', typeExtension = '';
/*
NOTE: technically, this can be wrong for certain svg elements
with `-` in the name like `<font-face>`
*/
if (localName) {
if (localName.indexOf('-') > -1) {
is = localName;
} else {
typeExtension = localName;
is = (element.getAttribute && element.getAttribute('is')) || '';
}
} else {
is = /** @type {?} */(element).is;
typeExtension = /** @type {?} */(element).extends;
}
return {is, typeExtension};
}
/**
* @param {Element|DocumentFragment} element
* @return {string}
*/
export function gatherStyleText(element) {
/** @type {!Array<string>} */
const styleTextParts = [];
const styles = /** @type {!NodeList<!HTMLStyleElement>} */(element.querySelectorAll('style'));
for (let i = 0; i < styles.length; i++) {
const style = styles[i];
if (isUnscopedStyle(style)) {
if (!nativeShadow) {
processUnscopedStyle(style);
style.parentNode.removeChild(style);
}
} else {
styleTextParts.push(style.textContent);
style.parentNode.removeChild(style);
}
}
return styleTextParts.join('').trim();
}
/**
* Split a selector separated by commas into an array in a smart way
* @param {string} selector
* @return {!Array<string>}
*/
export function splitSelectorList(selector) {
const parts = [];
let part = '';
for (let i = 0; i >= 0 && i < selector.length; i++) {
// A selector with parentheses will be one complete part
if (selector[i] === '(') {
// find the matching paren
const end = findMatchingParen(selector, i);
// push the paren block into the part
part += selector.slice(i, end + 1);
// move the index to after the paren block
i = end;
} else if (selector[i] === ',') {
parts.push(part);
part = '';
} else {
part += selector[i];
}
}
// catch any pieces after the last comma
if (part) {
parts.push(part);
}
return parts;
}
const CSS_BUILD_ATTR = 'css-build';
/**
* Return the polymer-css-build "build type" applied to this element
*
* @param {!HTMLElement} element
* @return {string} Can be "", "shady", or "shadow"
*/
export function getCssBuild(element) {
if (cssBuild !== undefined) {
return /** @type {string} */(cssBuild);
}
if (element.__cssBuild === undefined) {
// try attribute first, as it is the common case
const attrValue = element.getAttribute(CSS_BUILD_ATTR);
if (attrValue) {
element.__cssBuild = attrValue;
} else {
const buildComment = getBuildComment(element);
if (buildComment !== '') {
// remove build comment so it is not needlessly copied into every element instance
removeBuildComment(element);
}
element.__cssBuild = buildComment;
}
}
return element.__cssBuild || '';
}
/**
* Check if the given element, either a <template> or <style>, has been processed
* by polymer-css-build.
*
* If so, then we can make a number of optimizations:
* - polymer-css-build will decompose mixins into individual CSS Custom Properties,
* so the ApplyShim can be skipped entirely.
* - Under native ShadowDOM, the style text can just be copied into each instance
* without modification
* - If the build is "shady" and ShadyDOM is in use, the styling does not need
* scoping beyond the shimming of CSS Custom Properties
*
* @param {!HTMLElement} element
* @return {boolean}
*/
export function elementHasBuiltCss(element) {
return getCssBuild(element) !== '';
}
/**
* For templates made with tagged template literals, polymer-css-build will
* insert a comment of the form `<!--css-build:shadow-->`
*
* @param {!HTMLElement} element
* @return {string}
*/
export function getBuildComment(element) {
const buildComment = element.localName === 'template' ?
/** @type {!HTMLTemplateElement} */ (element).content.firstChild :
element.firstChild;
if (buildComment instanceof Comment) {
const commentParts = buildComment.textContent.trim().split(':');
if (commentParts[0] === CSS_BUILD_ATTR) {
return commentParts[1];
}
}
return '';
}
/**
* Check if the css build status is optimal, and do no unneeded work.
*
* @param {string=} cssBuild CSS build status
* @return {boolean} css build is optimal or not
*/
export function isOptimalCssBuild(cssBuild = '') {
// CSS custom property shim always requires work
if (cssBuild === '' || !nativeCssVariables) {
return false;
}
return nativeShadow ? cssBuild === 'shadow' : cssBuild === 'shady';
}
/**
* @param {!HTMLElement} element
*/
function removeBuildComment(element) {
const buildComment = element.localName === 'template' ?
/** @type {!HTMLTemplateElement} */ (element).content.firstChild :
element.firstChild;
buildComment.parentNode.removeChild(buildComment);
}