Files
Martin Guillon 17658ed777 fix: only require parsers if need be
this also allows to remove them from bundle.
However this is not the best way. We should use global vars for cssParser so that weback automatically removes the code
2020-11-01 11:10:18 +01:00

939 lines
30 KiB
TypeScript

import { Keyframes } from '../animation/keyframe-animation';
import { ViewBase } from '../core/view-base';
import { View } from '../core/view';
import { unsetValue, _evaluateCssVariableExpression, _evaluateCssCalcExpression, isCssVariable, isCssVariableExpression, isCssCalcExpression } from '../core/properties';
import { SyntaxTree, Keyframes as KeyframesDefinition, Node as CssNode } from '../../css';
import { RuleSet, SelectorsMap, SelectorCore, SelectorsMatch, ChangeMap, fromAstNodes, Node } from './css-selector';
import { Trace } from '../../trace';
import { File, knownFolders, path } from '../../file-system';
import * as application from '../../application';
import { profile } from '../../profiling';
import * as kam from '../animation/keyframe-animation';
let keyframeAnimationModule: typeof kam;
function ensureKeyframeAnimationModule() {
if (!keyframeAnimationModule) {
keyframeAnimationModule = require('../animation/keyframe-animation');
}
}
import * as capm from './css-animation-parser';
import { sanitizeModuleName } from '../builder/module-name-sanitizer';
import { resolveModuleName } from '../../module-name-resolver';
let cssAnimationParserModule: typeof capm;
function ensureCssAnimationParserModule() {
if (!cssAnimationParserModule) {
cssAnimationParserModule = require('./css-animation-parser');
}
}
let parser: 'rework' | 'nativescript' | 'css-tree' = 'css-tree';
try {
const appConfig = require('~/package.json');
if (appConfig) {
if (appConfig.cssParser === 'rework') {
parser = 'rework';
} else if (appConfig.cssParser === 'nativescript') {
parser = 'nativescript';
}
}
} catch (e) {
//
}
/**
* Evaluate css-variable and css-calc expressions
*/
function evaluateCssExpressions(view: ViewBase, property: string, value: string) {
const newValue = _evaluateCssVariableExpression(view, property, value);
if (newValue === 'unset') {
return unsetValue;
}
value = newValue;
try {
value = _evaluateCssCalcExpression(value);
} catch (e) {
Trace.write(`Failed to evaluate css-calc for property [${property}] for expression [${value}] to ${view}. ${e.stack}`, Trace.categories.Error, Trace.messageType.error);
return unsetValue;
}
return value;
}
export function mergeCssSelectors(): void {
applicationCssSelectors = applicationSelectors.slice();
applicationCssSelectors.push.apply(applicationCssSelectors, applicationAdditionalSelectors);
applicationCssSelectorVersion++;
}
let applicationCssSelectors: RuleSet[] = [];
let applicationCssSelectorVersion: number = 0;
let applicationSelectors: RuleSet[] = [];
const applicationAdditionalSelectors: RuleSet[] = [];
const applicationKeyframes: any = {};
const animationsSymbol: symbol = Symbol('animations');
const pattern: RegExp = /('|")(.*?)\1/;
class CSSSource {
private _selectors: RuleSet[] = [];
private constructor(private _ast: SyntaxTree, private _url: string, private _file: string, private _keyframes: KeyframesMap, private _source: string) {
this.parse();
}
public static fromDetect(cssOrAst: any, keyframes: KeyframesMap, fileName?: string): CSSSource {
if (typeof cssOrAst === 'string') {
// raw-loader
return CSSSource.fromSource(cssOrAst, keyframes, fileName);
} else if (typeof cssOrAst === 'object' && cssOrAst.type === 'stylesheet' && cssOrAst.stylesheet && cssOrAst.stylesheet.rules) {
// css-loader
return CSSSource.fromAST(cssOrAst, keyframes, fileName);
} else {
// css2json-loader
return CSSSource.fromSource(cssOrAst.toString(), keyframes, fileName);
}
}
public static fromURI(uri: string, keyframes: KeyframesMap): CSSSource {
// webpack modules require all file paths to be relative to /app folder
const appRelativeUri = CSSSource.pathRelativeToApp(uri);
const sanitizedModuleName = sanitizeModuleName(appRelativeUri);
const resolvedModuleName = resolveModuleName(sanitizedModuleName, 'css');
try {
const cssOrAst = global.loadModule(resolvedModuleName, true);
if (cssOrAst) {
return CSSSource.fromDetect(cssOrAst, keyframes, resolvedModuleName);
}
} catch (e) {
Trace.write(`Could not load CSS from ${uri}: ${e}`, Trace.categories.Error, Trace.messageType.error);
}
return CSSSource.fromFile(appRelativeUri, keyframes);
}
private static pathRelativeToApp(uri: string): string {
if (!uri.startsWith('/')) {
return uri;
}
const appPath = knownFolders.currentApp().path;
if (!uri.startsWith(appPath)) {
Trace.write(`${uri} does not start with ${appPath}`, Trace.categories.Error, Trace.messageType.error);
return uri;
}
const relativeUri = `.${uri.substr(appPath.length)}`;
return relativeUri;
}
public static fromFile(url: string, keyframes: KeyframesMap): CSSSource {
// .scss, .sass, etc. css files in vanilla app are usually compiled to .css so we will try to load a compiled file first.
let cssFileUrl = url.replace(/\..\w+$/, '.css');
if (cssFileUrl !== url) {
const cssFile = CSSSource.resolveCSSPathFromURL(cssFileUrl);
if (cssFile) {
return new CSSSource(undefined, url, cssFile, keyframes, undefined);
}
}
const file = CSSSource.resolveCSSPathFromURL(url);
return new CSSSource(undefined, url, file, keyframes, undefined);
}
public static fromFileImport(url: string, keyframes: KeyframesMap, importSource: string): CSSSource {
const file = CSSSource.resolveCSSPathFromURL(url, importSource);
return new CSSSource(undefined, url, file, keyframes, undefined);
}
@profile
public static resolveCSSPathFromURL(url: string, importSource?: string): string {
const app = knownFolders.currentApp().path;
const file = resolveFileNameFromUrl(url, app, File.exists, importSource);
return file;
}
public static fromSource(source: string, keyframes: KeyframesMap, url?: string): CSSSource {
return new CSSSource(undefined, url, undefined, keyframes, source);
}
public static fromAST(ast: SyntaxTree, keyframes: KeyframesMap, url?: string): CSSSource {
return new CSSSource(ast, url, undefined, keyframes, undefined);
}
get selectors(): RuleSet[] {
return this._selectors;
}
get source(): string {
return this._source;
}
@profile
private load(): void {
const file = File.fromPath(this._file);
this._source = file.readTextSync();
}
@profile
private parse(): void {
try {
if (!this._ast) {
if (!this._source && this._file) {
this.load();
}
// [object Object] check guards against empty app.css file
if (this._source && this.source !== '[object Object]') {
this.parseCSSAst();
}
}
if (this._ast) {
this.createSelectors();
} else {
this._selectors = [];
}
} catch (e) {
Trace.write('Css styling failed: ' + e, Trace.categories.Error, Trace.messageType.error);
this._selectors = [];
}
}
@profile
private parseCSSAst() {
if (this._source) {
switch (parser) {
case 'css-tree':
const cssTreeParse = require('../../css/css-tree-parser').pacssTreeParserse;
this._ast = cssTreeParse(this._source, this._file);
return;
case 'nativescript':
const CSS3Parser = require('../../css/parser').CSS3Parser;
const CSSNativeScript = require('../../css/parser').CSSNativeScript;
const cssparser = new CSS3Parser(this._source);
const stylesheet = cssparser.parseAStylesheet();
const cssNS = new CSSNativeScript();
this._ast = cssNS.parseStylesheet(stylesheet);
return;
case 'rework':
const parseCss = require('../../css').parse;
this._ast = parseCss(this._source, { source: this._file });
return;
}
}
}
@profile
private createSelectors() {
if (this._ast) {
this._selectors = [...this.createSelectorsFromImports(), ...this.createSelectorsFromSyntaxTree()];
}
}
private createSelectorsFromImports(): RuleSet[] {
const imports = this._ast['stylesheet']['rules'].filter((r) => r.type === 'import');
const urlFromImportObject = (importObject) => {
const importItem = importObject['import'] as string;
const urlMatch = importItem && importItem.match(pattern);
return urlMatch && urlMatch[2];
};
const sourceFromImportObject = (importObject) => importObject['position'] && importObject['position']['source'];
const toUrlSourcePair = (importObject) => ({
url: urlFromImportObject(importObject),
source: sourceFromImportObject(importObject),
});
const getCssFile = ({ url, source }) => (source ? CSSSource.fromFileImport(url, this._keyframes, source) : CSSSource.fromURI(url, this._keyframes));
const cssFiles = imports
.map(toUrlSourcePair)
.filter(({ url }) => !!url)
.map(getCssFile);
const selectors = cssFiles.map((file) => (file && file.selectors) || []);
return selectors.reduce((acc, val) => acc.concat(val), []);
}
private createSelectorsFromSyntaxTree(): RuleSet[] {
const nodes = this._ast.stylesheet.rules;
(<KeyframesDefinition[]>nodes.filter(isKeyframe)).forEach((node) => (this._keyframes[node.name] = node));
const rulesets = fromAstNodes(nodes);
if (rulesets && rulesets.length) {
ensureCssAnimationParserModule();
rulesets.forEach((rule) => {
rule[animationsSymbol] = cssAnimationParserModule.CssAnimationParser.keyframeAnimationsFromCSSDeclarations(rule.declarations);
});
}
return rulesets;
}
toString(): string {
return this._file || this._url || '(in-memory)';
}
}
export function removeTaggedAdditionalCSS(tag: String | Number): Boolean {
let changed = false;
for (let i = 0; i < applicationAdditionalSelectors.length; i++) {
if (applicationAdditionalSelectors[i].tag === tag) {
applicationAdditionalSelectors.splice(i, 1);
i--;
changed = true;
}
}
if (changed) {
mergeCssSelectors();
}
return changed;
}
export function addTaggedAdditionalCSS(cssText: string, tag?: string | Number): Boolean {
const parsed: RuleSet[] = CSSSource.fromDetect(cssText, applicationKeyframes, undefined).selectors;
let changed = false;
if (parsed && parsed.length) {
changed = true;
if (tag != null) {
for (let i = 0; i < parsed.length; i++) {
parsed[i].tag = tag;
}
}
applicationAdditionalSelectors.push.apply(applicationAdditionalSelectors, parsed);
mergeCssSelectors();
}
return changed;
}
const onCssChanged = profile('"style-scope".onCssChanged', (args: application.CssChangedEventData) => {
if (args.cssText) {
const parsed = CSSSource.fromSource(args.cssText, applicationKeyframes, args.cssFile).selectors;
if (parsed) {
applicationAdditionalSelectors.push.apply(applicationAdditionalSelectors, parsed);
mergeCssSelectors();
}
} else if (args.cssFile) {
loadCss(args.cssFile, null, null);
}
});
function onLiveSync(args: application.CssChangedEventData): void {
loadCss(application.getCssFileName(), null, null);
}
const loadCss = profile(`"style-scope".loadCss`, (cssModule: string) => {
if (!cssModule) {
return undefined;
}
// safely remove "./" as global CSS should be resolved relative to app folder
if (cssModule.startsWith('./')) {
cssModule = cssModule.substr(2);
}
const result = CSSSource.fromURI(cssModule, applicationKeyframes).selectors;
if (result.length > 0) {
applicationSelectors = result;
mergeCssSelectors();
}
});
global.NativeScriptGlobals.events.on('cssChanged', <any>onCssChanged);
global.NativeScriptGlobals.events.on('livesync', onLiveSync);
// Call to this method is injected in the application in:
// - 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)
// - with-snapshot - code injected in snapshot bundle by [NativeScriptSnapshotPlugin](https://github.com/NativeScript/nativescript-dev-webpack/blob/48b26f412fd70c19dc0b9c7763e08e9505a0ae11/plugins/NativeScriptSnapshotPlugin/index.js#L48-L56)
// 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,
// when the snapshot is created - there is no way to use file qualifiers or change the name of on app.css
export const loadAppCSS = profile('"style-scope".loadAppCSS', (args: application.LoadAppCSSEventData) => {
loadCss(args.cssFile, null, null);
global.NativeScriptGlobals.events.off('loadAppCss', loadAppCSS);
});
if (application.hasLaunched()) {
loadAppCSS(
{
eventName: 'loadAppCss',
object: <any>application,
cssFile: application.getCssFileName(),
},
null,
null
);
} else {
global.NativeScriptGlobals.events.on('loadAppCss', <any>loadAppCSS);
}
export class CssState {
static emptyChangeMap: Readonly<ChangeMap<ViewBase>> = Object.freeze(new Map());
static emptyPropertyBag: Readonly<{}> = Object.freeze({});
static emptyAnimationArray: ReadonlyArray<kam.KeyframeAnimation> = Object.freeze([]);
static emptyMatch: Readonly<SelectorsMatch<ViewBase>> = {
selectors: [],
changeMap: new Map(),
addAttribute: () => {},
addPseudoClass: () => {},
properties: null,
};
_onDynamicStateChangeHandler: () => void;
_appliedChangeMap: Readonly<ChangeMap<ViewBase>>;
_appliedPropertyValues: Readonly<{}>;
_appliedAnimations: ReadonlyArray<kam.KeyframeAnimation>;
_appliedSelectorsVersion: number;
_match: SelectorsMatch<ViewBase>;
_matchInvalid: boolean;
_playsKeyframeAnimations: boolean;
constructor(private viewRef: WeakRef<ViewBase>) {
this._onDynamicStateChangeHandler = () => this.updateDynamicState();
}
/**
* Called when a change had occurred that may invalidate the statically matching selectors (class, id, ancestor selectors).
* As a result, at some point in time, the selectors matched have to be requerried from the style scope and applied to the view.
*/
public onChange(): void {
const view = this.viewRef.get();
if (view && view.isLoaded) {
this.unsubscribeFromDynamicUpdates();
this.updateMatch();
this.subscribeForDynamicUpdates();
this.updateDynamicState();
} else {
this._matchInvalid = true;
}
}
public isSelectorsLatestVersionApplied(): boolean {
const view = this.viewRef.get();
if (!view) {
Trace.write(`isSelectorsLatestVersionApplied returns default value "false" because "this.viewRef" cleared.`, Trace.categories.Style, Trace.messageType.warn);
return false;
}
return this.viewRef.get()._styleScope.getSelectorsVersion() === this._appliedSelectorsVersion;
}
public onLoaded(): void {
if (this._matchInvalid) {
this.updateMatch();
}
this.subscribeForDynamicUpdates();
this.updateDynamicState();
}
public onUnloaded(): void {
this.unsubscribeFromDynamicUpdates();
}
@profile
private updateMatch() {
const view = this.viewRef.get();
if (view && view._styleScope) {
this._match = view._styleScope.matchSelectors(view);
this._appliedSelectorsVersion = view._styleScope.getSelectorsVersion();
} else {
this._match = CssState.emptyMatch;
}
this._matchInvalid = false;
}
@profile
private updateDynamicState(): void {
const view = this.viewRef.get();
if (!view) {
Trace.write(`updateDynamicState not executed to view because ".viewRef" is cleared`, Trace.categories.Style, Trace.messageType.warn);
return;
}
const matchingSelectors = this._match.selectors.filter((sel) => (sel.dynamic ? sel.match(view) : true));
view._batchUpdate(() => {
this.stopKeyframeAnimations();
this.setPropertyValues(matchingSelectors);
this.playKeyframeAnimations(matchingSelectors);
});
}
private playKeyframeAnimations(matchingSelectors: SelectorCore[]): void {
const animations: kam.KeyframeAnimation[] = [];
matchingSelectors.forEach((selector) => {
let ruleAnimations: kam.KeyframeAnimationInfo[] = selector.ruleset[animationsSymbol];
if (ruleAnimations) {
ensureKeyframeAnimationModule();
for (let animationInfo of ruleAnimations) {
let animation = keyframeAnimationModule.KeyframeAnimation.keyframeAnimationFromInfo(animationInfo);
if (animation) {
animations.push(animation);
}
}
}
});
if ((this._playsKeyframeAnimations = animations.length > 0)) {
const view = this.viewRef.get();
if (!view) {
Trace.write(`KeyframeAnimations cannot play because ".viewRef" is cleared`, Trace.categories.Animation, Trace.messageType.warn);
return;
}
animations.map((animation) => animation.play(<View>view));
Object.freeze(animations);
this._appliedAnimations = animations;
}
}
private stopKeyframeAnimations(): void {
if (!this._playsKeyframeAnimations) {
return;
}
this._appliedAnimations.filter((animation) => animation.isPlaying).forEach((animation) => animation.cancel());
this._appliedAnimations = CssState.emptyAnimationArray;
const view = this.viewRef.get();
if (view) {
view.style['keyframe:rotate'] = unsetValue;
view.style['keyframe:rotateX'] = unsetValue;
view.style['keyframe:rotateY'] = unsetValue;
view.style['keyframe:scaleX'] = unsetValue;
view.style['keyframe:scaleY'] = unsetValue;
view.style['keyframe:translateX'] = unsetValue;
view.style['keyframe:translateY'] = unsetValue;
view.style['keyframe:backgroundColor'] = unsetValue;
view.style['keyframe:opacity'] = unsetValue;
} else {
Trace.write(`KeyframeAnimations cannot be stopped because ".viewRef" is cleared`, Trace.categories.Animation, Trace.messageType.warn);
}
this._playsKeyframeAnimations = false;
}
/**
* Calculate the difference between the previously applied property values,
* and the new set of property values that have to be applied for the provided selectors.
* Apply the values and ensure each property setter is called at most once to avoid excessive change notifications.
* @param matchingSelectors
*/
private setPropertyValues(matchingSelectors: SelectorCore[]): void {
const view = this.viewRef.get();
if (!view) {
Trace.write(`${matchingSelectors} not set to view's property because ".viewRef" is cleared`, Trace.categories.Style, Trace.messageType.warn);
return;
}
const newPropertyValues = new view.style.PropertyBag();
matchingSelectors.forEach((selector) => selector.ruleset.declarations.forEach((declaration) => (newPropertyValues[declaration.property] = declaration.value)));
const oldProperties = this._appliedPropertyValues;
let isCssExpressionInUse = false;
// Update values for the scope's css-variables
view.style.resetScopedCssVariables();
for (const property in newPropertyValues) {
const value = newPropertyValues[property];
if (isCssVariable(property)) {
view.style.setScopedCssVariable(property, value);
delete newPropertyValues[property];
continue;
}
isCssExpressionInUse = isCssExpressionInUse || isCssVariableExpression(value) || isCssCalcExpression(value);
}
if (isCssExpressionInUse) {
// Evalute css-expressions to get the latest values.
for (const property in newPropertyValues) {
const value = evaluateCssExpressions(view, property, newPropertyValues[property]);
if (value === unsetValue) {
delete newPropertyValues[property];
continue;
}
newPropertyValues[property] = value;
}
}
// Property values are fully updated, freeze the object to be used for next update.
Object.freeze(newPropertyValues);
// Unset removed values
for (const property in oldProperties) {
if (!(property in newPropertyValues)) {
if (property in view.style) {
view.style[`css:${property}`] = unsetValue;
} else {
// TRICKY: How do we unset local value?
}
}
}
// Set new values to the style
for (const property in newPropertyValues) {
if (oldProperties && property in oldProperties && oldProperties[property] === newPropertyValues[property]) {
// Skip unchanged values
continue;
}
const value = newPropertyValues[property];
try {
if (property in view.style) {
view.style[`css:${property}`] = value;
} else {
const camelCasedProperty = property.replace(/-([a-z])/g, function (g) {
return g[1].toUpperCase();
});
view[camelCasedProperty] = value;
}
} catch (e) {
Trace.write(`Failed to apply property [${property}] with value [${value}] to ${view}. ${e.stack}`, Trace.categories.Error, Trace.messageType.error);
}
}
this._appliedPropertyValues = newPropertyValues;
}
private subscribeForDynamicUpdates(): void {
const changeMap = this._match.changeMap;
changeMap.forEach((changes, view) => {
if (changes.attributes) {
changes.attributes.forEach((attribute) => {
view.addEventListener(attribute + 'Change', this._onDynamicStateChangeHandler);
});
}
if (changes.pseudoClasses) {
changes.pseudoClasses.forEach((pseudoClass) => {
let eventName = ':' + pseudoClass;
view.addEventListener(':' + pseudoClass, this._onDynamicStateChangeHandler);
if (view[eventName]) {
view[eventName](+1);
}
});
}
});
this._appliedChangeMap = changeMap;
}
private unsubscribeFromDynamicUpdates(): void {
this._appliedChangeMap.forEach((changes, view) => {
if (changes.attributes) {
changes.attributes.forEach((attribute) => {
view.removeEventListener(attribute + 'Change', this._onDynamicStateChangeHandler);
});
}
if (changes.pseudoClasses) {
changes.pseudoClasses.forEach((pseudoClass) => {
let eventName = ':' + pseudoClass;
view.removeEventListener(eventName, this._onDynamicStateChangeHandler);
if (view[eventName]) {
view[eventName](-1);
}
});
}
});
this._appliedChangeMap = CssState.emptyChangeMap;
}
toString(): string {
const view = this.viewRef.get();
if (!view) {
Trace.write(`toString() of CssState cannot execute correctly because ".viewRef" is cleared`, Trace.categories.Animation, Trace.messageType.warn);
return '';
}
return `${view}._cssState`;
}
}
CssState.prototype._appliedChangeMap = CssState.emptyChangeMap;
CssState.prototype._appliedPropertyValues = CssState.emptyPropertyBag;
CssState.prototype._appliedAnimations = CssState.emptyAnimationArray;
CssState.prototype._matchInvalid = true;
export class StyleScope {
private _selectors: SelectorsMap<any>;
private _css: string = '';
private _mergedCssSelectors: RuleSet[];
private _localCssSelectors: RuleSet[] = [];
private _localCssSelectorVersion: number = 0;
private _localCssSelectorsAppliedVersion: number = 0;
private _applicationCssSelectorsAppliedVersion: number = 0;
private _keyframes = new Map<string, Keyframes>();
get css(): string {
return this._css;
}
set css(value: string) {
this.setCss(value);
}
public addCss(cssString: string, cssFileName?: string): void {
this.appendCss(cssString, cssFileName);
}
public addCssFile(cssFileName: string): void {
this.appendCss(null, cssFileName);
}
public changeCssFile(cssFileName: string): void {
if (!cssFileName) {
return;
}
const cssSelectors = CSSSource.fromURI(cssFileName, this._keyframes);
this._css = cssSelectors.source;
this._localCssSelectors = cssSelectors.selectors;
this._localCssSelectorVersion++;
this.ensureSelectors();
}
@profile
private setCss(cssString: string, cssFileName?): void {
this._css = cssString;
const cssFile = CSSSource.fromSource(cssString, this._keyframes, cssFileName);
this._localCssSelectors = cssFile.selectors;
this._localCssSelectorVersion++;
this.ensureSelectors();
}
@profile
private appendCss(cssString: string, cssFileName?): void {
if (!cssString && !cssFileName) {
return;
}
let parsedCssSelectors = cssString ? CSSSource.fromSource(cssString, this._keyframes, cssFileName) : CSSSource.fromURI(cssFileName, this._keyframes);
this._css = this._css + parsedCssSelectors.source;
this._localCssSelectors.push.apply(this._localCssSelectors, parsedCssSelectors.selectors);
this._localCssSelectorVersion++;
this.ensureSelectors();
}
public getKeyframeAnimationWithName(animationName: string): kam.KeyframeAnimationInfo {
const cssKeyframes = this._keyframes[animationName];
if (!cssKeyframes) {
return;
}
ensureKeyframeAnimationModule();
const animation = new keyframeAnimationModule.KeyframeAnimationInfo();
ensureCssAnimationParserModule();
animation.keyframes = cssAnimationParserModule.CssAnimationParser.keyframesArrayFromCSS(cssKeyframes.keyframes);
return animation;
}
public ensureSelectors(): number {
if (!this.isApplicationCssSelectorsLatestVersionApplied() || !this.isLocalCssSelectorsLatestVersionApplied() || !this._mergedCssSelectors) {
this._createSelectors();
}
return this.getSelectorsVersion();
}
public _increaseApplicationCssSelectorVersion(): void {
applicationCssSelectorVersion++;
}
public isApplicationCssSelectorsLatestVersionApplied(): boolean {
return this._applicationCssSelectorsAppliedVersion === applicationCssSelectorVersion;
}
public isLocalCssSelectorsLatestVersionApplied(): boolean {
return this._localCssSelectorsAppliedVersion === this._localCssSelectorVersion;
}
@profile
private _createSelectors() {
let toMerge: RuleSet[][] = [];
toMerge.push(applicationCssSelectors);
this._applicationCssSelectorsAppliedVersion = applicationCssSelectorVersion;
toMerge.push(this._localCssSelectors);
this._localCssSelectorsAppliedVersion = this._localCssSelectorVersion;
for (let keyframe in applicationKeyframes) {
this._keyframes[keyframe] = applicationKeyframes[keyframe];
}
if (toMerge.length > 0) {
this._mergedCssSelectors = toMerge.reduce((merged, next) => merged.concat(next || []), []);
this._applyKeyframesOnSelectors();
this._selectors = new SelectorsMap(this._mergedCssSelectors);
}
}
// HACK: This @profile decorator creates a circular dependency
// HACK: because the function parameter type is evaluated with 'typeof'
@profile
public matchSelectors(view: any): SelectorsMatch<ViewBase> {
// should be (view: ViewBase): SelectorsMatch<ViewBase>
this.ensureSelectors();
return this._selectors.query(view);
}
public query(node: Node): SelectorCore[] {
this.ensureSelectors();
return this._selectors.query(node).selectors;
}
getSelectorsVersion() {
// The counters can only go up. So we can return just appVersion + localVersion
// The 100000 * appVersion is just for easier debugging
return 100000 * this._applicationCssSelectorsAppliedVersion + this._localCssSelectorsAppliedVersion;
}
private _applyKeyframesOnSelectors() {
for (let i = this._mergedCssSelectors.length - 1; i >= 0; i--) {
let ruleset = this._mergedCssSelectors[i];
let animations: kam.KeyframeAnimationInfo[] = ruleset[animationsSymbol];
if (animations !== undefined && animations.length) {
ensureCssAnimationParserModule();
for (let animation of animations) {
const cssKeyframe = this._keyframes[animation.name];
if (cssKeyframe !== undefined) {
animation.keyframes = cssAnimationParserModule.CssAnimationParser.keyframesArrayFromCSS(cssKeyframe.keyframes);
}
}
}
}
}
public getAnimations(ruleset: RuleSet): kam.KeyframeAnimationInfo[] {
return ruleset[animationsSymbol];
}
}
type KeyframesMap = Map<string, Keyframes>;
export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (name: string) => boolean, importSource?: string): string {
let fileName: string = typeof url === 'string' ? url.trim() : '';
if (fileName.indexOf('~/') === 0) {
fileName = fileName.replace('~/', '');
}
const isAbsolutePath = fileName.indexOf('/') === 0;
const absolutePath = isAbsolutePath ? fileName : path.join(appDirectory, fileName);
if (fileExists(absolutePath)) {
return absolutePath;
}
if (!isAbsolutePath) {
if (fileName[0] === '~' && fileName[1] !== '/' && fileName[1] !== '"') {
fileName = fileName.substr(1);
}
if (importSource) {
const importFile = resolveFilePathFromImport(importSource, fileName);
if (fileExists(importFile)) {
return importFile;
}
}
const external = path.join(appDirectory, 'tns_modules', fileName);
if (fileExists(external)) {
return external;
}
}
return null;
}
function resolveFilePathFromImport(importSource: string, fileName: string): string {
const importSourceParts = importSource.split(path.separator);
const fileNameParts = fileName
.split(path.separator)
// exclude the dot-segment for current directory
.filter((p) => !isCurrentDirectory(p));
// remove current file name
importSourceParts.pop();
// remove element in case of dot-segment for parent directory or add file name
fileNameParts.forEach((p) => (isParentDirectory(p) ? importSourceParts.pop() : importSourceParts.push(p)));
return importSourceParts.join(path.separator);
}
export const applyInlineStyle = profile(function applyInlineStyle(view: ViewBase, styleStr: string) {
let localStyle = `local { ${styleStr} }`;
let inlineRuleSet = CSSSource.fromSource(localStyle, new Map()).selectors;
// Reset unscoped css-variables
view.style.resetUnscopedCssVariables();
// Set all the css-variables first, so we can be sure they are up-to-date
inlineRuleSet[0].declarations.forEach((d) => {
// Use the actual property name so that a local value is set.
let property = d.property;
if (isCssVariable(property)) {
view.style.setUnscopedCssVariable(property, d.value);
}
});
inlineRuleSet[0].declarations.forEach((d) => {
// Use the actual property name so that a local value is set.
let property = d.property;
try {
if (isCssVariable(property)) {
// Skip css-variables, they have been handled
return;
}
const value = evaluateCssExpressions(view, property, d.value);
if (property in view.style) {
view.style[property] = value;
} else {
view[property] = value;
}
} catch (e) {
Trace.write(`Failed to apply property [${d.property}] with value [${d.value}] to ${view}. ${e}`, Trace.categories.Error, Trace.messageType.error);
}
});
// This is needed in case of changes to css-variable or css-calc expressions.
view._onCssStateChange();
});
function isCurrentDirectory(uriPart: string): boolean {
return uriPart === '.';
}
function isParentDirectory(uriPart: string): boolean {
return uriPart === '..';
}
function isKeyframe(node: CssNode): node is KeyframesDefinition {
return node.type === 'keyframes';
}