| /** |
| @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); |
| } |