1/** 2@license 3Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7Code distributed by Google as part of the polymer project is also 8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9*/ 10 11'use strict'; 12 13import {nativeShadow, nativeCssVariables, cssBuild} from './style-settings.js'; 14import {parse, stringify, types, StyleNode} from './css-parse.js'; // eslint-disable-line no-unused-vars 15import {MEDIA_MATCH} from './common-regex.js'; 16import {processUnscopedStyle, isUnscopedStyle} from './unscoped-style-handler.js'; 17 18/** 19 * @param {string|StyleNode} rules 20 * @param {function(StyleNode)=} callback 21 * @return {string} 22 */ 23export function toCssText (rules, callback) { 24 if (!rules) { 25 return ''; 26 } 27 if (typeof rules === 'string') { 28 rules = parse(rules); 29 } 30 if (callback) { 31 forEachRule(rules, callback); 32 } 33 return stringify(rules, nativeCssVariables); 34} 35 36/** 37 * @param {HTMLStyleElement} style 38 * @return {StyleNode} 39 */ 40export function rulesForStyle(style) { 41 if (!style['__cssRules'] && style.textContent) { 42 style['__cssRules'] = parse(style.textContent); 43 } 44 return style['__cssRules'] || null; 45} 46 47// Tests if a rule is a keyframes selector, which looks almost exactly 48// like a normal selector but is not (it has nothing to do with scoping 49// for example). 50/** 51 * @param {StyleNode} rule 52 * @return {boolean} 53 */ 54export function isKeyframesSelector(rule) { 55 return Boolean(rule['parent']) && 56 rule['parent']['type'] === types.KEYFRAMES_RULE; 57} 58 59/** 60 * @param {StyleNode} node 61 * @param {Function=} styleRuleCallback 62 * @param {Function=} keyframesRuleCallback 63 * @param {boolean=} onlyActiveRules 64 */ 65export function forEachRule(node, styleRuleCallback, keyframesRuleCallback, onlyActiveRules) { 66 if (!node) { 67 return; 68 } 69 let skipRules = false; 70 let type = node['type']; 71 if (onlyActiveRules) { 72 if (type === types.MEDIA_RULE) { 73 let matchMedia = node['selector'].match(MEDIA_MATCH); 74 if (matchMedia) { 75 // if rule is a non matching @media rule, skip subrules 76 if (!window.matchMedia(matchMedia[1]).matches) { 77 skipRules = true; 78 } 79 } 80 } 81 } 82 if (type === types.STYLE_RULE) { 83 styleRuleCallback(node); 84 } else if (keyframesRuleCallback && 85 type === types.KEYFRAMES_RULE) { 86 keyframesRuleCallback(node); 87 } else if (type === types.MIXIN_RULE) { 88 skipRules = true; 89 } 90 let r$ = node['rules']; 91 if (r$ && !skipRules) { 92 for (let i=0, l=r$.length, r; (i<l) && (r=r$[i]); i++) { 93 forEachRule(r, styleRuleCallback, keyframesRuleCallback, onlyActiveRules); 94 } 95 } 96} 97 98// add a string of cssText to the document. 99/** 100 * @param {string} cssText 101 * @param {string} moniker 102 * @param {Node} target 103 * @param {Node} contextNode 104 * @return {HTMLStyleElement} 105 */ 106export function applyCss(cssText, moniker, target, contextNode) { 107 let style = createScopeStyle(cssText, moniker); 108 applyStyle(style, target, contextNode); 109 return style; 110} 111 112/** 113 * @param {string} cssText 114 * @param {string} moniker 115 * @return {HTMLStyleElement} 116 */ 117export function createScopeStyle(cssText, moniker) { 118 let style = /** @type {HTMLStyleElement} */(document.createElement('style')); 119 if (moniker) { 120 style.setAttribute('scope', moniker); 121 } 122 style.textContent = cssText; 123 return style; 124} 125 126/** 127 * Track the position of the last added style for placing placeholders 128 * @type {Node} 129 */ 130let lastHeadApplyNode = null; 131 132// insert a comment node as a styling position placeholder. 133/** 134 * @param {string} moniker 135 * @return {!Comment} 136 */ 137export function applyStylePlaceHolder(moniker) { 138 let placeHolder = document.createComment(' Shady DOM styles for ' + 139 moniker + ' '); 140 let after = lastHeadApplyNode ? 141 lastHeadApplyNode['nextSibling'] : null; 142 let scope = document.head; 143 scope.insertBefore(placeHolder, after || scope.firstChild); 144 lastHeadApplyNode = placeHolder; 145 return placeHolder; 146} 147 148/** 149 * @param {HTMLStyleElement} style 150 * @param {?Node} target 151 * @param {?Node} contextNode 152 */ 153export function applyStyle(style, target, contextNode) { 154 target = target || document.head; 155 let after = (contextNode && contextNode.nextSibling) || 156 target.firstChild; 157 target.insertBefore(style, after); 158 if (!lastHeadApplyNode) { 159 lastHeadApplyNode = style; 160 } else { 161 // only update lastHeadApplyNode if the new style is inserted after the old lastHeadApplyNode 162 let position = style.compareDocumentPosition(lastHeadApplyNode); 163 if (position === Node.DOCUMENT_POSITION_PRECEDING) { 164 lastHeadApplyNode = style; 165 } 166 } 167} 168 169/** 170 * @param {string} buildType 171 * @return {boolean} 172 */ 173export function isTargetedBuild(buildType) { 174 return nativeShadow ? buildType === 'shadow' : buildType === 'shady'; 175} 176 177/** 178 * Walk from text[start] matching parens and 179 * returns position of the outer end paren 180 * @param {string} text 181 * @param {number} start 182 * @return {number} 183 */ 184export function findMatchingParen(text, start) { 185 let level = 0; 186 for (let i=start, l=text.length; i < l; i++) { 187 if (text[i] === '(') { 188 level++; 189 } else if (text[i] === ')') { 190 if (--level === 0) { 191 return i; 192 } 193 } 194 } 195 return -1; 196} 197 198/** 199 * @param {string} str 200 * @param {function(string, string, string, string)} callback 201 */ 202export function processVariableAndFallback(str, callback) { 203 // find 'var(' 204 let start = str.indexOf('var('); 205 if (start === -1) { 206 // no var?, everything is prefix 207 return callback(str, '', '', ''); 208 } 209 //${prefix}var(${inner})${suffix} 210 let end = findMatchingParen(str, start + 3); 211 let inner = str.substring(start + 4, end); 212 let prefix = str.substring(0, start); 213 // suffix may have other variables 214 let suffix = processVariableAndFallback(str.substring(end + 1), callback); 215 let comma = inner.indexOf(','); 216 // value and fallback args should be trimmed to match in property lookup 217 if (comma === -1) { 218 // variable, no fallback 219 return callback(prefix, inner.trim(), '', suffix); 220 } 221 // var(${value},${fallback}) 222 let value = inner.substring(0, comma).trim(); 223 let fallback = inner.substring(comma + 1).trim(); 224 return callback(prefix, value, fallback, suffix); 225} 226 227/** 228 * @param {Element} element 229 * @param {string} value 230 */ 231export function setElementClassRaw(element, value) { 232 // use native setAttribute provided by ShadyDOM when setAttribute is patched 233 if (nativeShadow) { 234 element.setAttribute('class', value); 235 } else { 236 window['ShadyDOM']['nativeMethods']['setAttribute'].call(element, 'class', value); 237 } 238} 239 240export const wrap = window['ShadyDOM'] && window['ShadyDOM']['wrap'] || ((node) => node); 241 242/** 243 * @param {Element | {is: string, extends: string}} element 244 * @return {{is: string, typeExtension: string}} 245 */ 246export function getIsExtends(element) { 247 let localName = element['localName']; 248 let is = '', typeExtension = ''; 249 /* 250 NOTE: technically, this can be wrong for certain svg elements 251 with `-` in the name like `<font-face>` 252 */ 253 if (localName) { 254 if (localName.indexOf('-') > -1) { 255 is = localName; 256 } else { 257 typeExtension = localName; 258 is = (element.getAttribute && element.getAttribute('is')) || ''; 259 } 260 } else { 261 is = /** @type {?} */(element).is; 262 typeExtension = /** @type {?} */(element).extends; 263 } 264 return {is, typeExtension}; 265} 266 267/** 268 * @param {Element|DocumentFragment} element 269 * @return {string} 270 */ 271export function gatherStyleText(element) { 272 /** @type {!Array<string>} */ 273 const styleTextParts = []; 274 const styles = /** @type {!NodeList<!HTMLStyleElement>} */(element.querySelectorAll('style')); 275 for (let i = 0; i < styles.length; i++) { 276 const style = styles[i]; 277 if (isUnscopedStyle(style)) { 278 if (!nativeShadow) { 279 processUnscopedStyle(style); 280 style.parentNode.removeChild(style); 281 } 282 } else { 283 styleTextParts.push(style.textContent); 284 style.parentNode.removeChild(style); 285 } 286 } 287 return styleTextParts.join('').trim(); 288} 289 290/** 291 * Split a selector separated by commas into an array in a smart way 292 * @param {string} selector 293 * @return {!Array<string>} 294 */ 295export function splitSelectorList(selector) { 296 const parts = []; 297 let part = ''; 298 for (let i = 0; i >= 0 && i < selector.length; i++) { 299 // A selector with parentheses will be one complete part 300 if (selector[i] === '(') { 301 // find the matching paren 302 const end = findMatchingParen(selector, i); 303 // push the paren block into the part 304 part += selector.slice(i, end + 1); 305 // move the index to after the paren block 306 i = end; 307 } else if (selector[i] === ',') { 308 parts.push(part); 309 part = ''; 310 } else { 311 part += selector[i]; 312 } 313 } 314 // catch any pieces after the last comma 315 if (part) { 316 parts.push(part); 317 } 318 return parts; 319} 320 321const CSS_BUILD_ATTR = 'css-build'; 322 323/** 324 * Return the polymer-css-build "build type" applied to this element 325 * 326 * @param {!HTMLElement} element 327 * @return {string} Can be "", "shady", or "shadow" 328 */ 329export function getCssBuild(element) { 330 if (cssBuild !== undefined) { 331 return /** @type {string} */(cssBuild); 332 } 333 if (element.__cssBuild === undefined) { 334 // try attribute first, as it is the common case 335 const attrValue = element.getAttribute(CSS_BUILD_ATTR); 336 if (attrValue) { 337 element.__cssBuild = attrValue; 338 } else { 339 const buildComment = getBuildComment(element); 340 if (buildComment !== '') { 341 // remove build comment so it is not needlessly copied into every element instance 342 removeBuildComment(element); 343 } 344 element.__cssBuild = buildComment; 345 } 346 } 347 return element.__cssBuild || ''; 348} 349 350/** 351 * Check if the given element, either a <template> or <style>, has been processed 352 * by polymer-css-build. 353 * 354 * If so, then we can make a number of optimizations: 355 * - polymer-css-build will decompose mixins into individual CSS Custom Properties, 356 * so the ApplyShim can be skipped entirely. 357 * - Under native ShadowDOM, the style text can just be copied into each instance 358 * without modification 359 * - If the build is "shady" and ShadyDOM is in use, the styling does not need 360 * scoping beyond the shimming of CSS Custom Properties 361 * 362 * @param {!HTMLElement} element 363 * @return {boolean} 364 */ 365export function elementHasBuiltCss(element) { 366 return getCssBuild(element) !== ''; 367} 368 369/** 370 * For templates made with tagged template literals, polymer-css-build will 371 * insert a comment of the form `<!--css-build:shadow-->` 372 * 373 * @param {!HTMLElement} element 374 * @return {string} 375 */ 376export function getBuildComment(element) { 377 const buildComment = element.localName === 'template' ? 378 /** @type {!HTMLTemplateElement} */ (element).content.firstChild : 379 element.firstChild; 380 if (buildComment instanceof Comment) { 381 const commentParts = buildComment.textContent.trim().split(':'); 382 if (commentParts[0] === CSS_BUILD_ATTR) { 383 return commentParts[1]; 384 } 385 } 386 return ''; 387} 388 389/** 390 * Check if the css build status is optimal, and do no unneeded work. 391 * 392 * @param {string=} cssBuild CSS build status 393 * @return {boolean} css build is optimal or not 394 */ 395export function isOptimalCssBuild(cssBuild = '') { 396 // CSS custom property shim always requires work 397 if (cssBuild === '' || !nativeCssVariables) { 398 return false; 399 } 400 return nativeShadow ? cssBuild === 'shadow' : cssBuild === 'shady'; 401} 402 403/** 404 * @param {!HTMLElement} element 405 */ 406function removeBuildComment(element) { 407 const buildComment = element.localName === 'template' ? 408 /** @type {!HTMLTemplateElement} */ (element).content.firstChild : 409 element.firstChild; 410 buildComment.parentNode.removeChild(buildComment); 411} 412