UNPKG

40.8 kBJavaScriptView Raw
1import { unsetValue, _evaluateCssVariableExpression, _evaluateCssCalcExpression, isCssVariable, isCssVariableExpression, isCssCalcExpression } from '../core/properties';
2import { StyleSheetSelectorScope, SelectorsMatch, fromAstNode, MEDIA_QUERY_SEPARATOR, matchMediaQueryString } from './css-selector';
3import { Trace } from '../../trace';
4import { File, knownFolders, path } from '../../file-system';
5import { Application } from '../../application';
6import { profile } from '../../profiling';
7let keyframeAnimationModule;
8function ensureKeyframeAnimationModule() {
9 if (!keyframeAnimationModule) {
10 keyframeAnimationModule = require('../animation/keyframe-animation');
11 }
12}
13import { sanitizeModuleName } from '../../utils/common';
14import { resolveModuleName } from '../../module-name-resolver';
15import { cleanupImportantFlags } from './css-utils';
16let cssAnimationParserModule;
17function ensureCssAnimationParserModule() {
18 if (!cssAnimationParserModule) {
19 cssAnimationParserModule = require('./css-animation-parser');
20 }
21}
22let parser = 'css-tree';
23try {
24 const appConfig = require('~/package.json');
25 if (appConfig) {
26 if (appConfig.cssParser === 'rework') {
27 parser = 'rework';
28 }
29 else if (appConfig.cssParser === 'nativescript') {
30 parser = 'nativescript';
31 }
32 }
33}
34catch (e) {
35 //
36}
37let mergedApplicationCssSelectors = [];
38let applicationCssSelectors = [];
39const applicationAdditionalSelectors = [];
40let mergedApplicationCssKeyframes = [];
41let applicationCssKeyframes = [];
42const applicationAdditionalKeyframes = [];
43let applicationCssSelectorVersion = 0;
44const tagToScopeTag = new Map();
45let currentScopeTag = null;
46const animationsSymbol = Symbol('animations');
47const kebabCasePattern = /-([a-z])/g;
48const pattern = /('|")(.*?)\1/;
49/**
50 * Evaluate css-variable and css-calc expressions
51 */
52function evaluateCssExpressions(view, property, value) {
53 const newValue = _evaluateCssVariableExpression(view, property, value);
54 if (newValue === 'unset') {
55 return unsetValue;
56 }
57 value = newValue;
58 try {
59 value = _evaluateCssCalcExpression(value);
60 }
61 catch (e) {
62 Trace.write(`Failed to evaluate css-calc for property [${property}] for expression [${value}] to ${view}. ${e.stack}`, Trace.categories.Error, Trace.messageType.error);
63 return unsetValue;
64 }
65 return value;
66}
67export function mergeCssSelectors() {
68 mergedApplicationCssSelectors = applicationCssSelectors.slice();
69 mergedApplicationCssSelectors.push(...applicationAdditionalSelectors);
70}
71export function mergeCssKeyframes() {
72 mergedApplicationCssKeyframes = applicationCssKeyframes.slice();
73 mergedApplicationCssKeyframes.push(...applicationAdditionalKeyframes);
74}
75class CSSSource {
76 constructor(_ast, _url, _file, _source) {
77 this._ast = _ast;
78 this._url = _url;
79 this._file = _file;
80 this._source = _source;
81 this._selectors = [];
82 this._keyframes = [];
83 this.parse();
84 }
85 static fromDetect(cssOrAst, fileName) {
86 if (typeof cssOrAst === 'string') {
87 // raw-loader
88 return CSSSource.fromSource(cssOrAst, fileName);
89 }
90 else if (typeof cssOrAst === 'object') {
91 if (cssOrAst.default) {
92 cssOrAst = cssOrAst.default;
93 }
94 if (cssOrAst.type === 'stylesheet' && cssOrAst.stylesheet && cssOrAst.stylesheet.rules) {
95 // css-loader
96 return CSSSource.fromAST(cssOrAst, fileName);
97 }
98 }
99 // css2json-loader
100 return CSSSource.fromSource(cssOrAst.toString(), fileName);
101 }
102 static fromURI(uri) {
103 // webpack modules require all file paths to be relative to /app folder
104 const appRelativeUri = CSSSource.pathRelativeToApp(uri);
105 const sanitizedModuleName = sanitizeModuleName(appRelativeUri);
106 const resolvedModuleName = resolveModuleName(sanitizedModuleName, 'css');
107 try {
108 const cssOrAst = global.loadModule(resolvedModuleName, true);
109 if (cssOrAst) {
110 return CSSSource.fromDetect(cssOrAst, resolvedModuleName);
111 }
112 }
113 catch (e) {
114 if (Trace.isEnabled()) {
115 Trace.write(`Could not load CSS from ${uri}: ${e}`, Trace.categories.Error, Trace.messageType.warn);
116 }
117 }
118 return CSSSource.fromFile(appRelativeUri);
119 }
120 static pathRelativeToApp(uri) {
121 if (!uri.startsWith('/')) {
122 return uri;
123 }
124 const appPath = knownFolders.currentApp().path;
125 if (!uri.startsWith(appPath)) {
126 Trace.write(`${uri} does not start with ${appPath}`, Trace.categories.Error, Trace.messageType.error);
127 return uri;
128 }
129 const relativeUri = `.${uri.substring(appPath.length)}`;
130 return relativeUri;
131 }
132 static fromFile(url) {
133 // .scss, .sass, etc. css files in vanilla app are usually compiled to .css so we will try to load a compiled file first.
134 const cssFileUrl = url.replace(/\..\w+$/, '.css');
135 if (cssFileUrl !== url) {
136 const cssFile = CSSSource.resolveCSSPathFromURL(cssFileUrl);
137 if (cssFile) {
138 return new CSSSource(undefined, url, cssFile, undefined);
139 }
140 }
141 const file = CSSSource.resolveCSSPathFromURL(url);
142 return new CSSSource(undefined, url, file, undefined);
143 }
144 static fromFileImport(url, importSource) {
145 const file = CSSSource.resolveCSSPathFromURL(url, importSource);
146 return new CSSSource(undefined, url, file, undefined);
147 }
148 static resolveCSSPathFromURL(url, importSource) {
149 const app = knownFolders.currentApp().path;
150 const file = resolveFileNameFromUrl(url, app, File.exists, importSource);
151 return file;
152 }
153 static fromSource(source, url) {
154 return new CSSSource(undefined, url, undefined, source);
155 }
156 static fromAST(ast, url) {
157 return new CSSSource(ast, url, undefined, undefined);
158 }
159 get selectors() {
160 return this._selectors;
161 }
162 get keyframes() {
163 return this._keyframes;
164 }
165 get source() {
166 return this._source;
167 }
168 load() {
169 const file = File.fromPath(this._file);
170 this._source = file.readTextSync();
171 }
172 parse() {
173 try {
174 if (!this._ast) {
175 if (!this._source && this._file) {
176 this.load();
177 }
178 // [object Object] check guards against empty app.css file
179 if (this._source && this.source !== '[object Object]') {
180 this.parseCSSAst();
181 }
182 }
183 if (this._ast) {
184 this.createSelectorsAndKeyframes();
185 }
186 else {
187 this._selectors = [];
188 }
189 }
190 catch (e) {
191 if (Trace.isEnabled()) {
192 Trace.write('Css styling failed: ' + e, Trace.categories.Style, Trace.messageType.error);
193 }
194 this._selectors = [];
195 }
196 }
197 parseCSSAst() {
198 if (this._source) {
199 if (__CSS_PARSER__ === 'css-tree') {
200 const cssTreeParse = require('../../css/css-tree-parser').cssTreeParse;
201 this._ast = cssTreeParse(this._source, this._file);
202 }
203 else if (__CSS_PARSER__ === 'nativescript') {
204 const CSS3Parser = require('../../css/CSS3Parser').CSS3Parser;
205 const CSSNativeScript = require('../../css/CSSNativeScript').CSSNativeScript;
206 const cssparser = new CSS3Parser(this._source);
207 const stylesheet = cssparser.parseAStylesheet();
208 const cssNS = new CSSNativeScript();
209 this._ast = cssNS.parseStylesheet(stylesheet);
210 }
211 else if (__CSS_PARSER__ === 'rework') {
212 const parseCss = require('../../css').parse;
213 this._ast = parseCss(this._source, { source: this._file });
214 }
215 }
216 }
217 createSelectorsAndKeyframes() {
218 if (this._ast) {
219 const nodes = this._ast.stylesheet.rules;
220 const rulesets = [];
221 const keyframes = [];
222 // When css2json-loader is enabled, imports are handled there and removed from AST rules
223 populateRulesFromImports(nodes, rulesets, keyframes);
224 _populateRules(nodes, rulesets, keyframes);
225 if (rulesets && rulesets.length) {
226 ensureCssAnimationParserModule();
227 rulesets.forEach((rule) => {
228 rule[animationsSymbol] = cssAnimationParserModule.CssAnimationParser.keyframeAnimationsFromCSSDeclarations(rule.declarations);
229 });
230 }
231 this._selectors = rulesets;
232 this._keyframes = keyframes;
233 }
234 }
235 toString() {
236 return this._file || this._url || '(in-memory)';
237 }
238}
239__decorate([
240 profile,
241 __metadata("design:type", Function),
242 __metadata("design:paramtypes", []),
243 __metadata("design:returntype", void 0)
244], CSSSource.prototype, "load", null);
245__decorate([
246 profile,
247 __metadata("design:type", Function),
248 __metadata("design:paramtypes", []),
249 __metadata("design:returntype", void 0)
250], CSSSource.prototype, "parse", null);
251__decorate([
252 profile,
253 __metadata("design:type", Function),
254 __metadata("design:paramtypes", []),
255 __metadata("design:returntype", void 0)
256], CSSSource.prototype, "parseCSSAst", null);
257__decorate([
258 profile,
259 __metadata("design:type", Function),
260 __metadata("design:paramtypes", []),
261 __metadata("design:returntype", void 0)
262], CSSSource.prototype, "createSelectorsAndKeyframes", null);
263__decorate([
264 profile,
265 __metadata("design:type", Function),
266 __metadata("design:paramtypes", [String, String]),
267 __metadata("design:returntype", String)
268], CSSSource, "resolveCSSPathFromURL", null);
269function populateRulesFromImports(nodes, rulesets, keyframes) {
270 const imports = nodes.filter((r) => r.type === 'import');
271 if (!imports.length) {
272 return;
273 }
274 const urlFromImportObject = (importObject) => {
275 const importItem = importObject['import'];
276 const urlMatch = importItem && importItem.match(pattern);
277 return urlMatch && urlMatch[2];
278 };
279 const sourceFromImportObject = (importObject) => importObject['position'] && importObject['position']['source'];
280 const toUrlSourcePair = (importObject) => ({
281 url: urlFromImportObject(importObject),
282 source: sourceFromImportObject(importObject),
283 });
284 const getCssFile = ({ url, source }) => (source ? CSSSource.fromFileImport(url, source) : CSSSource.fromURI(url));
285 const cssFiles = imports
286 .map(toUrlSourcePair)
287 .filter(({ url }) => !!url)
288 .map(getCssFile);
289 for (const cssFile of cssFiles) {
290 if (cssFile) {
291 rulesets.push(...cssFile.selectors);
292 keyframes.push(...cssFile.keyframes);
293 }
294 }
295}
296export function _populateRules(nodes, rulesets, keyframes, mediaQueryString) {
297 for (const node of nodes) {
298 if (isKeyframe(node)) {
299 const keyframeRule = {
300 name: node.name,
301 keyframes: node.keyframes,
302 mediaQueryString: mediaQueryString,
303 };
304 keyframes.push(keyframeRule);
305 }
306 else if (isMedia(node)) {
307 // Media query is composite in the case of nested media queries
308 const compositeMediaQuery = mediaQueryString ? mediaQueryString + MEDIA_QUERY_SEPARATOR + node.media : node.media;
309 _populateRules(node.rules, rulesets, keyframes, compositeMediaQuery);
310 }
311 else if (isRule(node)) {
312 const ruleset = fromAstNode(node);
313 ruleset.mediaQueryString = mediaQueryString;
314 rulesets.push(ruleset);
315 }
316 }
317}
318export function removeTaggedAdditionalCSS(tag) {
319 let selectorsChanged = false;
320 let keyframesChanged = false;
321 let updated = false;
322 for (let i = 0; i < applicationAdditionalSelectors.length; i++) {
323 if (applicationAdditionalSelectors[i].tag === tag) {
324 applicationAdditionalSelectors.splice(i, 1);
325 i--;
326 selectorsChanged = true;
327 }
328 }
329 for (let i = 0; i < applicationAdditionalKeyframes.length; i++) {
330 if (applicationAdditionalKeyframes[i].tag === tag) {
331 applicationAdditionalKeyframes.splice(i, 1);
332 i--;
333 keyframesChanged = true;
334 }
335 }
336 if (selectorsChanged) {
337 mergeCssSelectors();
338 updated = true;
339 }
340 if (keyframesChanged) {
341 mergeCssKeyframes();
342 updated = true;
343 }
344 if (updated) {
345 applicationCssSelectorVersion++;
346 }
347 return updated;
348}
349export function addTaggedAdditionalCSS(cssText, tag) {
350 const { selectors, keyframes } = CSSSource.fromDetect(cssText, undefined);
351 const tagScope = currentScopeTag || (tag && tagToScopeTag.has(tag) && tagToScopeTag.get(tag)) || null;
352 if (tagScope && tag) {
353 tagToScopeTag.set(tag, tagScope);
354 }
355 let selectorsChanged = false;
356 let keyframesChanged = false;
357 let updated = false;
358 if (selectors && selectors.length) {
359 selectorsChanged = true;
360 if (tag != null || tagScope != null) {
361 for (let i = 0, length = selectors.length; i < length; i++) {
362 selectors[i].tag = tag;
363 selectors[i].scopedTag = tagScope;
364 }
365 }
366 applicationAdditionalSelectors.push(...selectors);
367 mergeCssSelectors();
368 updated = true;
369 }
370 if (keyframes && keyframes.length) {
371 keyframesChanged = true;
372 if (tag != null || tagScope != null) {
373 for (let i = 0, length = keyframes.length; i < length; i++) {
374 keyframes[i].tag = tag;
375 keyframes[i].scopedTag = tagScope;
376 }
377 }
378 applicationAdditionalKeyframes.push(...keyframes);
379 mergeCssKeyframes();
380 updated = true;
381 }
382 if (updated) {
383 applicationCssSelectorVersion++;
384 }
385 return updated;
386}
387const onCssChanged = profile('"style-scope".onCssChanged', (args) => {
388 if (args.cssText) {
389 const { selectors, keyframes } = CSSSource.fromSource(args.cssText, args.cssFile);
390 let updated = false;
391 if (selectors) {
392 applicationAdditionalSelectors.push(...selectors);
393 mergeCssSelectors();
394 updated = true;
395 }
396 if (keyframes) {
397 applicationAdditionalKeyframes.push(...keyframes);
398 mergeCssKeyframes();
399 updated = true;
400 }
401 if (updated) {
402 applicationCssSelectorVersion++;
403 }
404 }
405 else if (args.cssFile) {
406 loadCss(args.cssFile, null, null);
407 }
408});
409function onLiveSync(args) {
410 loadCss(Application.getCssFileName(), null, null);
411}
412const loadCss = profile(`"style-scope".loadCss`, (cssModule) => {
413 if (!cssModule) {
414 return;
415 }
416 // safely remove "./" as global CSS should be resolved relative to app folder
417 if (cssModule.startsWith('./')) {
418 cssModule = cssModule.substring(2);
419 }
420 const { selectors, keyframes } = CSSSource.fromURI(cssModule);
421 let updated = false;
422 // Check for existing application css selectors too in case the app is undergoing a live-sync
423 if (selectors.length > 0 || applicationCssSelectors.length > 0) {
424 applicationCssSelectors = selectors;
425 mergeCssSelectors();
426 updated = true;
427 }
428 // Check for existing application css keyframes too in case the app is undergoing a live-sync
429 if (keyframes.length > 0 || applicationCssKeyframes.length > 0) {
430 applicationCssKeyframes = keyframes;
431 mergeCssKeyframes();
432 updated = true;
433 }
434 if (updated) {
435 applicationCssSelectorVersion++;
436 }
437});
438global.NativeScriptGlobals.events.on('cssChanged', onCssChanged);
439global.NativeScriptGlobals.events.on('livesync', onLiveSync);
440// Call to this method is injected in the application in:
441// - no-snapshot - code injected in app.ts by [bundle-config-loader](https://github.com/NativeScript/nativescript-dev-webpack/blob/9b1e34d8ef838006c9b575285c42d2304f5f02b5/bundle-config-loader.ts#L85-L92)
442// - with-snapshot - code injected in snapshot bundle by [NativeScriptSnapshotPlugin](https://github.com/NativeScript/nativescript-dev-webpack/blob/48b26f412fd70c19dc0b9c7763e08e9505a0ae11/plugins/NativeScriptSnapshotPlugin/index.js#L48-L56)
443// Having the app.css loaded in snapshot provides significant boost in startup (when using the ns-theme ~150 ms). However, because app.css is resolved at build-time,
444// when the snapshot is created - there is no way to use file qualifiers or change the name of on app.css
445export const loadAppCSS = profile('"style-scope".loadAppCSS', (args) => {
446 loadCss(args.cssFile, null, null);
447 global.NativeScriptGlobals.events.off('loadAppCss', loadAppCSS);
448});
449if (Application.hasLaunched()) {
450 loadAppCSS({
451 eventName: 'loadAppCss',
452 object: Application,
453 cssFile: Application.getCssFileName(),
454 }, null, null);
455}
456else {
457 global.NativeScriptGlobals.events.on('loadAppCss', loadAppCSS);
458}
459export class CssState {
460 constructor(viewRef) {
461 this.viewRef = viewRef;
462 this._appliedPropertyValues = CssState.emptyPropertyBag;
463 this._onDynamicStateChangeHandler = () => this.updateDynamicState();
464 }
465 /**
466 * Called when a change had occurred that may invalidate the statically matching selectors (class, id, ancestor selectors).
467 * As a result, at some point in time, the selectors matched have to be requerried from the style scope and applied to the view.
468 */
469 onChange() {
470 const view = this.viewRef.get();
471 if (view && view.isLoaded) {
472 this.unsubscribeFromDynamicUpdates();
473 this.updateMatch();
474 this.subscribeForDynamicUpdates();
475 this.updateDynamicState();
476 }
477 else {
478 this._matchInvalid = true;
479 }
480 }
481 isSelectorsLatestVersionApplied() {
482 const view = this.viewRef.get();
483 if (!view) {
484 Trace.write(`isSelectorsLatestVersionApplied returns default value "false" because "this.viewRef" cleared.`, Trace.categories.Style, Trace.messageType.warn);
485 return false;
486 }
487 return this.viewRef.get()._styleScope.getSelectorsVersion() === this._appliedSelectorsVersion;
488 }
489 onLoaded() {
490 if (this._matchInvalid) {
491 this.updateMatch();
492 }
493 this.subscribeForDynamicUpdates();
494 this.updateDynamicState();
495 }
496 onUnloaded() {
497 this.unsubscribeFromDynamicUpdates();
498 this.stopKeyframeAnimations();
499 }
500 updateMatch() {
501 const view = this.viewRef.get();
502 if (view && view._styleScope) {
503 this._match = view._styleScope.matchSelectors(view) ?? CssState.emptyMatch;
504 this._appliedSelectorsVersion = view._styleScope.getSelectorsVersion();
505 }
506 else {
507 this._match = CssState.emptyMatch;
508 }
509 this._matchInvalid = false;
510 }
511 updateDynamicState() {
512 const view = this.viewRef.get();
513 if (!view) {
514 Trace.write(`updateDynamicState not executed to view because ".viewRef" is cleared`, Trace.categories.Style, Trace.messageType.warn);
515 return;
516 }
517 const matchingSelectors = this._match.selectors.filter((sel) => (sel.dynamic ? sel.match(view) : true));
518 // Ideally we should return here if there are no matching selectors, however
519 // if there are property removals, returning here would not remove them
520 // this is seen in STYLE test in automated.
521 // if (!matchingSelectors || matchingSelectors.length === 0) {
522 // return;
523 // }
524 view._batchUpdate(() => {
525 this.stopKeyframeAnimations();
526 this.setPropertyValues(matchingSelectors);
527 this.playKeyframeAnimations(matchingSelectors);
528 });
529 }
530 playKeyframeAnimations(matchingSelectors) {
531 const animations = [];
532 matchingSelectors.forEach((selector) => {
533 const ruleAnimations = selector.ruleset?.[animationsSymbol];
534 if (ruleAnimations) {
535 ensureKeyframeAnimationModule();
536 for (const animationInfo of ruleAnimations) {
537 const animation = keyframeAnimationModule.KeyframeAnimation.keyframeAnimationFromInfo(animationInfo);
538 if (animation) {
539 animations.push(animation);
540 }
541 }
542 }
543 });
544 if ((this._playsKeyframeAnimations = animations.length > 0)) {
545 const view = this.viewRef.get();
546 if (!view) {
547 Trace.write(`KeyframeAnimations cannot play because ".viewRef" is cleared`, Trace.categories.Animation, Trace.messageType.warn);
548 return;
549 }
550 animations.map((animation) => animation.play(view));
551 Object.freeze(animations);
552 this._appliedAnimations = animations;
553 }
554 }
555 stopKeyframeAnimations() {
556 if (!this._playsKeyframeAnimations) {
557 return;
558 }
559 this._appliedAnimations.filter((animation) => animation.isPlaying).forEach((animation) => animation.cancel());
560 this._appliedAnimations = CssState.emptyAnimationArray;
561 const view = this.viewRef.get();
562 if (view) {
563 view.style['keyframe:rotate'] = unsetValue;
564 view.style['keyframe:rotateX'] = unsetValue;
565 view.style['keyframe:rotateY'] = unsetValue;
566 view.style['keyframe:scaleX'] = unsetValue;
567 view.style['keyframe:scaleY'] = unsetValue;
568 view.style['keyframe:translateX'] = unsetValue;
569 view.style['keyframe:translateY'] = unsetValue;
570 view.style['keyframe:backgroundColor'] = unsetValue;
571 view.style['keyframe:opacity'] = unsetValue;
572 }
573 else {
574 Trace.write(`KeyframeAnimations cannot be stopped because ".viewRef" is cleared`, Trace.categories.Animation, Trace.messageType.warn);
575 }
576 this._playsKeyframeAnimations = false;
577 }
578 /**
579 * Calculate the difference between the previously applied property values,
580 * and the new set of property values that have to be applied for the provided selectors.
581 * Apply the values and ensure each property setter is called at most once to avoid excessive change notifications.
582 * @param matchingSelectors
583 */
584 setPropertyValues(matchingSelectors) {
585 const view = this.viewRef.get();
586 if (!view) {
587 Trace.write(`${matchingSelectors} not set to view's property because ".viewRef" is cleared`, Trace.categories.Style, Trace.messageType.warn);
588 return;
589 }
590 const newPropertyValues = new view.style.PropertyBag();
591 matchingSelectors.forEach((selector) => selector.ruleset.declarations.forEach((declaration) => (newPropertyValues[declaration.property] = declaration.value)));
592 const oldProperties = this._appliedPropertyValues;
593 // Update values for the scope's css-variables
594 view.style.resetScopedCssVariables();
595 const valuesToApply = {};
596 const cssExpsProperties = {};
597 const replacementFunc = (g) => g[1].toUpperCase();
598 for (const property in newPropertyValues) {
599 const value = cleanupImportantFlags(newPropertyValues[property], property);
600 const isCssExp = isCssVariableExpression(value) || isCssCalcExpression(value);
601 if (isCssExp) {
602 // we handle css exp separately because css vars must be evaluated first
603 cssExpsProperties[property] = value;
604 continue;
605 }
606 delete oldProperties[property];
607 if (property in oldProperties && oldProperties[property] === value) {
608 // Skip unchanged values
609 continue;
610 }
611 if (isCssVariable(property)) {
612 view.style.setScopedCssVariable(property, value);
613 delete newPropertyValues[property];
614 continue;
615 }
616 valuesToApply[property] = value;
617 }
618 //we need to parse CSS vars first before evaluating css expressions
619 for (const property in cssExpsProperties) {
620 delete oldProperties[property];
621 const value = evaluateCssExpressions(view, property, cssExpsProperties[property]);
622 if (property in oldProperties && oldProperties[property] === value) {
623 // Skip unchanged values
624 continue;
625 }
626 if (value === unsetValue) {
627 delete newPropertyValues[property];
628 }
629 if (isCssVariable(property)) {
630 view.style.setScopedCssVariable(property, value);
631 delete newPropertyValues[property];
632 }
633 valuesToApply[property] = value;
634 }
635 // Unset removed values
636 for (const property in oldProperties) {
637 if (property in view.style) {
638 view.style[`css:${property}`] = unsetValue;
639 }
640 else {
641 const camelCasedProperty = property.replace(kebabCasePattern, replacementFunc);
642 view[camelCasedProperty] = unsetValue;
643 }
644 }
645 // Set new values to the style
646 for (const property in valuesToApply) {
647 const value = valuesToApply[property];
648 try {
649 if (property in view.style) {
650 view.style[`css:${property}`] = value;
651 }
652 else {
653 const camelCasedProperty = property.replace(kebabCasePattern, replacementFunc);
654 view[camelCasedProperty] = value;
655 }
656 }
657 catch (e) {
658 Trace.write(`Failed to apply property [${property}] with value [${value}] to ${view}. ${e.stack}`, Trace.categories.Error, Trace.messageType.error);
659 }
660 }
661 this._appliedPropertyValues = newPropertyValues;
662 }
663 subscribeForDynamicUpdates() {
664 const changeMap = this._match.changeMap;
665 changeMap.forEach((changes, view) => {
666 if (changes.attributes) {
667 changes.attributes.forEach((attribute) => {
668 view.addEventListener(attribute + 'Change', this._onDynamicStateChangeHandler);
669 });
670 }
671 if (changes.pseudoClasses) {
672 changes.pseudoClasses.forEach((pseudoClass) => {
673 const eventName = ':' + pseudoClass;
674 view.addEventListener(':' + pseudoClass, this._onDynamicStateChangeHandler);
675 if (view[eventName]) {
676 view[eventName](+1);
677 }
678 });
679 }
680 });
681 this._appliedChangeMap = changeMap;
682 }
683 unsubscribeFromDynamicUpdates() {
684 this._appliedChangeMap.forEach((changes, view) => {
685 if (changes.attributes) {
686 changes.attributes.forEach((attribute) => {
687 view.removeEventListener(attribute + 'Change', this._onDynamicStateChangeHandler);
688 });
689 }
690 if (changes.pseudoClasses) {
691 changes.pseudoClasses.forEach((pseudoClass) => {
692 const eventName = ':' + pseudoClass;
693 view.removeEventListener(eventName, this._onDynamicStateChangeHandler);
694 if (view[eventName]) {
695 view[eventName](-1);
696 }
697 });
698 }
699 });
700 this._appliedChangeMap = CssState.emptyChangeMap;
701 }
702 toString() {
703 const view = this.viewRef.get();
704 if (!view) {
705 Trace.write(`toString() of CssState cannot execute correctly because ".viewRef" is cleared`, Trace.categories.Animation, Trace.messageType.warn);
706 return '';
707 }
708 return `${view}._cssState`;
709 }
710}
711CssState.emptyChangeMap = Object.freeze(new Map());
712CssState.emptyPropertyBag = {};
713CssState.emptyAnimationArray = Object.freeze([]);
714CssState.emptyMatch = {
715 selectors: [],
716 changeMap: new Map(),
717 addAttribute: () => { },
718 addPseudoClass: () => { },
719 properties: null,
720};
721__decorate([
722 profile,
723 __metadata("design:type", Function),
724 __metadata("design:paramtypes", []),
725 __metadata("design:returntype", void 0)
726], CssState.prototype, "updateMatch", null);
727__decorate([
728 profile,
729 __metadata("design:type", Function),
730 __metadata("design:paramtypes", []),
731 __metadata("design:returntype", void 0)
732], CssState.prototype, "updateDynamicState", null);
733CssState.prototype._appliedChangeMap = CssState.emptyChangeMap;
734CssState.prototype._appliedAnimations = CssState.emptyAnimationArray;
735CssState.prototype._matchInvalid = true;
736export class StyleScope {
737 constructor() {
738 this._css = '';
739 this._localCssSelectors = [];
740 this._localCssKeyframes = [];
741 this._localCssSelectorVersion = 0;
742 this._localCssSelectorsAppliedVersion = 0;
743 this._applicationCssSelectorsAppliedVersion = 0;
744 this._cssFiles = [];
745 }
746 get css() {
747 return this._css;
748 }
749 set css(value) {
750 this.setCss(value);
751 }
752 addCss(cssString, cssFileName) {
753 this.appendCss(cssString, cssFileName);
754 }
755 addCssFile(cssFileName) {
756 this.appendCss(null, cssFileName);
757 }
758 changeCssFile(cssFileName) {
759 if (!cssFileName) {
760 return;
761 }
762 this._cssFiles.push(cssFileName);
763 currentScopeTag = cssFileName;
764 const cssFile = CSSSource.fromURI(cssFileName);
765 currentScopeTag = null;
766 this._css = cssFile.source;
767 this._localCssSelectors = cssFile.selectors;
768 this._localCssKeyframes = cssFile.keyframes;
769 this._localCssSelectorVersion++;
770 this.ensureSelectors();
771 }
772 setCss(cssString, cssFileName) {
773 this._css = cssString;
774 const cssFile = CSSSource.fromSource(cssString, cssFileName);
775 this._localCssSelectors = cssFile.selectors;
776 this._localCssKeyframes = cssFile.keyframes;
777 this._localCssSelectorVersion++;
778 this.ensureSelectors();
779 }
780 appendCss(cssString, cssFileName) {
781 if (!cssString && !cssFileName) {
782 return;
783 }
784 if (cssFileName) {
785 this._cssFiles.push(cssFileName);
786 currentScopeTag = cssFileName;
787 }
788 const cssFile = cssString ? CSSSource.fromSource(cssString, cssFileName) : CSSSource.fromURI(cssFileName);
789 currentScopeTag = null;
790 this._css = this._css + cssFile.source;
791 this._localCssSelectors.push(...cssFile.selectors);
792 this._localCssKeyframes.push(...cssFile.keyframes);
793 this._localCssSelectorVersion++;
794 this.ensureSelectors();
795 }
796 getKeyframeAnimationWithName(animationName) {
797 if (!this._mergedCssKeyframes) {
798 return null;
799 }
800 const keyframeRule = this.findKeyframeRule(animationName);
801 ensureKeyframeAnimationModule();
802 const animation = new keyframeAnimationModule.KeyframeAnimationInfo();
803 ensureCssAnimationParserModule();
804 animation.keyframes = keyframeRule ? cssAnimationParserModule.CssAnimationParser.keyframesArrayFromCSS(keyframeRule.keyframes) : null;
805 return animation;
806 }
807 ensureSelectors() {
808 if (!this.isApplicationCssSelectorsLatestVersionApplied() || !this.isLocalCssSelectorsLatestVersionApplied() || !this._mergedCssSelectors) {
809 this._createSelectors();
810 }
811 return this.getSelectorsVersion();
812 }
813 _increaseApplicationCssSelectorVersion() {
814 applicationCssSelectorVersion++;
815 }
816 isApplicationCssSelectorsLatestVersionApplied() {
817 return this._applicationCssSelectorsAppliedVersion === applicationCssSelectorVersion;
818 }
819 isLocalCssSelectorsLatestVersionApplied() {
820 return this._localCssSelectorsAppliedVersion === this._localCssSelectorVersion;
821 }
822 _createSelectors() {
823 const toMerge = [];
824 const toMergeKeyframes = [];
825 toMerge.push(...mergedApplicationCssSelectors.filter((v) => !v.scopedTag || this._cssFiles.indexOf(v.scopedTag) >= 0));
826 toMergeKeyframes.push(...mergedApplicationCssKeyframes.filter((v) => !v.scopedTag || this._cssFiles.indexOf(v.scopedTag) >= 0));
827 this._applicationCssSelectorsAppliedVersion = applicationCssSelectorVersion;
828 toMerge.push(...this._localCssSelectors);
829 toMergeKeyframes.push(...this._localCssKeyframes);
830 this._localCssSelectorsAppliedVersion = this._localCssSelectorVersion;
831 if (toMerge.length > 0) {
832 this._mergedCssSelectors = toMerge;
833 this._selectorScope = new StyleSheetSelectorScope(this._mergedCssSelectors);
834 }
835 else {
836 this._mergedCssSelectors = null;
837 this._selectorScope = null;
838 }
839 this._mergedCssKeyframes = toMergeKeyframes.length > 0 ? toMergeKeyframes : null;
840 }
841 // HACK: This @profile decorator creates a circular dependency
842 // HACK: because the function parameter type is evaluated with 'typeof'
843 matchSelectors(view) {
844 let match;
845 // should be (view: ViewBase): SelectorsMatch<ViewBase>
846 this.ensureSelectors();
847 if (this._selectorScope) {
848 match = this._selectorScope.query(view);
849 // Make sure to re-apply keyframes to matching selectors as a media query keyframe might be applicable at this point
850 this._applyKeyframesToSelectors(match.selectors);
851 }
852 else {
853 match = null;
854 }
855 return match;
856 }
857 query(node) {
858 this.ensureSelectors();
859 const match = this.matchSelectors(node);
860 return match ? match.selectors : [];
861 }
862 getSelectorsVersion() {
863 // The counters can only go up. So we can return just appVersion + localVersion
864 // The 100000 * appVersion is just for easier debugging
865 return 100000 * this._applicationCssSelectorsAppliedVersion + this._localCssSelectorsAppliedVersion;
866 }
867 _applyKeyframesToSelectors(selectors) {
868 if (!selectors?.length) {
869 return;
870 }
871 for (let i = selectors.length - 1; i >= 0; i--) {
872 const ruleset = selectors[i].ruleset;
873 const animations = ruleset[animationsSymbol];
874 if (animations != null && animations.length) {
875 ensureCssAnimationParserModule();
876 for (const animation of animations) {
877 const keyframeRule = this.findKeyframeRule(animation.name);
878 animation.keyframes = keyframeRule ? cssAnimationParserModule.CssAnimationParser.keyframesArrayFromCSS(keyframeRule.keyframes) : null;
879 }
880 }
881 }
882 }
883 getAnimations(ruleset) {
884 return ruleset[animationsSymbol];
885 }
886 findKeyframeRule(animationName) {
887 if (!this._mergedCssKeyframes) {
888 return null;
889 }
890 // Cache media query results to avoid validations of other identical queries
891 let validatedMediaQueries;
892 // Iterate in reverse order as the last usable keyframe rule matters the most
893 for (let i = this._mergedCssKeyframes.length - 1; i >= 0; i--) {
894 const rule = this._mergedCssKeyframes[i];
895 if (rule.name !== animationName) {
896 continue;
897 }
898 if (!rule.mediaQueryString) {
899 return rule;
900 }
901 if (!validatedMediaQueries) {
902 validatedMediaQueries = [];
903 }
904 const isMatchingAllQueries = matchMediaQueryString(rule.mediaQueryString, validatedMediaQueries);
905 if (isMatchingAllQueries) {
906 return rule;
907 }
908 }
909 return null;
910 }
911}
912__decorate([
913 profile,
914 __metadata("design:type", Function),
915 __metadata("design:paramtypes", [String, Object]),
916 __metadata("design:returntype", void 0)
917], StyleScope.prototype, "setCss", null);
918__decorate([
919 profile,
920 __metadata("design:type", Function),
921 __metadata("design:paramtypes", [String, Object]),
922 __metadata("design:returntype", void 0)
923], StyleScope.prototype, "appendCss", null);
924__decorate([
925 profile,
926 __metadata("design:type", Function),
927 __metadata("design:paramtypes", []),
928 __metadata("design:returntype", void 0)
929], StyleScope.prototype, "_createSelectors", null);
930__decorate([
931 profile,
932 __metadata("design:type", Function),
933 __metadata("design:paramtypes", [Object]),
934 __metadata("design:returntype", SelectorsMatch)
935], StyleScope.prototype, "matchSelectors", null);
936export function resolveFileNameFromUrl(url, appDirectory, fileExists, importSource) {
937 let fileName = typeof url === 'string' ? url.trim() : '';
938 if (fileName.indexOf('~/') === 0) {
939 fileName = fileName.replace('~/', '');
940 }
941 const isAbsolutePath = fileName.indexOf('/') === 0;
942 const absolutePath = isAbsolutePath ? fileName : path.join(appDirectory, fileName);
943 if (fileExists(absolutePath)) {
944 return absolutePath;
945 }
946 if (!isAbsolutePath) {
947 if (fileName[0] === '~' && fileName[1] !== '/' && fileName[1] !== '"') {
948 fileName = fileName.substring(1);
949 }
950 if (importSource) {
951 const importFile = resolveFilePathFromImport(importSource, fileName);
952 if (fileExists(importFile)) {
953 return importFile;
954 }
955 }
956 const external = path.join(appDirectory, 'tns_modules', fileName);
957 if (fileExists(external)) {
958 return external;
959 }
960 }
961 return null;
962}
963function resolveFilePathFromImport(importSource, fileName) {
964 const importSourceParts = importSource.split(path.separator);
965 const fileNameParts = fileName
966 .split(path.separator)
967 // exclude the dot-segment for current directory
968 .filter((p) => !isCurrentDirectory(p));
969 // remove current file name
970 importSourceParts.pop();
971 // remove element in case of dot-segment for parent directory or add file name
972 fileNameParts.forEach((p) => (isParentDirectory(p) ? importSourceParts.pop() : importSourceParts.push(p)));
973 return importSourceParts.join(path.separator);
974}
975export const applyInlineStyle = profile(function applyInlineStyle(view, styleStr) {
976 const localStyle = `local { ${styleStr} }`;
977 const inlineRuleSet = CSSSource.fromSource(localStyle).selectors;
978 // Reset unscoped css-variables
979 view.style.resetUnscopedCssVariables();
980 // Set all the css-variables first, so we can be sure they are up-to-date
981 inlineRuleSet[0].declarations.forEach((d) => {
982 // Use the actual property name so that a local value is set.
983 const property = d.property;
984 if (isCssVariable(property)) {
985 view.style.setUnscopedCssVariable(property, d.value);
986 }
987 });
988 inlineRuleSet[0].declarations.forEach((d) => {
989 // Use the actual property name so that a local value is set.
990 const property = d.property;
991 try {
992 if (isCssVariable(property)) {
993 // Skip css-variables, they have been handled
994 return;
995 }
996 const value = evaluateCssExpressions(view, property, d.value);
997 if (property in view.style) {
998 view.style[property] = value;
999 }
1000 else {
1001 view[property] = value;
1002 }
1003 }
1004 catch (e) {
1005 Trace.write(`Failed to apply property [${d.property}] with value [${d.value}] to ${view}. ${e}`, Trace.categories.Error, Trace.messageType.error);
1006 }
1007 });
1008 // This is needed in case of changes to css-variable or css-calc expressions.
1009 view._onCssStateChange();
1010});
1011function isCurrentDirectory(uriPart) {
1012 return uriPart === '.';
1013}
1014function isParentDirectory(uriPart) {
1015 return uriPart === '..';
1016}
1017function isMedia(node) {
1018 return node.type === 'media';
1019}
1020function isKeyframe(node) {
1021 return node.type === 'keyframes';
1022}
1023function isRule(node) {
1024 return node.type === 'rule';
1025}
1026//# sourceMappingURL=style-scope.js.map
\No newline at end of file