Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(10)

Side by Side Diff: appengine/config_service/ui/bower_components/shadycss/src/apply-shim.js

Issue 2923973003: Added base template for config ui. (Closed)
Patch Set: Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 * The apply shim simulates the behavior of `@apply` proposed at
12 * https://tabatkins.github.io/specs/css-apply-rule/.
13 * The approach is to convert a property like this:
14 *
15 * --foo: {color: red; background: blue;}
16 *
17 * to this:
18 *
19 * --foo_-_color: red;
20 * --foo_-_background: blue;
21 *
22 * Then where `@apply --foo` is used, that is converted to:
23 *
24 * color: var(--foo_-_color);
25 * background: var(--foo_-_background);
26 *
27 * This approach generally works but there are some issues and limitations.
28 * Consider, for example, that somewhere *between* where `--foo` is set and used ,
29 * another element sets it to:
30 *
31 * --foo: { border: 2px solid red; }
32 *
33 * We must now ensure that the color and background from the previous setting
34 * do not apply. This is accomplished by changing the property set to this:
35 *
36 * --foo_-_border: 2px solid red;
37 * --foo_-_color: initial;
38 * --foo_-_background: initial;
39 *
40 * This works but introduces one new issue.
41 * Consider this setup at the point where the `@apply` is used:
42 *
43 * background: orange;
44 * `@apply` --foo;
45 *
46 * In this case the background will be unset (initial) rather than the desired
47 * `orange`. We address this by altering the property set to use a fallback
48 * value like this:
49 *
50 * color: var(--foo_-_color);
51 * background: var(--foo_-_background, orange);
52 * border: var(--foo_-_border);
53 *
54 * Note that the default is retained in the property set and the `background` is
55 * the desired `orange`. This leads us to a limitation.
56 *
57 * Limitation 1:
58
59 * Only properties in the rule where the `@apply`
60 * is used are considered as default values.
61 * If another rule matches the element and sets `background` with
62 * less specificity than the rule in which `@apply` appears,
63 * the `background` will not be set.
64 *
65 * Limitation 2:
66 *
67 * When using Polymer's `updateStyles` api, new properties may not be set for
68 * `@apply` properties.
69
70 */
71
72 'use strict';
73
74 import {forEachRule, processVariableAndFallback, rulesForStyle, toCssText} from './style-util.js'
75 import {MIXIN_MATCH, VAR_ASSIGN} from './common-regex.js'
76 import {detectMixin} from './common-utils.js'
77 import {StyleNode} from './css-parse.js' // eslint-disable-line no-unused-vars
78
79 const APPLY_NAME_CLEAN = /;\s*/m;
80 const INITIAL_INHERIT = /^\s*(initial)|(inherit)\s*$/;
81
82 // separator used between mixin-name and mixin-property-name when producing prop erties
83 // NOTE: plain '-' may cause collisions in user styles
84 const MIXIN_VAR_SEP = '_-_';
85
86 /**
87 * @typedef {!Object<string, string>}
88 */
89 let PropertyEntry; // eslint-disable-line no-unused-vars
90
91 /**
92 * @typedef {!Object<string, boolean>}
93 */
94 let DependantsEntry; // eslint-disable-line no-unused-vars
95
96 /** @typedef {{
97 * properties: PropertyEntry,
98 * dependants: DependantsEntry
99 * }}
100 */
101 let MixinMapEntry; // eslint-disable-line no-unused-vars
102
103 // map of mixin to property names
104 // --foo: {border: 2px} -> {properties: {(--foo, ['border'])}, dependants: {'ele ment-name': proto}}
105 class MixinMap {
106 constructor() {
107 /** @type {!Object<string, !MixinMapEntry>} */
108 this._map = {};
109 }
110 /**
111 * @param {string} name
112 * @param {!PropertyEntry} props
113 */
114 set(name, props) {
115 name = name.trim();
116 this._map[name] = {
117 properties: props,
118 dependants: {}
119 }
120 }
121 /**
122 * @param {string} name
123 * @return {MixinMapEntry}
124 */
125 get(name) {
126 name = name.trim();
127 return this._map[name] || null;
128 }
129 }
130
131 /**
132 * Callback for when an element is marked invalid
133 * @type {?function(string)}
134 */
135 let invalidCallback = null;
136
137 /** @unrestricted */
138 class ApplyShim {
139 constructor() {
140 /** @type {?string} */
141 this._currentElement = null;
142 /** @type {HTMLMetaElement} */
143 this._measureElement = null;
144 this._map = new MixinMap();
145 }
146 /**
147 * return true if `cssText` contains a mixin definition or consumption
148 * @param {string} cssText
149 * @return {boolean}
150 */
151 detectMixin(cssText) {
152 return detectMixin(cssText);
153 }
154 /**
155 * @param {!HTMLTemplateElement} template
156 * @param {string} elementName
157 * @return {StyleNode}
158 */
159 transformTemplate(template, elementName) {
160 const style = /** @type {HTMLStyleElement} */(template.content.querySelector ('style'));
161 /** @type {StyleNode} */
162 let ast = null;
163 if (style) {
164 ast = this.transformStyle(style, elementName);
165 }
166 return ast;
167 }
168 /**
169 * @param {!HTMLStyleElement} style
170 * @param {string} elementName
171 * @return {StyleNode}
172 */
173 transformStyle(style, elementName = '') {
174 let ast = rulesForStyle(style);
175 this.transformRules(ast, elementName);
176 style.textContent = toCssText(ast);
177 return ast;
178 }
179 /**
180 * @param {!HTMLStyleElement} style
181 * @return {StyleNode}
182 */
183 transformCustomStyle(style) {
184 let ast = rulesForStyle(style);
185 forEachRule(ast, (rule) => {
186 if (rule['selector'] === ':root') {
187 rule['selector'] = 'html';
188 }
189 this.transformRule(rule);
190 })
191 style.textContent = toCssText(ast);
192 return ast;
193 }
194 /**
195 * @param {StyleNode} rules
196 * @param {string} elementName
197 */
198 transformRules(rules, elementName) {
199 this._currentElement = elementName;
200 forEachRule(rules, (r) => {
201 this.transformRule(r);
202 });
203 this._currentElement = null;
204 }
205 /**
206 * @param {!StyleNode} rule
207 */
208 transformRule(rule) {
209 rule['cssText'] = this.transformCssText(rule['parsedCssText']);
210 // :root was only used for variable assignment in property shim,
211 // but generates invalid selectors with real properties.
212 // replace with `:host > *`, which serves the same effect
213 if (rule['selector'] === ':root') {
214 rule['selector'] = ':host > *';
215 }
216 }
217 /**
218 * @param {string} cssText
219 * @return {string}
220 */
221 transformCssText(cssText) {
222 // produce variables
223 cssText = cssText.replace(VAR_ASSIGN, (matchText, propertyName, valuePropert y, valueMixin) =>
224 this._produceCssProperties(matchText, propertyName, valueProperty, valueMi xin));
225 // consume mixins
226 return this._consumeCssProperties(cssText);
227 }
228 /**
229 * @param {string} property
230 * @return {string}
231 */
232 _getInitialValueForProperty(property) {
233 if (!this._measureElement) {
234 this._measureElement = /** @type {HTMLMetaElement} */(document.createEleme nt('meta'));
235 this._measureElement.setAttribute('apply-shim-measure', '');
236 this._measureElement.style.all = 'initial';
237 document.head.appendChild(this._measureElement);
238 }
239 return window.getComputedStyle(this._measureElement).getPropertyValue(proper ty);
240 }
241 /**
242 * replace mixin consumption with variable consumption
243 * @param {string} text
244 * @return {string}
245 */
246 _consumeCssProperties(text) {
247 /** @type {Array} */
248 let m = null;
249 // loop over text until all mixins with defintions have been applied
250 while((m = MIXIN_MATCH.exec(text))) {
251 let matchText = m[0];
252 let mixinName = m[1];
253 let idx = m.index;
254 // collect properties before apply to be "defaults" if mixin might overrid e them
255 // match includes a "prefix", so find the start and end positions of @appl y
256 let applyPos = idx + matchText.indexOf('@apply');
257 let afterApplyPos = idx + matchText.length;
258 // find props defined before this @apply
259 let textBeforeApply = text.slice(0, applyPos);
260 let textAfterApply = text.slice(afterApplyPos);
261 let defaults = this._cssTextToMap(textBeforeApply);
262 let replacement = this._atApplyToCssProperties(mixinName, defaults);
263 // use regex match position to replace mixin, keep linear processing time
264 text = `${textBeforeApply}${replacement}${textAfterApply}`;
265 // move regex search to _after_ replacement
266 MIXIN_MATCH.lastIndex = idx + replacement.length;
267 }
268 return text;
269 }
270 /**
271 * produce variable consumption at the site of mixin consumption
272 * `@apply` --foo; -> for all props (${propname}: var(--foo_-_${propname}, ${f allback[propname]}}))
273 * Example:
274 * border: var(--foo_-_border); padding: var(--foo_-_padding, 2px)
275 *
276 * @param {string} mixinName
277 * @param {Object} fallbacks
278 * @return {string}
279 */
280 _atApplyToCssProperties(mixinName, fallbacks) {
281 mixinName = mixinName.replace(APPLY_NAME_CLEAN, '');
282 let vars = [];
283 let mixinEntry = this._map.get(mixinName);
284 // if we depend on a mixin before it is created
285 // make a sentinel entry in the map to add this element as a dependency for when it is defined.
286 if (!mixinEntry) {
287 this._map.set(mixinName, {});
288 mixinEntry = this._map.get(mixinName);
289 }
290 if (mixinEntry) {
291 if (this._currentElement) {
292 mixinEntry.dependants[this._currentElement] = true;
293 }
294 let p, parts, f;
295 for (p in mixinEntry.properties) {
296 f = fallbacks && fallbacks[p];
297 parts = [p, ': var(', mixinName, MIXIN_VAR_SEP, p];
298 if (f) {
299 parts.push(',', f);
300 }
301 parts.push(')');
302 vars.push(parts.join(''));
303 }
304 }
305 return vars.join('; ');
306 }
307
308 /**
309 * @param {string} property
310 * @param {string} value
311 * @return {string}
312 */
313 _replaceInitialOrInherit(property, value) {
314 let match = INITIAL_INHERIT.exec(value);
315 if (match) {
316 if (match[1]) {
317 // initial
318 // replace `initial` with the concrete initial value for this property
319 value = this._getInitialValueForProperty(property);
320 } else {
321 // inherit
322 // with this purposfully illegal value, the variable will be invalid at
323 // compute time (https://www.w3.org/TR/css-variables/#invalid-at-compute d-value-time)
324 // and for inheriting values, will behave similarly
325 // we cannot support the same behavior for non inheriting values like 'b order'
326 value = 'apply-shim-inherit';
327 }
328 }
329 return value;
330 }
331
332 /**
333 * "parse" a mixin definition into a map of properties and values
334 * cssTextToMap('border: 2px solid black') -> ('border', '2px solid black')
335 * @param {string} text
336 * @return {!Object<string, string>}
337 */
338 _cssTextToMap(text) {
339 let props = text.split(';');
340 let property, value;
341 let out = {};
342 for (let i = 0, p, sp; i < props.length; i++) {
343 p = props[i];
344 if (p) {
345 sp = p.split(':');
346 // ignore lines that aren't definitions like @media
347 if (sp.length > 1) {
348 property = sp[0].trim();
349 // some properties may have ':' in the value, like data urls
350 value = this._replaceInitialOrInherit(property, sp.slice(1).join(':')) ;
351 out[property] = value;
352 }
353 }
354 }
355 return out;
356 }
357
358 /**
359 * @param {MixinMapEntry} mixinEntry
360 */
361 _invalidateMixinEntry(mixinEntry) {
362 if (!invalidCallback) {
363 return;
364 }
365 for (let elementName in mixinEntry.dependants) {
366 if (elementName !== this._currentElement) {
367 invalidCallback(elementName);
368 }
369 }
370 }
371
372 /**
373 * @param {string} matchText
374 * @param {string} propertyName
375 * @param {?string} valueProperty
376 * @param {?string} valueMixin
377 * @return {string}
378 */
379 _produceCssProperties(matchText, propertyName, valueProperty, valueMixin) {
380 // handle case where property value is a mixin
381 if (valueProperty) {
382 // form: --mixin2: var(--mixin1), where --mixin1 is in the map
383 processVariableAndFallback(valueProperty, (prefix, value) => {
384 if (value && this._map.get(value)) {
385 valueMixin = `@apply ${value};`
386 }
387 });
388 }
389 if (!valueMixin) {
390 return matchText;
391 }
392 let mixinAsProperties = this._consumeCssProperties(valueMixin);
393 let prefix = matchText.slice(0, matchText.indexOf('--'));
394 let mixinValues = this._cssTextToMap(mixinAsProperties);
395 let combinedProps = mixinValues;
396 let mixinEntry = this._map.get(propertyName);
397 let oldProps = mixinEntry && mixinEntry.properties;
398 if (oldProps) {
399 // NOTE: since we use mixin, the map of properties is updated here
400 // and this is what we want.
401 combinedProps = Object.assign(Object.create(oldProps), mixinValues);
402 } else {
403 this._map.set(propertyName, combinedProps);
404 }
405 let out = [];
406 let p, v;
407 // set variables defined by current mixin
408 let needToInvalidate = false;
409 for (p in combinedProps) {
410 v = mixinValues[p];
411 // if property not defined by current mixin, set initial
412 if (v === undefined) {
413 v = 'initial';
414 }
415 if (oldProps && !(p in oldProps)) {
416 needToInvalidate = true;
417 }
418 out.push(`${propertyName}${MIXIN_VAR_SEP}${p}: ${v}`);
419 }
420 if (needToInvalidate) {
421 this._invalidateMixinEntry(mixinEntry);
422 }
423 if (mixinEntry) {
424 mixinEntry.properties = combinedProps;
425 }
426 // because the mixinMap is global, the mixin might conflict with
427 // a different scope's simple variable definition:
428 // Example:
429 // some style somewhere:
430 // --mixin1:{ ... }
431 // --mixin2: var(--mixin1);
432 // some other element:
433 // --mixin1: 10px solid red;
434 // --foo: var(--mixin1);
435 // In this case, we leave the original variable definition in place.
436 if (valueProperty) {
437 prefix = `${matchText};${prefix}`;
438 }
439 return `${prefix}${out.join('; ')};`;
440 }
441 }
442
443 /* exports */
444 ApplyShim.prototype['detectMixin'] = ApplyShim.prototype.detectMixin;
445 ApplyShim.prototype['transformStyle'] = ApplyShim.prototype.transformStyle;
446 ApplyShim.prototype['transformCustomStyle'] = ApplyShim.prototype.transformCusto mStyle;
447 ApplyShim.prototype['transformRules'] = ApplyShim.prototype.transformRules;
448 ApplyShim.prototype['transformRule'] = ApplyShim.prototype.transformRule;
449 ApplyShim.prototype['transformTemplate'] = ApplyShim.prototype.transformTemplate ;
450 ApplyShim.prototype['_separator'] = MIXIN_VAR_SEP;
451 Object.defineProperty(ApplyShim.prototype, 'invalidCallback', {
452 /** @return {?function(string)} */
453 get() {
454 return invalidCallback;
455 },
456 /** @param {?function(string)} cb */
457 set(cb) {
458 invalidCallback = cb;
459 }
460 });
461
462 export default ApplyShim;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698