| OLD | NEW |
| (Empty) | |
| 1 /** |
| 2 @license |
| 3 Copyright (c) 2017 The Polymer Project Authors. All rights reserved. |
| 4 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt |
| 5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| 6 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt |
| 7 Code distributed by Google as part of the polymer project is also |
| 8 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt |
| 9 */ |
| 10 |
| 11 'use strict'; |
| 12 |
| 13 import {StyleNode} from './css-parse.js' // eslint-disable-line no-unused-vars |
| 14 import * as StyleUtil from './style-util.js' |
| 15 import {nativeShadow} from './style-settings.js' |
| 16 |
| 17 /* Transforms ShadowDOM styling into ShadyDOM styling |
| 18 |
| 19 * scoping: |
| 20 |
| 21 * elements in scope get scoping selector class="x-foo-scope" |
| 22 * selectors re-written as follows: |
| 23 |
| 24 div button -> div.x-foo-scope button.x-foo-scope |
| 25 |
| 26 * :host -> scopeName |
| 27 |
| 28 * :host(...) -> scopeName... |
| 29 |
| 30 * ::slotted(...) -> scopeName > ... |
| 31 |
| 32 * ...:dir(ltr|rtl) -> [dir="ltr|rtl"] ..., ...[dir="ltr|rtl"] |
| 33 |
| 34 * :host(:dir[rtl]) -> scopeName:dir(rtl) -> [dir="rtl"] scopeName, scopeName[dir
="rtl"] |
| 35 |
| 36 */ |
| 37 const SCOPE_NAME = 'style-scope'; |
| 38 |
| 39 class StyleTransformer { |
| 40 get SCOPE_NAME() { |
| 41 return SCOPE_NAME; |
| 42 } |
| 43 // Given a node and scope name, add a scoping class to each node |
| 44 // in the tree. This facilitates transforming css into scoped rules. |
| 45 dom(node, scope, shouldRemoveScope) { |
| 46 // one time optimization to skip scoping... |
| 47 if (node['__styleScoped']) { |
| 48 node['__styleScoped'] = null; |
| 49 } else { |
| 50 this._transformDom(node, scope || '', shouldRemoveScope); |
| 51 } |
| 52 } |
| 53 |
| 54 _transformDom(node, selector, shouldRemoveScope) { |
| 55 if (node.nodeType === Node.ELEMENT_NODE) { |
| 56 this.element(node, selector, shouldRemoveScope); |
| 57 } |
| 58 let c$ = (node.localName === 'template') ? |
| 59 (node.content || node._content).childNodes : |
| 60 node.children || node.childNodes; |
| 61 if (c$) { |
| 62 for (let i=0; i<c$.length; i++) { |
| 63 this._transformDom(c$[i], selector, shouldRemoveScope); |
| 64 } |
| 65 } |
| 66 } |
| 67 |
| 68 element(element, scope, shouldRemoveScope) { |
| 69 // note: if using classes, we add both the general 'style-scope' class |
| 70 // as well as the specific scope. This enables easy filtering of all |
| 71 // `style-scope` elements |
| 72 if (scope) { |
| 73 // note: svg on IE does not have classList so fallback to class |
| 74 if (element.classList) { |
| 75 if (shouldRemoveScope) { |
| 76 element.classList.remove(SCOPE_NAME); |
| 77 element.classList.remove(scope); |
| 78 } else { |
| 79 element.classList.add(SCOPE_NAME); |
| 80 element.classList.add(scope); |
| 81 } |
| 82 } else if (element.getAttribute) { |
| 83 let c = element.getAttribute(CLASS); |
| 84 if (shouldRemoveScope) { |
| 85 if (c) { |
| 86 let newValue = c.replace(SCOPE_NAME, '').replace(scope, ''); |
| 87 StyleUtil.setElementClassRaw(element, newValue); |
| 88 } |
| 89 } else { |
| 90 let newValue = (c ? c + ' ' : '') + SCOPE_NAME + ' ' + scope; |
| 91 StyleUtil.setElementClassRaw(element, newValue); |
| 92 } |
| 93 } |
| 94 } |
| 95 } |
| 96 |
| 97 elementStyles(element, styleRules, callback) { |
| 98 let cssBuildType = element['__cssBuild']; |
| 99 // no need to shim selectors if settings.useNativeShadow, also |
| 100 // a shady css build will already have transformed selectors |
| 101 // NOTE: This method may be called as part of static or property shimming. |
| 102 // When there is a targeted build it will not be called for static shimming, |
| 103 // but when the property shim is used it is called and should opt out of |
| 104 // static shimming work when a proper build exists. |
| 105 let cssText = ''; |
| 106 if (nativeShadow || cssBuildType === 'shady') { |
| 107 cssText = StyleUtil.toCssText(styleRules, callback); |
| 108 } else { |
| 109 let {is, typeExtension} = StyleUtil.getIsExtends(element); |
| 110 cssText = this.css(styleRules, is, typeExtension, callback) + '\n\n'; |
| 111 } |
| 112 return cssText.trim(); |
| 113 } |
| 114 |
| 115 // Given a string of cssText and a scoping string (scope), returns |
| 116 // a string of scoped css where each selector is transformed to include |
| 117 // a class created from the scope. ShadowDOM selectors are also transformed |
| 118 // (e.g. :host) to use the scoping selector. |
| 119 css(rules, scope, ext, callback) { |
| 120 let hostScope = this._calcHostScope(scope, ext); |
| 121 scope = this._calcElementScope(scope); |
| 122 let self = this; |
| 123 return StyleUtil.toCssText(rules, function(/** StyleNode */rule) { |
| 124 if (!rule.isScoped) { |
| 125 self.rule(rule, scope, hostScope); |
| 126 rule.isScoped = true; |
| 127 } |
| 128 if (callback) { |
| 129 callback(rule, scope, hostScope); |
| 130 } |
| 131 }); |
| 132 } |
| 133 |
| 134 _calcElementScope(scope) { |
| 135 if (scope) { |
| 136 return CSS_CLASS_PREFIX + scope; |
| 137 } else { |
| 138 return ''; |
| 139 } |
| 140 } |
| 141 |
| 142 _calcHostScope(scope, ext) { |
| 143 return ext ? `[is=${scope}]` : scope; |
| 144 } |
| 145 |
| 146 rule(rule, scope, hostScope) { |
| 147 this._transformRule(rule, this._transformComplexSelector, |
| 148 scope, hostScope); |
| 149 } |
| 150 |
| 151 /** |
| 152 * transforms a css rule to a scoped rule. |
| 153 * |
| 154 * @param {StyleNode} rule |
| 155 * @param {Function} transformer |
| 156 * @param {string=} scope |
| 157 * @param {string=} hostScope |
| 158 */ |
| 159 _transformRule(rule, transformer, scope, hostScope) { |
| 160 // NOTE: save transformedSelector for subsequent matching of elements |
| 161 // against selectors (e.g. when calculating style properties) |
| 162 rule['selector'] = rule.transformedSelector = |
| 163 this._transformRuleCss(rule, transformer, scope, hostScope); |
| 164 } |
| 165 |
| 166 /** |
| 167 * @param {StyleNode} rule |
| 168 * @param {Function} transformer |
| 169 * @param {string=} scope |
| 170 * @param {string=} hostScope |
| 171 */ |
| 172 _transformRuleCss(rule, transformer, scope, hostScope) { |
| 173 let p$ = rule['selector'].split(COMPLEX_SELECTOR_SEP); |
| 174 // we want to skip transformation of rules that appear in keyframes, |
| 175 // because they are keyframe selectors, not element selectors. |
| 176 if (!StyleUtil.isKeyframesSelector(rule)) { |
| 177 for (let i=0, l=p$.length, p; (i<l) && (p=p$[i]); i++) { |
| 178 p$[i] = transformer.call(this, p, scope, hostScope); |
| 179 } |
| 180 } |
| 181 return p$.join(COMPLEX_SELECTOR_SEP); |
| 182 } |
| 183 |
| 184 /** |
| 185 * @param {string} selector |
| 186 * @param {string} scope |
| 187 * @param {string=} hostScope |
| 188 */ |
| 189 _transformComplexSelector(selector, scope, hostScope) { |
| 190 let stop = false; |
| 191 selector = selector.trim(); |
| 192 // Remove spaces inside of selectors like `:nth-of-type` because it confuses
SIMPLE_SELECTOR_SEP |
| 193 selector = selector.replace(NTH, (m, type, inner) => `:${type}(${inner.repla
ce(/\s/g, '')})`); |
| 194 selector = selector.replace(SLOTTED_START, `${HOST} $1`); |
| 195 selector = selector.replace(SIMPLE_SELECTOR_SEP, (m, c, s) => { |
| 196 if (!stop) { |
| 197 let info = this._transformCompoundSelector(s, c, scope, hostScope); |
| 198 stop = stop || info.stop; |
| 199 c = info.combinator; |
| 200 s = info.value; |
| 201 } |
| 202 return c + s; |
| 203 }); |
| 204 return selector; |
| 205 } |
| 206 |
| 207 _transformCompoundSelector(selector, combinator, scope, hostScope) { |
| 208 // replace :host with host scoping class |
| 209 let slottedIndex = selector.indexOf(SLOTTED); |
| 210 if (selector.indexOf(HOST) >= 0) { |
| 211 selector = this._transformHostSelector(selector, hostScope); |
| 212 // replace other selectors with scoping class |
| 213 } else if (slottedIndex !== 0) { |
| 214 selector = scope ? this._transformSimpleSelector(selector, scope) : |
| 215 selector; |
| 216 } |
| 217 // mark ::slotted() scope jump to replace with descendant selector + arg |
| 218 // also ignore left-side combinator |
| 219 let slotted = false; |
| 220 if (slottedIndex >= 0) { |
| 221 combinator = ''; |
| 222 slotted = true; |
| 223 } |
| 224 // process scope jumping selectors up to the scope jump and then stop |
| 225 let stop; |
| 226 if (slotted) { |
| 227 stop = true; |
| 228 if (slotted) { |
| 229 // .zonk ::slotted(.foo) -> .zonk.scope > .foo |
| 230 selector = selector.replace(SLOTTED_PAREN, (m, paren) => ` > ${paren}`); |
| 231 } |
| 232 } |
| 233 selector = selector.replace(DIR_PAREN, (m, before, dir) => |
| 234 `[dir="${dir}"] ${before}, ${before}[dir="${dir}"]`); |
| 235 return {value: selector, combinator, stop}; |
| 236 } |
| 237 |
| 238 _transformSimpleSelector(selector, scope) { |
| 239 let p$ = selector.split(PSEUDO_PREFIX); |
| 240 p$[0] += scope; |
| 241 return p$.join(PSEUDO_PREFIX); |
| 242 } |
| 243 |
| 244 // :host(...) -> scopeName... |
| 245 _transformHostSelector(selector, hostScope) { |
| 246 let m = selector.match(HOST_PAREN); |
| 247 let paren = m && m[2].trim() || ''; |
| 248 if (paren) { |
| 249 if (!paren[0].match(SIMPLE_SELECTOR_PREFIX)) { |
| 250 // paren starts with a type selector |
| 251 let typeSelector = paren.split(SIMPLE_SELECTOR_PREFIX)[0]; |
| 252 // if the type selector is our hostScope then avoid pre-pending it |
| 253 if (typeSelector === hostScope) { |
| 254 return paren; |
| 255 // otherwise, this selector should not match in this scope so |
| 256 // output a bogus selector. |
| 257 } else { |
| 258 return SELECTOR_NO_MATCH; |
| 259 } |
| 260 } else { |
| 261 // make sure to do a replace here to catch selectors like: |
| 262 // `:host(.foo)::before` |
| 263 return selector.replace(HOST_PAREN, function(m, host, paren) { |
| 264 return hostScope + paren; |
| 265 }); |
| 266 } |
| 267 // if no paren, do a straight :host replacement. |
| 268 // TODO(sorvell): this should not strictly be necessary but |
| 269 // it's needed to maintain support for `:host[foo]` type selectors |
| 270 // which have been improperly used under Shady DOM. This should be |
| 271 // deprecated. |
| 272 } else { |
| 273 return selector.replace(HOST, hostScope); |
| 274 } |
| 275 } |
| 276 |
| 277 /** |
| 278 * @param {StyleNode} rule |
| 279 */ |
| 280 documentRule(rule) { |
| 281 // reset selector in case this is redone. |
| 282 rule['selector'] = rule['parsedSelector']; |
| 283 this.normalizeRootSelector(rule); |
| 284 this._transformRule(rule, this._transformDocumentSelector); |
| 285 } |
| 286 |
| 287 /** |
| 288 * @param {StyleNode} rule |
| 289 */ |
| 290 normalizeRootSelector(rule) { |
| 291 if (rule['selector'] === ROOT) { |
| 292 rule['selector'] = 'html'; |
| 293 } |
| 294 } |
| 295 |
| 296 /** |
| 297 * @param {string} selector |
| 298 */ |
| 299 _transformDocumentSelector(selector) { |
| 300 return selector.match(SLOTTED) ? |
| 301 this._transformComplexSelector(selector, SCOPE_DOC_SELECTOR) : |
| 302 this._transformSimpleSelector(selector.trim(), SCOPE_DOC_SELECTOR); |
| 303 } |
| 304 } |
| 305 |
| 306 let NTH = /:(nth[-\w]+)\(([^)]+)\)/; |
| 307 let SCOPE_DOC_SELECTOR = `:not(.${SCOPE_NAME})`; |
| 308 let COMPLEX_SELECTOR_SEP = ','; |
| 309 let SIMPLE_SELECTOR_SEP = /(^|[\s>+~]+)((?:\[.+?\]|[^\s>+~=\[])+)/g; |
| 310 let SIMPLE_SELECTOR_PREFIX = /[[.:#*]/; |
| 311 let HOST = ':host'; |
| 312 let ROOT = ':root'; |
| 313 let SLOTTED = '::slotted'; |
| 314 let SLOTTED_START = new RegExp(`^(${SLOTTED})`); |
| 315 // NOTE: this supports 1 nested () pair for things like |
| 316 // :host(:not([selected]), more general support requires |
| 317 // parsing which seems like overkill |
| 318 let HOST_PAREN = /(:host)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/; |
| 319 // similar to HOST_PAREN |
| 320 let SLOTTED_PAREN = /(?:::slotted)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/; |
| 321 let DIR_PAREN = /(.*):dir\((?:(ltr|rtl))\)/; |
| 322 let CSS_CLASS_PREFIX = '.'; |
| 323 let PSEUDO_PREFIX = ':'; |
| 324 let CLASS = 'class'; |
| 325 let SELECTOR_NO_MATCH = 'should_not_match'; |
| 326 |
| 327 export default new StyleTransformer() |
| OLD | NEW |