Merge branch 'master'

This commit is contained in:
Vasil Trifonov
2020-01-17 15:12:28 +02:00
parent 432a029499
commit b76893801d
3131 changed files with 418263 additions and 185769 deletions

View File

@@ -0,0 +1,296 @@
// Deifinitions.
import { Background as BackgroundDefinition } from "./background";
import { BackgroundRepeat } from "../core/view";
import { LinearGradient } from "./linear-gradient";
// Types.
import { Color } from "../../color";
export class Background implements BackgroundDefinition {
public static default = new Background();
public color: Color;
public image: string | LinearGradient;
public repeat: BackgroundRepeat;
public position: string;
public size: string;
public borderTopColor: Color;
public borderRightColor: Color;
public borderBottomColor: Color;
public borderLeftColor: Color;
public borderTopWidth: number = 0;
public borderRightWidth: number = 0;
public borderBottomWidth: number = 0;
public borderLeftWidth: number = 0;
public borderTopLeftRadius: number = 0;
public borderTopRightRadius: number = 0;
public borderBottomLeftRadius: number = 0;
public borderBottomRightRadius: number = 0;
public clipPath: string;
private clone(): Background {
const clone = new Background();
clone.color = this.color;
clone.image = this.image;
clone.repeat = this.repeat;
clone.position = this.position;
clone.size = this.size;
clone.borderTopColor = this.borderTopColor;
clone.borderRightColor = this.borderRightColor;
clone.borderBottomColor = this.borderBottomColor;
clone.borderLeftColor = this.borderLeftColor;
clone.borderTopWidth = this.borderTopWidth;
clone.borderRightWidth = this.borderRightWidth;
clone.borderBottomWidth = this.borderBottomWidth;
clone.borderLeftWidth = this.borderLeftWidth;
clone.borderTopLeftRadius = this.borderTopLeftRadius;
clone.borderTopRightRadius = this.borderTopRightRadius;
clone.borderBottomRightRadius = this.borderBottomRightRadius;
clone.borderBottomLeftRadius = this.borderBottomLeftRadius;
clone.clipPath = this.clipPath;
return clone;
}
public withColor(value: Color): Background {
const clone = this.clone();
clone.color = value;
return clone;
}
public withImage(value: string | LinearGradient): Background {
const clone = this.clone();
clone.image = value;
return clone;
}
public withRepeat(value: BackgroundRepeat): Background {
const clone = this.clone();
clone.repeat = value;
return clone;
}
public withPosition(value: string): Background {
const clone = this.clone();
clone.position = value;
return clone;
}
public withSize(value: string): Background {
const clone = this.clone();
clone.size = value;
return clone;
}
public withBorderTopColor(value: Color): Background {
const clone = this.clone();
clone.borderTopColor = value;
return clone;
}
public withBorderRightColor(value: Color): Background {
const clone = this.clone();
clone.borderRightColor = value;
return clone;
}
public withBorderBottomColor(value: Color): Background {
const clone = this.clone();
clone.borderBottomColor = value;
return clone;
}
public withBorderLeftColor(value: Color): Background {
const clone = this.clone();
clone.borderLeftColor = value;
return clone;
}
public withBorderTopWidth(value: number): Background {
const clone = this.clone();
clone.borderTopWidth = value;
return clone;
}
public withBorderRightWidth(value: number): Background {
const clone = this.clone();
clone.borderRightWidth = value;
return clone;
}
public withBorderBottomWidth(value: number): Background {
const clone = this.clone();
clone.borderBottomWidth = value;
return clone;
}
public withBorderLeftWidth(value: number): Background {
const clone = this.clone();
clone.borderLeftWidth = value;
return clone;
}
public withBorderTopLeftRadius(value: number): Background {
const clone = this.clone();
clone.borderTopLeftRadius = value;
return clone;
}
public withBorderTopRightRadius(value: number): Background {
const clone = this.clone();
clone.borderTopRightRadius = value;
return clone;
}
public withBorderBottomRightRadius(value: number): Background {
const clone = this.clone();
clone.borderBottomRightRadius = value;
return clone;
}
public withBorderBottomLeftRadius(value: number): Background {
const clone = this.clone();
clone.borderBottomLeftRadius = value;
return clone;
}
public withClipPath(value: string): Background {
const clone = this.clone();
clone.clipPath = value;
return clone;
}
public isEmpty(): boolean {
return !this.color
&& !this.image
&& !this.hasBorderWidth()
&& !this.hasBorderRadius()
&& !this.clipPath;
}
public static equals(value1: Background, value2: Background): boolean {
// both values are falsy
if (!value1 && !value2) {
return true;
}
// only one is falsy
if (!value1 || !value2) {
return false;
}
let imagesEqual = false;
if (value1 instanceof LinearGradient && value2 instanceof LinearGradient) {
imagesEqual = LinearGradient.equals(value1, value2);
} else {
imagesEqual = value1.image === value2.image;
}
return Color.equals(value1.color, value2.color)
&& imagesEqual
&& value1.position === value2.position
&& value1.repeat === value2.repeat
&& value1.size === value2.size
&& Color.equals(value1.borderTopColor, value2.borderTopColor)
&& Color.equals(value1.borderRightColor, value2.borderRightColor)
&& Color.equals(value1.borderBottomColor, value2.borderBottomColor)
&& Color.equals(value1.borderLeftColor, value2.borderLeftColor)
&& value1.borderTopWidth === value2.borderTopWidth
&& value1.borderRightWidth === value2.borderRightWidth
&& value1.borderBottomWidth === value2.borderBottomWidth
&& value1.borderLeftWidth === value2.borderLeftWidth
&& value1.borderTopLeftRadius === value2.borderTopLeftRadius
&& value1.borderTopRightRadius === value2.borderTopRightRadius
&& value1.borderBottomRightRadius === value2.borderBottomRightRadius
&& value1.borderBottomLeftRadius === value2.borderBottomLeftRadius
&& value1.clipPath === value2.clipPath;
}
public hasBorderColor(): boolean {
return !!this.borderTopColor || !!this.borderRightColor || !!this.borderBottomColor || !!this.borderLeftColor;
}
public hasBorderWidth(): boolean {
return this.borderTopWidth > 0
|| this.borderRightWidth > 0
|| this.borderBottomWidth > 0
|| this.borderLeftWidth > 0;
}
public hasBorderRadius(): boolean {
return this.borderTopLeftRadius > 0
|| this.borderTopRightRadius > 0
|| this.borderBottomRightRadius > 0
|| this.borderBottomLeftRadius > 0;
}
public hasUniformBorderColor(): boolean {
return Color.equals(this.borderTopColor, this.borderRightColor)
&& Color.equals(this.borderTopColor, this.borderBottomColor)
&& Color.equals(this.borderTopColor, this.borderLeftColor);
}
public hasUniformBorderWidth(): boolean {
return this.borderTopWidth === this.borderRightWidth
&& this.borderTopWidth === this.borderBottomWidth
&& this.borderTopWidth === this.borderLeftWidth;
}
public hasUniformBorderRadius(): boolean {
return this.borderTopLeftRadius === this.borderTopRightRadius
&& this.borderTopLeftRadius === this.borderBottomRightRadius
&& this.borderTopLeftRadius === this.borderBottomLeftRadius;
}
public hasUniformBorder(): boolean {
return this.hasUniformBorderColor()
&& this.hasUniformBorderWidth()
&& this.hasUniformBorderRadius();
}
public getUniformBorderColor(): Color {
if (this.hasUniformBorderColor()) {
return this.borderTopColor;
}
return undefined;
}
public getUniformBorderWidth(): number {
if (this.hasUniformBorderWidth()) {
return this.borderTopWidth;
}
return 0;
}
public getUniformBorderRadius(): number {
if (this.hasUniformBorderRadius()) {
return this.borderTopLeftRadius;
}
return 0;
}
public toString(): string {
return `isEmpty: ${this.isEmpty()}; color: ${this.color}; image: ${this.image}; repeat: ${this.repeat}; position: ${this.position}; size: ${this.size}; borderTopColor: ${this.borderTopColor}; borderRightColor: ${this.borderRightColor}; borderBottomColor: ${this.borderBottomColor}; borderLeftColor: ${this.borderLeftColor}; borderTopWidth: ${this.borderTopWidth}; borderRightWidth: ${this.borderRightWidth}; borderBottomWidth: ${this.borderBottomWidth}; borderLeftWidth: ${this.borderLeftWidth}; borderTopLeftRadius: ${this.borderTopLeftRadius}; borderTopRightRadius: ${this.borderTopRightRadius}; borderBottomRightRadius: ${this.borderBottomRightRadius}; borderBottomLeftRadius: ${this.borderBottomLeftRadius}; clipPath: ${this.clipPath};`;
}
}

View File

@@ -0,0 +1,298 @@
import { View } from "../core/view";
import { LinearGradient } from "./linear-gradient";
import { isDataURI, isFileOrResourcePath, layout, RESOURCE_PREFIX, FILE_PREFIX } from "../../utils/utils";
import { parse } from "../../css-value";
import { path, knownFolders } from "../../file-system";
import * as application from "../../application";
import { profile } from "../../profiling";
export * from "./background-common";
interface AndroidView {
_cachedDrawable: android.graphics.drawable.Drawable.ConstantState | android.graphics.drawable.Drawable;
}
// TODO: Change this implementation to use
// We are using "ad" here to avoid namespace collision with the global android object
export module ad {
let SDK: number;
function getSDK() {
if (!SDK) {
SDK = android.os.Build.VERSION.SDK_INT;
}
return SDK;
}
function isSetColorFilterOnlyWidget(nativeView: android.view.View): boolean {
return (
nativeView instanceof android.widget.Button ||
(nativeView instanceof androidx.appcompat.widget.Toolbar
&& getSDK() >= 21 // There is an issue with the DrawableContainer which was fixed for API version 21 and above: https://code.google.com/p/android/issues/detail?id=60183
)
);
}
export function onBackgroundOrBorderPropertyChanged(view: View) {
const nativeView = <android.view.View>view.nativeViewProtected;
if (!nativeView) {
return;
}
const background = view.style.backgroundInternal;
let drawable = nativeView.getBackground();
const androidView = <any>view as AndroidView;
// use undefined as not set. getBackground will never return undefined only Drawable or null;
if (androidView._cachedDrawable === undefined && drawable) {
const constantState = drawable.getConstantState();
androidView._cachedDrawable = constantState || drawable;
}
if (isSetColorFilterOnlyWidget(nativeView)
&& drawable
&& !background.hasBorderWidth()
&& !background.hasBorderRadius()
&& !background.clipPath
&& !background.image
&& background.color) {
if (drawable instanceof org.nativescript.widgets.BorderDrawable) {
if (!(androidView._cachedDrawable instanceof android.graphics.drawable.Drawable.ConstantState)) {
return;
}
drawable = androidView._cachedDrawable.newDrawable(nativeView.getResources());
nativeView.setBackground(drawable);
}
const backgroundColor = (<any>drawable).backgroundColor = background.color.android;
drawable.mutate();
drawable.setColorFilter(backgroundColor, android.graphics.PorterDuff.Mode.SRC_IN);
drawable.invalidateSelf(); // Make sure the drawable is invalidated. Android forgets to invalidate it in some cases: toolbar
(<any>drawable).backgroundColor = backgroundColor;
} else if (!background.isEmpty()) {
let backgroundDrawable = drawable as org.nativescript.widgets.BorderDrawable;
if (!(drawable instanceof org.nativescript.widgets.BorderDrawable)) {
backgroundDrawable = new org.nativescript.widgets.BorderDrawable(layout.getDisplayDensity(), view.toString());
refreshBorderDrawable(view, backgroundDrawable);
nativeView.setBackground(backgroundDrawable);
} else {
refreshBorderDrawable(view, backgroundDrawable);
}
} else {
const cachedDrawable = androidView._cachedDrawable;
let defaultDrawable: android.graphics.drawable.Drawable;
if (cachedDrawable instanceof android.graphics.drawable.Drawable.ConstantState) {
defaultDrawable = cachedDrawable.newDrawable(nativeView.getResources());
} else if (cachedDrawable instanceof android.graphics.drawable.Drawable) {
defaultDrawable = cachedDrawable;
} else {
defaultDrawable = null;
}
nativeView.setBackground(defaultDrawable);
// TODO: Do we need to clear the drawable here? Can't we just reuse it again?
androidView._cachedDrawable = undefined;
}
// TODO: Can we move BorderWidths as separate native setter?
// This way we could skip setPadding if borderWidth is not changed.
const leftPadding = Math.ceil(view.effectiveBorderLeftWidth + view.effectivePaddingLeft);
const topPadding = Math.ceil(view.effectiveBorderTopWidth + view.effectivePaddingTop);
const rightPadding = Math.ceil(view.effectiveBorderRightWidth + view.effectivePaddingRight);
const bottomPadding = Math.ceil(view.effectiveBorderBottomWidth + view.effectivePaddingBottom);
nativeView.setPadding(
leftPadding,
topPadding,
rightPadding,
bottomPadding
);
}
}
function fromBase64(source: string): android.graphics.Bitmap {
const bytes = android.util.Base64.decode(source, android.util.Base64.DEFAULT);
return android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
}
function fromGradient(gradient: LinearGradient): org.nativescript.widgets.LinearGradientDefinition {
const colors = Array.create("int", gradient.colorStops.length);
const stops = Array.create("float", gradient.colorStops.length);
let hasStops = false;
gradient.colorStops.forEach((stop, index) => {
colors[index] = stop.color.android;
if (stop.offset) {
stops[index] = stop.offset.value;
hasStops = true;
}
});
const alpha = gradient.angle / (Math.PI * 2);
const startX = Math.pow(
Math.sin(Math.PI * (alpha + 0.75)),
2
);
const startY = Math.pow(
Math.sin(Math.PI * (alpha + 0.5)),
2
);
const endX = Math.pow(
Math.sin(Math.PI * (alpha + 0.25)),
2
);
const endY = Math.pow(
Math.sin(Math.PI * alpha),
2
);
return new org.nativescript.widgets.LinearGradientDefinition(startX, startY, endX, endY, colors, hasStops ? stops : null);
}
const pattern: RegExp = /url\(('|")(.*?)\1\)/;
function refreshBorderDrawable(this: void, view: View, borderDrawable: org.nativescript.widgets.BorderDrawable) {
const nativeView = <android.view.View>view.nativeViewProtected;
const context = nativeView.getContext();
const background = view.style.backgroundInternal;
if (background) {
const backgroundPositionParsedCSSValues = createNativeCSSValueArray(background.position);
const backgroundSizeParsedCSSValues = createNativeCSSValueArray(background.size);
const blackColor = -16777216; //android.graphics.Color.BLACK;
let imageUri: string;
if (background.image && typeof background.image === "string") {
imageUri = background.image;
const match = imageUri.match(pattern);
if (match && match[2]) {
imageUri = match[2];
}
}
let bitmap: android.graphics.Bitmap = null;
if (isDataURI(imageUri)) {
const base64Data = imageUri.split(",")[1];
if (base64Data !== undefined) {
bitmap = fromBase64(base64Data);
imageUri = null;
}
} else if (isFileOrResourcePath(imageUri)) {
if (imageUri.indexOf(RESOURCE_PREFIX) !== 0) {
let fileName = imageUri;
if (fileName.indexOf("~/") === 0) {
fileName = path.join(knownFolders.currentApp().path, fileName.replace("~/", ""));
}
imageUri = FILE_PREFIX + fileName;
}
}
let gradient: org.nativescript.widgets.LinearGradientDefinition = null;
if (background.image && background.image instanceof LinearGradient) {
gradient = fromGradient(background.image);
}
borderDrawable.refresh(
background.borderTopColor ? background.borderTopColor.android : blackColor,
background.borderRightColor ? background.borderRightColor.android : blackColor,
background.borderBottomColor ? background.borderBottomColor.android : blackColor,
background.borderLeftColor ? background.borderLeftColor.android : blackColor,
background.borderTopWidth,
background.borderRightWidth,
background.borderBottomWidth,
background.borderLeftWidth,
background.borderTopLeftRadius,
background.borderTopRightRadius,
background.borderBottomRightRadius,
background.borderBottomLeftRadius,
background.clipPath,
background.color ? background.color.android : 0,
imageUri,
bitmap,
gradient,
context,
background.repeat,
background.position,
backgroundPositionParsedCSSValues,
background.size,
backgroundSizeParsedCSSValues
);
//console.log(`>>> ${borderDrawable.toDebugString()}`);
}
}
function createNativeCSSValueArray(css: string): native.Array<org.nativescript.widgets.CSSValue> {
if (!css) {
return null;
}
const cssValues = parse(css);
const nativeArray = Array.create(org.nativescript.widgets.CSSValue, cssValues.length);
for (let i = 0, length = cssValues.length; i < length; i++) {
nativeArray[i] = new org.nativescript.widgets.CSSValue(
cssValues[i].type,
cssValues[i].string,
cssValues[i].unit,
cssValues[i].value
);
}
return nativeArray;
}
export enum CacheMode {
none,
memory,
diskAndMemory
}
let currentCacheMode: CacheMode;
let imageFetcher: org.nativescript.widgets.image.Fetcher;
export function initImageCache(context: android.content.Context, mode = CacheMode.diskAndMemory, memoryCacheSize: number = 0.25, diskCacheSize: number = 10 * 1024 * 1024): void {
if (currentCacheMode === mode) {
return;
}
currentCacheMode = mode;
if (!imageFetcher) {
imageFetcher = org.nativescript.widgets.image.Fetcher.getInstance(context);
} else {
imageFetcher.clearCache();
}
const params = new org.nativescript.widgets.image.Cache.CacheParams();
params.memoryCacheEnabled = mode !== CacheMode.none;
params.setMemCacheSizePercent(memoryCacheSize); // Set memory cache to % of app memory
params.diskCacheEnabled = mode === CacheMode.diskAndMemory;
params.diskCacheSize = diskCacheSize;
const imageCache = org.nativescript.widgets.image.Cache.getInstance(params);
imageFetcher.addImageCache(imageCache);
imageFetcher.initCache();
}
function onLivesync(args): void {
if (imageFetcher) {
imageFetcher.clearCache();
}
}
application.on("livesync", onLivesync);
application.android.on("activityStarted", profile("initImageCache", args => {
if (!imageFetcher) {
initImageCache(args.activity);
} else {
imageFetcher.initCache();
}
}));
application.android.on("activityStopped", profile("closeImageCache", args => {
if (imageFetcher) {
imageFetcher.closeCache();
}
}));

View File

@@ -0,0 +1,70 @@
/**
* @module "ui/styling/background"
*/ /** */
import { Color } from "../../color";
import { View, BackgroundRepeat, LinearGradient } from "../core/view";
export class Background {
public static default: Background;
public color: Color;
public image: string | LinearGradient;
public repeat: BackgroundRepeat;
public position: string;
public size: string;
public borderTopColor: Color;
public borderRightColor: Color;
public borderBottomColor: Color;
public borderLeftColor: Color;
public borderTopWidth: number;
public borderRightWidth: number;
public borderBottomWidth: number;
public borderLeftWidth: number;
public borderTopLeftRadius: number;
public borderTopRightRadius: number;
public borderBottomRightRadius: number;
public borderBottomLeftRadius: number;
public clipPath: string;
public withColor(value: Color): Background;
public withImage(value: string | LinearGradient): Background;
public withRepeat(value: BackgroundRepeat): Background;
public withPosition(value: string): Background;
public withSize(value: string): Background;
public withBorderTopColor(value: Color): Background;
public withBorderRightColor(value: Color): Background;
public withBorderBottomColor(value: Color): Background;
public withBorderLeftColor(value: Color): Background;
public withBorderTopWidth(value: number): Background;
public withBorderRightWidth(value: number): Background;
public withBorderBottomWidth(value: number): Background;
public withBorderLeftWidth(value: number): Background;
public withBorderTopLeftRadius(value: number): Background;
public withBorderTopRightRadius(value: number): Background;
public withBorderBottomRightRadius(value: number): Background;
public withBorderBottomLeftRadius(value: number): Background;
public withClipPath(value: string): Background;
public isEmpty(): boolean;
public static equals(value1: Background, value2: Background): boolean;
public hasBorderColor(): boolean;
public hasBorderWidth(): boolean;
public hasBorderRadius(): boolean;
public hasUniformBorderColor(): boolean;
public hasUniformBorderWidth(): boolean;
public hasUniformBorderRadius(): boolean;
public hasUniformBorder(): boolean;
public getUniformBorderColor(): Color;
public getUniformBorderWidth(): number;
public getUniformBorderRadius(): number;
}
export module ios {
export function createBackgroundUIColor(view: View, callback: (uiColor: any /* UIColor */) => void, flip?: boolean): void;
}
export module ad {
export function onBackgroundOrBorderPropertyChanged(v: View);
}

View File

@@ -0,0 +1,891 @@
import { ScrollEventData } from "../scroll-view";
import { Background as BackgroundDefinition } from "./background";
import { View, Point } from "../core/view";
import { LinearGradient } from "./linear-gradient";
import { Color } from "../../color";
import { isDataURI, isFileOrResourcePath, layout } from "../../utils/utils";
import { ImageSource } from "../../image-source";
import { CSSValue, parse as cssParse } from "../../css-value";
export * from "./background-common";
interface NativeView extends UIView {
hasNonUniformBorder: boolean;
borderLayer: CALayer;
hasBorderMask: boolean;
borderOriginalMask: CALayer;
topBorderLayer: CALayer;
rightBorderLayer: CALayer;
bottomBorderLayer: CALayer;
leftBorderLayer: CALayer;
gradientLayer: CAGradientLayer;
}
interface Rect {
left: number;
top: number;
right: number;
bottom: number;
}
const clearCGColor = UIColor.clearColor.CGColor;
const symbolUrl = Symbol("backgroundImageUrl");
export module ios {
export function createBackgroundUIColor(view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void {
const background = view.style.backgroundInternal;
const nativeView = <NativeView>view.nativeViewProtected;
if (nativeView.hasNonUniformBorder) {
unsubscribeFromScrollNotifications(view);
clearNonUniformBorders(nativeView);
}
clearGradient(nativeView);
if (background.image instanceof LinearGradient) {
drawGradient(nativeView, background.image);
}
const hasNonUniformBorderWidths = background.hasBorderWidth() && !background.hasUniformBorder();
const hasNonUniformBorderRadiuses = background.hasBorderRadius() && !background.hasUniformBorderRadius();
if (background.hasUniformBorderColor() && (hasNonUniformBorderWidths || hasNonUniformBorderRadiuses)) {
drawUniformColorNonUniformBorders(nativeView, background);
subscribeForScrollNotifications(view);
} else if (background.hasUniformBorder()) {
const layer = nativeView.layer;
const borderColor = background.getUniformBorderColor();
layer.borderColor = !borderColor ? undefined : borderColor.ios.CGColor;
layer.borderWidth = layout.toDeviceIndependentPixels(background.getUniformBorderWidth());
const renderSize = view.getActualSize() || { width: 0, height: 0 };
const cornerRadius = layout.toDeviceIndependentPixels(background.getUniformBorderRadius());
layer.cornerRadius = Math.min(Math.min(renderSize.width / 2, renderSize.height / 2), cornerRadius);
} else {
drawNoRadiusNonUniformBorders(nativeView, background);
subscribeForScrollNotifications(view);
}
// Clip-path should be called after borders are applied.
// It will eventually move them to different layer if uniform.
if (background.clipPath) {
drawClipPath(nativeView, background);
}
if (!background.image || background.image instanceof LinearGradient) {
const uiColor = background.color ? background.color.ios : undefined;
callback(uiColor);
} else {
setUIColorFromImage(view, nativeView, callback, flip);
}
}
}
function onScroll(this: void, args: ScrollEventData): void {
const view = <View>args.object;
const nativeView = view.nativeViewProtected;
if (nativeView instanceof UIScrollView) {
adjustLayersForScrollView(<any>nativeView);
}
}
function adjustLayersForScrollView(nativeView: UIScrollView & NativeView) {
const layer = nativeView.borderLayer;
if (layer instanceof CALayer) {
// Compensates with transition for the background layers for scrolling in ScrollView based controls.
CATransaction.begin();
CATransaction.setValueForKey(kCFBooleanTrue, kCATransactionDisableActions);
const offset = nativeView.contentOffset;
const transform = { a: 1, b: 0, c: 0, d: 1, tx: offset.x, ty: offset.y };
layer.setAffineTransform(transform);
if (nativeView.layer.mask) {
nativeView.layer.mask.setAffineTransform(transform);
}
CATransaction.commit();
}
}
function unsubscribeFromScrollNotifications(view: View) {
if (view.nativeViewProtected instanceof UIScrollView) {
view.off("scroll", onScroll);
}
}
function subscribeForScrollNotifications(view: View) {
if (view.nativeViewProtected instanceof UIScrollView) {
view.on("scroll", onScroll);
adjustLayersForScrollView(<any>view.nativeViewProtected);
}
}
function clearNonUniformBorders(nativeView: NativeView): void {
if (nativeView.borderLayer) {
nativeView.borderLayer.removeFromSuperlayer();
}
if (nativeView.hasBorderMask) {
nativeView.layer.mask = nativeView.borderOriginalMask;
nativeView.hasBorderMask = false;
nativeView.borderOriginalMask = null;
}
if (nativeView.topBorderLayer) {
nativeView.topBorderLayer.removeFromSuperlayer();
}
if (nativeView.rightBorderLayer) {
nativeView.rightBorderLayer.removeFromSuperlayer();
}
if (nativeView.bottomBorderLayer) {
nativeView.bottomBorderLayer.removeFromSuperlayer();
}
if (nativeView.leftBorderLayer) {
nativeView.leftBorderLayer.removeFromSuperlayer();
}
}
const pattern: RegExp = /url\(('|")(.*?)\1\)/;
function setUIColorFromImage(view: View, nativeView: UIView, callback: (uiColor: UIColor) => void, flip?: boolean): void {
const frame = nativeView.frame;
const boundsWidth = view.scaleX ? frame.size.width / view.scaleX : frame.size.width;
const boundsHeight = view.scaleY ? frame.size.height / view.scaleY : frame.size.height;
if (!boundsWidth || !boundsHeight) {
return undefined;
}
const style = view.style;
const background = style.backgroundInternal;
let imageUri = background.image as string;
if (imageUri) {
const match = imageUri.match(pattern);
if (match && match[2]) {
imageUri = match[2];
}
}
let bitmap: UIImage;
if (isDataURI(imageUri)) {
const base64Data = imageUri.split(",")[1];
if (base64Data !== undefined) {
const imageSource = ImageSource.fromBase64Sync(base64Data);
bitmap = imageSource && imageSource.ios;
}
} else if (isFileOrResourcePath(imageUri)) {
const imageSource = ImageSource.fromFileOrResourceSync(imageUri);
bitmap = imageSource && imageSource.ios;
} else if (imageUri.indexOf("http") !== -1) {
style[symbolUrl] = imageUri;
ImageSource.fromUrl(imageUri).then((r) => {
if (style && style[symbolUrl] === imageUri) {
uiColorFromImage(r.ios, view, callback, flip);
}
});
}
uiColorFromImage(bitmap, view, callback, flip);
}
interface BackgroundDrawParams {
repeatX: boolean;
repeatY: boolean;
posX: number;
posY: number;
sizeX?: number;
sizeY?: number;
}
function parsePosition(pos: string): { x: CSSValue, y: CSSValue } {
const values = cssParse(pos);
if (values.length === 2) {
return { x: values[0], y: values[1] };
}
if (values.length === 1 && values[0].type === "ident") {
const val = values[0].string.toLocaleLowerCase();
const center = { type: "ident", string: "center" };
// If you only one keyword is specified, the other value is "center"
if (val === "left" || val === "right") {
return { x: values[0], y: center };
} else if (val === "top" || val === "bottom") {
return { x: center, y: values[0] };
} else if (val === "center") {
return { x: center, y: center };
}
}
return null;
}
function getDrawParams(this: void, image: UIImage, background: BackgroundDefinition, width: number, height: number): BackgroundDrawParams {
if (!image) {
return null;
}
const res: BackgroundDrawParams = {
repeatX: true,
repeatY: true,
posX: 0,
posY: 0,
};
// repeat
if (background.repeat) {
switch (background.repeat.toLowerCase()) {
case "no-repeat":
res.repeatX = false;
res.repeatY = false;
break;
case "repeat-x":
res.repeatY = false;
break;
case "repeat-y":
res.repeatX = false;
break;
}
}
const imageSize = image.size;
let imageWidth = imageSize.width;
let imageHeight = imageSize.height;
// size
const size = background.size;
if (size) {
const values = cssParse(size);
if (values.length === 2) {
const vx = values[0];
const vy = values[1];
if (vx.unit === "%" && vy.unit === "%") {
imageWidth = width * vx.value / 100;
imageHeight = height * vy.value / 100;
res.sizeX = imageWidth;
res.sizeY = imageHeight;
} else if (vx.type === "number" && vy.type === "number" &&
((vx.unit === "px" && vy.unit === "px") || (vx.unit === "" && vy.unit === ""))) {
imageWidth = vx.value;
imageHeight = vy.value;
res.sizeX = imageWidth;
res.sizeY = imageHeight;
}
}
else if (values.length === 1 && values[0].type === "ident") {
let scale = 0;
if (values[0].string === "cover") {
scale = Math.max(width / imageWidth, height / imageHeight);
} else if (values[0].string === "contain") {
scale = Math.min(width / imageWidth, height / imageHeight);
}
if (scale > 0) {
imageWidth *= scale;
imageHeight *= scale;
res.sizeX = imageWidth;
res.sizeY = imageHeight;
}
}
}
// position
const position = background.position;
if (position) {
const v = parsePosition(position);
if (v) {
const spaceX = width - imageWidth;
const spaceY = height - imageHeight;
if (v.x.unit === "%" && v.y.unit === "%") {
res.posX = spaceX * v.x.value / 100;
res.posY = spaceY * v.y.value / 100;
} else if (v.x.type === "number" && v.y.type === "number" &&
((v.x.unit === "px" && v.y.unit === "px") || (v.x.unit === "" && v.y.unit === ""))) {
res.posX = v.x.value;
res.posY = v.y.value;
} else if (v.x.type === "ident" && v.y.type === "ident") {
if (v.x.string.toLowerCase() === "center") {
res.posX = spaceX / 2;
} else if (v.x.string.toLowerCase() === "right") {
res.posX = spaceX;
}
if (v.y.string.toLowerCase() === "center") {
res.posY = spaceY / 2;
} else if (v.y.string.toLowerCase() === "bottom") {
res.posY = spaceY;
}
}
}
}
return res;
}
function uiColorFromImage(img: UIImage, view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void {
const background = view.style.backgroundInternal;
if (!img) {
callback(background.color && background.color.ios);
return;
}
const nativeView = view.nativeViewProtected as UIView;
const frame = nativeView.frame;
const boundsWidth = view.scaleX ? frame.size.width / view.scaleX : frame.size.width;
const boundsHeight = view.scaleY ? frame.size.height / view.scaleY : frame.size.height;
const params = getDrawParams(img, background, boundsWidth, boundsHeight);
if (params.sizeX > 0 && params.sizeY > 0) {
const resizeRect = CGRectMake(0, 0, params.sizeX, params.sizeY);
UIGraphicsBeginImageContextWithOptions(resizeRect.size, false, 0.0);
img.drawInRect(resizeRect);
img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
UIGraphicsBeginImageContextWithOptions(CGSizeFromString(`{${boundsWidth},${boundsHeight}}`), false, 0.0);
const context = UIGraphicsGetCurrentContext();
if (background.color && background.color.ios) {
CGContextSetFillColorWithColor(context, background.color.ios.CGColor);
CGContextFillRect(context, CGRectMake(0, 0, boundsWidth, boundsHeight));
}
if (!params.repeatX && !params.repeatY) {
img.drawAtPoint(CGPointMake(params.posX, params.posY));
} else {
const w = params.repeatX ? boundsWidth : img.size.width;
const h = params.repeatY ? boundsHeight : img.size.height;
CGContextSetPatternPhase(context, CGSizeMake(params.posX, params.posY));
params.posX = params.repeatX ? 0 : params.posX;
params.posY = params.repeatY ? 0 : params.posY;
const patternRect = CGRectMake(params.posX, params.posY, w, h);
img.drawAsPatternInRect(patternRect);
}
const bkgImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (flip) {
const flippedImage = _flipImage(bkgImage);
callback(UIColor.alloc().initWithPatternImage(flippedImage));
} else {
callback(UIColor.alloc().initWithPatternImage(bkgImage));
}
}
// Flipping the default coordinate system
// https://developer.apple.com/library/ios/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/GraphicsDrawingOverview/GraphicsDrawingOverview.html
function _flipImage(originalImage: UIImage): UIImage {
UIGraphicsBeginImageContextWithOptions(originalImage.size, false, 0.0);
const context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0.0, originalImage.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
originalImage.drawInRect(CGRectMake(0, 0, originalImage.size.width, originalImage.size.height));
CGContextRestoreGState(context);
const flippedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return flippedImage;
}
function cssValueToDeviceIndependentPixels(source: string, total: number): number {
source = source.trim();
if (source.indexOf("px") !== -1) {
return layout.toDeviceIndependentPixels(parseFloat(source.replace("px", "")));
} else if (source.indexOf("%") !== -1 && total > 0) {
return (parseFloat(source.replace("%", "")) / 100) * total;
} else {
return parseFloat(source);
}
}
function drawUniformColorNonUniformBorders(nativeView: NativeView, background: BackgroundDefinition) {
const layer = nativeView.layer;
layer.backgroundColor = undefined;
layer.borderColor = undefined;
layer.borderWidth = 0;
layer.cornerRadius = 0;
const { width, height } = layer.bounds.size;
const { x, y } = layer.bounds.origin;
const left = x;
const top = y;
const right = x + width;
const bottom = y + height;
const { min, max } = Math;
const borderTopWidth = max(0, layout.toDeviceIndependentPixels(background.borderTopWidth));
const borderRightWidth = max(0, layout.toDeviceIndependentPixels(background.borderRightWidth));
const borderBottomWidth = max(0, layout.toDeviceIndependentPixels(background.borderBottomWidth));
const borderLeftWidth = max(0, layout.toDeviceIndependentPixels(background.borderLeftWidth));
const borderVWidth = borderTopWidth + borderBottomWidth;
const borderHWidth = borderLeftWidth + borderRightWidth;
const cappedBorderTopWidth = borderTopWidth && borderTopWidth * min(1, height / borderVWidth);
const cappedBorderRightWidth = borderRightWidth && borderRightWidth * min(1, width / borderHWidth);
const cappedBorderBottomWidth = borderBottomWidth && borderBottomWidth * min(1, height / borderVWidth);
const cappedBorderLeftWidth = borderLeftWidth && borderLeftWidth * min(1, width / borderHWidth);
const outerTopLeftRadius = layout.toDeviceIndependentPixels(background.borderTopLeftRadius);
const outerTopRightRadius = layout.toDeviceIndependentPixels(background.borderTopRightRadius);
const outerBottomRightRadius = layout.toDeviceIndependentPixels(background.borderBottomRightRadius);
const outerBottomLeftRadius = layout.toDeviceIndependentPixels(background.borderBottomLeftRadius);
const topRadii = outerTopLeftRadius + outerTopRightRadius;
const rightRadii = outerTopRightRadius + outerBottomRightRadius;
const bottomRadii = outerBottomRightRadius + outerBottomLeftRadius;
const leftRadii = outerBottomLeftRadius + outerTopLeftRadius;
function capRadius(a: number, b: number, c: number): number {
return a && Math.min(a, Math.min(b, c));
}
const cappedOuterTopLeftRadius = capRadius(outerTopLeftRadius, outerTopLeftRadius / topRadii * width, outerTopLeftRadius / leftRadii * height);
const cappedOuterTopRightRadius = capRadius(outerTopRightRadius, outerTopRightRadius / topRadii * width, outerTopRightRadius / rightRadii * height);
const cappedOuterBottomRightRadius = capRadius(outerBottomRightRadius, outerBottomRightRadius / bottomRadii * width, outerBottomRightRadius / rightRadii * height);
const cappedOuterBottomLeftRadius = capRadius(outerBottomLeftRadius, outerBottomLeftRadius / bottomRadii * width, outerBottomLeftRadius / leftRadii * height);
// Outer contour
const clipPath = CGPathCreateMutable();
CGPathMoveToPoint(clipPath, null, left + cappedOuterTopLeftRadius, top);
CGPathAddArcToPoint(clipPath, null, right, top, right, top + cappedOuterTopRightRadius, cappedOuterTopRightRadius);
CGPathAddArcToPoint(clipPath, null, right, bottom, right - cappedOuterBottomRightRadius, bottom, cappedOuterBottomRightRadius);
CGPathAddArcToPoint(clipPath, null, left, bottom, left, bottom - cappedOuterBottomLeftRadius, cappedOuterBottomLeftRadius);
CGPathAddArcToPoint(clipPath, null, left, top, left + cappedOuterTopLeftRadius, top, cappedOuterTopLeftRadius);
CGPathCloseSubpath(clipPath);
nativeView.borderOriginalMask = layer.mask;
const clipShapeLayer = CAShapeLayer.layer();
clipShapeLayer.path = clipPath;
layer.mask = clipShapeLayer;
nativeView.hasBorderMask = true;
if (cappedBorderLeftWidth > 0 || cappedBorderTopWidth > 0 || cappedBorderRightWidth > 0 || cappedBorderBottomWidth > 0) {
const borderPath = CGPathCreateMutable();
CGPathAddRect(borderPath, null, CGRectMake(left, top, width, height));
// Inner contour
if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
CGPathMoveToPoint(borderPath, null, left + cappedOuterTopLeftRadius, top + cappedBorderTopWidth);
} else {
CGPathMoveToPoint(borderPath, null, left, top);
}
if (cappedBorderTopWidth > 0 || cappedBorderRightWidth > 0) {
const innerTopRightWRadius = max(0, cappedOuterTopRightRadius - cappedBorderRightWidth);
const innerTopRightHRadius = max(0, cappedOuterTopRightRadius - cappedBorderTopWidth);
const innerTopRightMaxRadius = max(innerTopRightWRadius, innerTopRightHRadius);
const innerTopRightTransform: any = CGAffineTransformMake(
innerTopRightMaxRadius && innerTopRightWRadius / innerTopRightMaxRadius, 0,
0, innerTopRightMaxRadius && innerTopRightHRadius / innerTopRightMaxRadius,
right - cappedBorderRightWidth - innerTopRightWRadius, top + cappedBorderTopWidth + innerTopRightHRadius
);
CGPathAddArc(borderPath, innerTopRightTransform, 0, 0, innerTopRightMaxRadius, Math.PI * 3 / 2, 0, false);
} else {
CGPathMoveToPoint(borderPath, null, right, top);
}
if (cappedBorderBottomWidth > 0 || cappedBorderRightWidth > 0) {
const innerBottomRightWRadius = max(0, cappedOuterBottomRightRadius - cappedBorderRightWidth);
const innerBottomRightHRadius = max(0, cappedOuterBottomRightRadius - cappedBorderBottomWidth);
const innerBottomRightMaxRadius = max(innerBottomRightWRadius, innerBottomRightHRadius);
const innerBottomRightTransform: any = CGAffineTransformMake(
innerBottomRightMaxRadius && innerBottomRightWRadius / innerBottomRightMaxRadius, 0,
0, innerBottomRightMaxRadius && innerBottomRightHRadius / innerBottomRightMaxRadius,
right - cappedBorderRightWidth - innerBottomRightWRadius, bottom - cappedBorderBottomWidth - innerBottomRightHRadius
);
CGPathAddArc(borderPath, innerBottomRightTransform, 0, 0, innerBottomRightMaxRadius, 0, Math.PI / 2, false);
} else {
CGPathAddLineToPoint(borderPath, null, right, bottom);
}
if (cappedBorderBottomWidth > 0 || cappedBorderLeftWidth > 0) {
const innerBottomLeftWRadius = max(0, cappedOuterBottomLeftRadius - cappedBorderLeftWidth);
const innerBottomLeftHRadius = max(0, cappedOuterBottomLeftRadius - cappedBorderBottomWidth);
const innerBottomLeftMaxRadius = max(innerBottomLeftWRadius, innerBottomLeftHRadius);
const innerBottomLeftTransform: any = CGAffineTransformMake(
innerBottomLeftMaxRadius && innerBottomLeftWRadius / innerBottomLeftMaxRadius, 0,
0, innerBottomLeftMaxRadius && innerBottomLeftHRadius / innerBottomLeftMaxRadius,
left + cappedBorderLeftWidth + innerBottomLeftWRadius, bottom - cappedBorderBottomWidth - innerBottomLeftHRadius
);
CGPathAddArc(borderPath, innerBottomLeftTransform, 0, 0, innerBottomLeftMaxRadius, Math.PI / 2, Math.PI, false);
} else {
CGPathAddLineToPoint(borderPath, null, left, bottom);
}
if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
const innerTopLeftWRadius = max(0, cappedOuterTopLeftRadius - cappedBorderLeftWidth);
const innerTopLeftHRadius = max(0, cappedOuterTopLeftRadius - cappedBorderTopWidth);
const innerTopLeftMaxRadius = max(innerTopLeftWRadius, innerTopLeftHRadius);
const innerTopLeftTransform: any = CGAffineTransformMake(
innerTopLeftMaxRadius && innerTopLeftWRadius / innerTopLeftMaxRadius, 0,
0, innerTopLeftMaxRadius && innerTopLeftHRadius / innerTopLeftMaxRadius,
left + cappedBorderLeftWidth + innerTopLeftWRadius, top + cappedBorderTopWidth + innerTopLeftHRadius
);
CGPathAddArc(borderPath, innerTopLeftTransform, 0, 0, innerTopLeftMaxRadius, Math.PI, Math.PI * 3 / 2, false);
} else {
CGPathAddLineToPoint(borderPath, null, left, top);
}
CGPathCloseSubpath(borderPath);
const borderLayer = CAShapeLayer.layer();
borderLayer.fillColor = background.borderTopColor && background.borderTopColor.ios.CGColor || UIColor.blackColor.CGColor;
borderLayer.fillRule = kCAFillRuleEvenOdd;
borderLayer.path = borderPath;
layer.addSublayer(borderLayer);
nativeView.borderLayer = borderLayer;
}
nativeView.hasNonUniformBorder = true;
}
function drawNoRadiusNonUniformBorders(nativeView: NativeView, background: BackgroundDefinition) {
const borderLayer = CALayer.layer();
nativeView.layer.addSublayer(borderLayer);
nativeView.borderLayer = borderLayer;
borderLayer.borderColor = undefined;
borderLayer.borderWidth = 0;
borderLayer.cornerRadius = 0;
const layerBounds = nativeView.layer.bounds;
const layerOrigin = layerBounds.origin;
const layerSize = layerBounds.size;
const nativeViewLayerBounds = {
left: layerOrigin.x,
top: layerOrigin.y,
bottom: layerSize.height,
right: layerSize.width
};
const top = layout.toDeviceIndependentPixels(background.borderTopWidth);
const right = layout.toDeviceIndependentPixels(background.borderRightWidth);
const bottom = layout.toDeviceIndependentPixels(background.borderBottomWidth);
const left = layout.toDeviceIndependentPixels(background.borderLeftWidth);
const lto: Point = { x: nativeViewLayerBounds.left, y: nativeViewLayerBounds.top }; // left-top-outside
const lti: Point = { x: nativeViewLayerBounds.left + left, y: nativeViewLayerBounds.top + top }; // left-top-inside
const rto: Point = { x: nativeViewLayerBounds.right, y: nativeViewLayerBounds.top }; // right-top-outside
const rti: Point = { x: nativeViewLayerBounds.right - right, y: nativeViewLayerBounds.top + top }; // right-top-inside
const rbo: Point = { x: nativeViewLayerBounds.right, y: nativeViewLayerBounds.bottom }; // right-bottom-outside
const rbi: Point = { x: nativeViewLayerBounds.right - right, y: nativeViewLayerBounds.bottom - bottom }; // right-bottom-inside
const lbo: Point = { x: nativeViewLayerBounds.left, y: nativeViewLayerBounds.bottom }; // left-bottom-outside
const lbi: Point = { x: nativeViewLayerBounds.left + left, y: nativeViewLayerBounds.bottom - bottom }; // left-bottom-inside
let hasNonUniformBorder: boolean;
const borderTopColor = background.borderTopColor;
if (top > 0 && borderTopColor && borderTopColor.ios) {
const topBorderPath = CGPathCreateMutable();
CGPathMoveToPoint(topBorderPath, null, lto.x, lto.y);
CGPathAddLineToPoint(topBorderPath, null, rto.x, rto.y);
CGPathAddLineToPoint(topBorderPath, null, rti.x, rti.y);
CGPathAddLineToPoint(topBorderPath, null, lti.x, lti.y);
CGPathAddLineToPoint(topBorderPath, null, lto.x, lto.y);
const topBorderLayer = CAShapeLayer.layer();
topBorderLayer.fillColor = background.borderTopColor.ios.CGColor;
topBorderLayer.path = topBorderPath;
borderLayer.addSublayer(topBorderLayer);
nativeView.topBorderLayer = topBorderLayer;
hasNonUniformBorder = true;
}
const borderRightColor = background.borderRightColor;
if (right > 0 && borderRightColor && borderRightColor.ios) {
const rightBorderPath = CGPathCreateMutable();
CGPathMoveToPoint(rightBorderPath, null, rto.x, rto.y);
CGPathAddLineToPoint(rightBorderPath, null, rbo.x, rbo.y);
CGPathAddLineToPoint(rightBorderPath, null, rbi.x, rbi.y);
CGPathAddLineToPoint(rightBorderPath, null, rti.x, rti.y);
CGPathAddLineToPoint(rightBorderPath, null, rto.x, rto.y);
const rightBorderLayer = CAShapeLayer.layer();
rightBorderLayer.fillColor = background.borderRightColor.ios.CGColor;
rightBorderLayer.path = rightBorderPath;
borderLayer.addSublayer(rightBorderLayer);
nativeView.rightBorderLayer = rightBorderLayer;
hasNonUniformBorder = true;
}
const borderBottomColor = background.borderBottomColor;
if (bottom > 0 && borderBottomColor && borderBottomColor.ios) {
const bottomBorderPath = CGPathCreateMutable();
CGPathMoveToPoint(bottomBorderPath, null, rbo.x, rbo.y);
CGPathAddLineToPoint(bottomBorderPath, null, lbo.x, lbo.y);
CGPathAddLineToPoint(bottomBorderPath, null, lbi.x, lbi.y);
CGPathAddLineToPoint(bottomBorderPath, null, rbi.x, rbi.y);
CGPathAddLineToPoint(bottomBorderPath, null, rbo.x, rbo.y);
const bottomBorderLayer = CAShapeLayer.layer();
bottomBorderLayer.fillColor = background.borderBottomColor.ios.CGColor;
bottomBorderLayer.path = bottomBorderPath;
borderLayer.addSublayer(bottomBorderLayer);
nativeView.bottomBorderLayer = bottomBorderLayer;
hasNonUniformBorder = true;
}
const borderLeftColor = background.borderLeftColor;
if (left > 0 && borderLeftColor && borderLeftColor.ios) {
const leftBorderPath = CGPathCreateMutable();
CGPathMoveToPoint(leftBorderPath, null, lbo.x, lbo.y);
CGPathAddLineToPoint(leftBorderPath, null, lto.x, lto.y);
CGPathAddLineToPoint(leftBorderPath, null, lti.x, lti.y);
CGPathAddLineToPoint(leftBorderPath, null, lbi.x, lbi.y);
CGPathAddLineToPoint(leftBorderPath, null, lbo.x, lbo.y);
const leftBorderLayer = CAShapeLayer.layer();
leftBorderLayer.fillColor = background.borderLeftColor.ios.CGColor;
leftBorderLayer.path = leftBorderPath;
borderLayer.addSublayer(leftBorderLayer);
nativeView.leftBorderLayer = leftBorderLayer;
hasNonUniformBorder = true;
}
nativeView.hasNonUniformBorder = hasNonUniformBorder;
}
function drawGradient(nativeView: NativeView, gradient: LinearGradient) {
const gradientLayer = CAGradientLayer.layer();
gradientLayer.frame = nativeView.bounds;
nativeView.gradientLayer = gradientLayer;
const iosColors = NSMutableArray.alloc().initWithCapacity(gradient.colorStops.length);
const iosStops = NSMutableArray.alloc<number>().initWithCapacity(gradient.colorStops.length);
let hasStops = false;
gradient.colorStops.forEach(stop => {
iosColors.addObject(stop.color.ios.CGColor);
if (stop.offset) {
iosStops.addObject(stop.offset.value);
hasStops = true;
}
});
gradientLayer.colors = iosColors;
if (hasStops) {
gradientLayer.locations = iosStops;
}
const alpha = gradient.angle / (Math.PI * 2);
const startX = Math.pow(
Math.sin(Math.PI * (alpha + 0.75)),
2
);
const startY = Math.pow(
Math.sin(Math.PI * (alpha + 0.5)),
2
);
const endX = Math.pow(
Math.sin(Math.PI * (alpha + 0.25)),
2
);
const endY = Math.pow(
Math.sin(Math.PI * alpha),
2
);
gradientLayer.startPoint = { x: startX, y: startY };
gradientLayer.endPoint = { x: endX, y: endY };
nativeView.layer.insertSublayerAtIndex(gradientLayer, 0);
}
function clearGradient(nativeView: NativeView): void {
if (nativeView.gradientLayer) {
nativeView.gradientLayer.removeFromSuperlayer();
}
}
function drawClipPath(nativeView: UIView, background: BackgroundDefinition) {
const layer = nativeView.layer;
const layerBounds = layer.bounds;
const layerOrigin = layerBounds.origin;
const layerSize = layerBounds.size;
const bounds = {
left: layerOrigin.x,
top: layerOrigin.y,
bottom: layerSize.height,
right: layerSize.width
};
if (bounds.right === 0 || bounds.bottom === 0) {
return;
}
let path: UIBezierPath;
const clipPath = background.clipPath;
const functionName = clipPath.substring(0, clipPath.indexOf("("));
const value = clipPath.replace(`${functionName}(`, "").replace(")", "");
switch (functionName) {
case "rect":
path = rectPath(value, bounds);
break;
case "inset":
path = insetPath(value, bounds);
break;
case "circle":
path = circlePath(value, bounds);
break;
case "ellipse":
path = ellipsePath(value, bounds);
break;
case "polygon":
path = polygonPath(value, bounds);
break;
}
if (path) {
const shape = CAShapeLayer.layer();
shape.path = path;
layer.mask = shape;
nativeView.clipsToBounds = true;
const borderWidth = background.getUniformBorderWidth();
const borderColor = background.getUniformBorderColor();
if (borderWidth > 0 && borderColor instanceof Color) {
const borderLayer = CAShapeLayer.layer();
borderLayer.path = path;
borderLayer.lineWidth = borderWidth * 2;
borderLayer.strokeColor = borderColor.ios.CGColor;
borderLayer.fillColor = clearCGColor;
borderLayer.frame = nativeView.bounds;
layer.borderColor = undefined;
layer.borderWidth = 0;
layer.addSublayer(borderLayer);
}
}
}
function rectPath(value: string, bounds: Rect): UIBezierPath {
const arr = value.split(/[\s]+/);
const top = cssValueToDeviceIndependentPixels(arr[0], bounds.top);
const right = cssValueToDeviceIndependentPixels(arr[1], bounds.right);
const bottom = cssValueToDeviceIndependentPixels(arr[2], bounds.bottom);
const left = cssValueToDeviceIndependentPixels(arr[3], bounds.left);
return UIBezierPath.bezierPathWithRect(CGRectMake(left, top, right - left, bottom - top)).CGPath;
}
function insetPath(value: string, bounds: Rect): UIBezierPath {
const arr = value.split(/[\s]+/);
let topString: string;
let rightString: string;
let bottomString: string;
let leftString: string;
if (arr.length === 1) {
topString = rightString = bottomString = leftString = arr[0];
}
else if (arr.length === 2) {
topString = bottomString = arr[0];
rightString = leftString = arr[1];
}
else if (arr.length === 3) {
topString = arr[0];
rightString = leftString = arr[1];
bottomString = arr[2];
}
else if (arr.length === 4) {
topString = arr[0];
rightString = arr[1];
bottomString = arr[2];
leftString = arr[3];
}
const top = cssValueToDeviceIndependentPixels(topString, bounds.bottom);
const right = cssValueToDeviceIndependentPixels("100%", bounds.right) - cssValueToDeviceIndependentPixels(rightString, bounds.right);
const bottom = cssValueToDeviceIndependentPixels("100%", bounds.bottom) - cssValueToDeviceIndependentPixels(bottomString, bounds.bottom);
const left = cssValueToDeviceIndependentPixels(leftString, bounds.right);
return UIBezierPath.bezierPathWithRect(CGRectMake(left, top, right - left, bottom - top)).CGPath;
}
function circlePath(value: string, bounds: Rect): UIBezierPath {
const arr = value.split(/[\s]+/);
const radius = cssValueToDeviceIndependentPixels(arr[0], (bounds.right > bounds.bottom ? bounds.bottom : bounds.right) / 2);
const y = cssValueToDeviceIndependentPixels(arr[2], bounds.bottom);
const x = cssValueToDeviceIndependentPixels(arr[3], bounds.right);
return UIBezierPath.bezierPathWithArcCenterRadiusStartAngleEndAngleClockwise(CGPointMake(x, y), radius, 0, 360, true).CGPath;
}
function ellipsePath(value: string, bounds: Rect): UIBezierPath {
const arr = value.split(/[\s]+/);
const rX = cssValueToDeviceIndependentPixels(arr[0], bounds.right);
const rY = cssValueToDeviceIndependentPixels(arr[1], bounds.bottom);
const cX = cssValueToDeviceIndependentPixels(arr[3], bounds.right);
const cY = cssValueToDeviceIndependentPixels(arr[4], bounds.bottom);
const left = cX - rX;
const top = cY - rY;
const width = rX * 2;
const height = rY * 2;
return UIBezierPath.bezierPathWithOvalInRect(CGRectMake(left, top, width, height)).CGPath;
}
function polygonPath(value: string, bounds: Rect): UIBezierPath {
const path = CGPathCreateMutable();
let firstPoint: Point;
const arr = value.split(/[,]+/);
for (let i = 0; i < arr.length; i++) {
const xy = arr[i].trim().split(/[\s]+/);
const point: Point = {
x: cssValueToDeviceIndependentPixels(xy[0], bounds.right),
y: cssValueToDeviceIndependentPixels(xy[1], bounds.bottom)
};
if (!firstPoint) {
firstPoint = point;
CGPathMoveToPoint(path, null, point.x, point.y);
}
CGPathAddLineToPoint(path, null, point.x, point.y);
}
CGPathAddLineToPoint(path, null, firstPoint.x, firstPoint.y);
return path;
}

View File

@@ -0,0 +1,8 @@
/**
* @module "ui/styling/converters"
*/ /** */
import { AnimationCurve } from "../enums/enums";
export function timeConverter(value: string): number;
export function animationTimingFunctionConverter(value: string): any;

View File

@@ -0,0 +1,54 @@
import { AnimationCurve } from "../enums";
const STYLE_CURVE_MAP = Object.freeze({
"ease": AnimationCurve.ease,
"linear": AnimationCurve.linear,
"ease-in": AnimationCurve.easeIn,
"ease-out": AnimationCurve.easeOut,
"ease-in-out": AnimationCurve.easeInOut,
"spring": AnimationCurve.spring,
});
export function timeConverter(value: string): number {
let result = parseFloat(value);
if (value.indexOf("ms") === -1) {
result = result * 1000;
}
return Math.max(0.0, result);
}
export function animationTimingFunctionConverter(value: string): any {
return value ?
STYLE_CURVE_MAP[value] || parseCubicBezierCurve(value) :
AnimationCurve.ease;
}
function parseCubicBezierCurve(value: string) {
const coordsString = /\((.*?)\)/.exec(value);
const coords = coordsString && coordsString[1]
.split(",")
.map(stringToBezieCoords);
if (value.startsWith("cubic-bezier") &&
coordsString &&
coords.length === 4) {
const [x1, x2, y1, y2] = [...coords];
return AnimationCurve.cubicBezier(x1, x2, y1, y2);
} else {
throw new Error(`Invalid value for animation: ${value}`);
}
}
function stringToBezieCoords(value: string): number {
let result = parseFloat(value);
if (result < 0) {
return 0;
} else if (result > 1) {
return 1;
}
return result;
}

View File

@@ -0,0 +1,21 @@
/**
* @module "ui/styling/css-animations-parser"
*/ /** */
import {
KeyframeAnimationInfo,
KeyframeDeclaration,
KeyframeInfo,
UnparsedKeyframe,
} from "../animation/keyframe-animation";
import { timeConverter, animationTimingFunctionConverter } from "./converters";
import { transformConverter } from "./style-properties";
export class CssAnimationParser {
public static keyframeAnimationsFromCSSDeclarations(declarations: KeyframeDeclaration[]): KeyframeAnimationInfo[];
public static keyframesArrayFromCSS(keyframes: UnparsedKeyframe[]): KeyframeInfo[];
}
export function parseKeyframeDeclarations(unparsedKeyframeDeclarations: KeyframeDeclaration[]) : KeyframeDeclaration[];

View File

@@ -0,0 +1,147 @@
import { CssAnimationProperty } from "../core/properties";
import {
KeyframeAnimationInfo,
KeyframeDeclaration,
KeyframeInfo,
UnparsedKeyframe,
} from "../animation/keyframe-animation";
import { timeConverter, animationTimingFunctionConverter } from "../styling/converters";
import { transformConverter } from "../styling/style-properties";
const ANIMATION_PROPERTY_HANDLERS = Object.freeze({
"animation-name": (info, value) => info.name = value,
"animation-duration": (info, value) => info.duration = timeConverter(value),
"animation-delay": (info, value) => info.delay = timeConverter(value),
"animation-timing-function": (info, value) => info.curve = animationTimingFunctionConverter(value),
"animation-iteration-count": (info, value) => info.iterations = value === "infinite" ? Number.MAX_VALUE : parseFloat(value),
"animation-direction": (info, value) => info.isReverse = value === "reverse",
"animation-fill-mode": (info, value) => info.isForwards = value === "forwards"
});
export class CssAnimationParser {
public static keyframeAnimationsFromCSSDeclarations(declarations: KeyframeDeclaration[])
: KeyframeAnimationInfo[] {
if (declarations === null || declarations === undefined) {
return undefined;
}
let animations = new Array<KeyframeAnimationInfo>();
let animationInfo: KeyframeAnimationInfo = undefined;
declarations.forEach(({ property, value }) => {
if (property === "animation") {
keyframeAnimationsFromCSSProperty(value, animations);
} else {
const propertyHandler = ANIMATION_PROPERTY_HANDLERS[property];
if (propertyHandler) {
if (animationInfo === undefined) {
animationInfo = new KeyframeAnimationInfo();
animations.push(animationInfo);
}
propertyHandler(animationInfo, value);
}
}
});
return animations.length === 0 ? undefined : animations;
}
public static keyframesArrayFromCSS(keyframes: UnparsedKeyframe[]): KeyframeInfo[] {
let parsedKeyframes = new Array<KeyframeInfo>();
for (let keyframe of keyframes) {
let declarations = parseKeyframeDeclarations(keyframe.declarations);
for (let time of keyframe.values) {
if (time === "from") {
time = 0;
} else if (time === "to") {
time = 1;
} else {
time = parseFloat(time) / 100;
if (time < 0) {
time = 0;
}
if (time > 100) {
time = 100;
}
}
let current = parsedKeyframes[time];
if (current === undefined) {
current = <KeyframeInfo>{};
current.duration = time;
parsedKeyframes[time] = current;
}
for (let declaration of keyframe.declarations) {
if (declaration.property === "animation-timing-function") {
current.curve = animationTimingFunctionConverter(declaration.value);
}
}
current.declarations = declarations;
}
}
let array = new Array();
for (let parsedKeyframe in parsedKeyframes) {
array.push(parsedKeyframes[parsedKeyframe]);
}
array.sort(function (a, b) { return a.duration - b.duration; });
return array;
}
}
function keyframeAnimationsFromCSSProperty(value: any, animations: KeyframeAnimationInfo[]) {
if (typeof value === "string") {
let values = value.split(/[,]+/);
for (let parsedValue of values) {
let animationInfo = new KeyframeAnimationInfo();
let arr = (<string>parsedValue).trim().split(/[ ]+/);
if (arr.length > 0) {
animationInfo.name = arr[0];
}
if (arr.length > 1) {
animationInfo.duration = timeConverter(arr[1]);
}
if (arr.length > 2) {
animationInfo.curve = animationTimingFunctionConverter(arr[2]);
}
if (arr.length > 3) {
animationInfo.delay = timeConverter(arr[3]);
}
if (arr.length > 4) {
animationInfo.iterations = parseInt(arr[4]);
}
if (arr.length > 5) {
animationInfo.isReverse = arr[4] === "reverse";
}
if (arr.length > 6) {
animationInfo.isForwards = arr[5] === "forwards";
}
if (arr.length > 7) {
throw new Error("Invalid value for animation: " + value);
}
animations.push(animationInfo);
}
}
}
export function parseKeyframeDeclarations(unparsedKeyframeDeclarations: KeyframeDeclaration[]): KeyframeDeclaration[] {
const declarations = unparsedKeyframeDeclarations
.reduce((declarations, { property: unparsedProperty, value: unparsedValue }) => {
const property = CssAnimationProperty._getByCssName(unparsedProperty);
if (typeof unparsedProperty === "string" && property && property._valueConverter) {
declarations[property.name] = property._valueConverter(<string>unparsedValue);
} else if (typeof unparsedValue === "string" && unparsedProperty === "transform") {
const transformations = transformConverter(unparsedValue);
Object.assign(declarations, transformations);
}
return declarations;
}, {});
return Object.keys(declarations).map(property => ({ property, value: declarations[property] }));
}

View File

@@ -0,0 +1,83 @@
/**
* @module "ui/styling/css-selector"
*/ /** */
import { Node as ParserNode } from "../../../css";
/**
* An interface describing the shape of a type on which the selectors may apply.
* Note, the ui/core/view.View implements Node.
*/
export interface Node {
parent?: Node;
id?: string;
cssType?: string;
cssClasses?: Set<string>;
cssPseudoClasses?: Set<string>;
getChildIndex?(node: Node): number
getChildAt?(index: number): Node
}
export interface Declaration {
property: string;
value: string;
}
export interface SelectorCore {
/**
* Dynamic selectors depend on attributes and pseudo classes.
*/
dynamic: boolean;
match(node: Node): boolean;
ruleset: RuleSet;
}
export class RuleSet {
/**
* Gets the selectors in this ruleset's selector group.
*/
selectors: SelectorCore[];
/**
* Gets the key-value list of declarations for the ruleset.
*/
declarations: Declaration[];
/**
* Optional Tag for rules
**/
tag: string | Number;
}
export class SelectorsMap {
constructor(rules: RuleSet[]);
/**
* Get a list of selectors that are likely to match the node.
*/
query<T extends Node>(node: T): SelectorsMatch<T>;
}
export type ChangeMap<T extends Node> = Map<T, Changes>;
export interface Changes {
attributes?: Set<string>;
pseudoClasses?: Set<string>;
}
export class SelectorsMatch<T extends Node> {
/**
* Gets the static selectors that match the queried node and the dynamic selectors that may potentially match the queried node.
*/
selectors: SelectorCore[];
/**
* Gets a map of nodes to attributes and pseudo classes, that may affect the state of the dynamic
*/
changeMap: ChangeMap<T>;
}
export function fromAstNodes(astRules: ParserNode[]): RuleSet[];
export function createSelector(sel: string): SelectorCore;

View File

@@ -0,0 +1,587 @@
import { Node, Declaration, Changes, ChangeMap } from ".";
import { isNullOrUndefined } from "../../../utils/types";
import { escapeRegexSymbols } from "../../../utils/utils-common";
import * as cssParser from "../../../css";
import * as parser from "../../../css/parser";
const enum Specificity {
Inline = 0x01000000,
Id = 0x00010000,
Attribute = 0x00000100,
Class = 0x00000100,
PseudoClass = 0x00000100,
Type = 0x00000001,
Universal = 0x00000000,
Invalid = 0x00000000
}
const enum Rarity {
Invalid = 4,
Id = 3,
Class = 2,
Type = 1,
PseudoClass = 0,
Attribute = 0,
Universal = 0,
Inline = 0
}
interface LookupSorter {
sortById(id: string, sel: SelectorCore);
sortByClass(cssClass: string, sel: SelectorCore);
sortByType(cssType: string, sel: SelectorCore);
sortAsUniversal(sel: SelectorCore);
}
namespace Match {
/**
* Depends on attributes or pseudoclasses state;
*/
export const Dynamic = true;
/**
* Depends only on the tree structure.
*/
export const Static = false;
}
function getNodeDirectSibling(node): null | Node {
if (!node.parent || !node.parent.getChildIndex || !node.parent.getChildAt) {
return null;
}
const nodeIndex = node.parent.getChildIndex(node);
if (nodeIndex === 0) {
return null;
}
return node.parent.getChildAt(nodeIndex - 1);
}
function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic: boolean = false): ClassDecorator {
return cls => {
cls.prototype.specificity = specificity;
cls.prototype.rarity = rarity;
cls.prototype.combinator = undefined;
cls.prototype.dynamic = dynamic;
return cls;
};
}
declare type Combinator = "+" | ">" | "~" | " ";
@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
export abstract class SelectorCore {
public specificity: number;
public rarity: Rarity;
public combinator: Combinator;
public ruleset: RuleSet;
public dynamic: boolean;
public abstract match(node: Node): boolean;
/**
* If the selector is static returns if it matches the node.
* If the selector is dynamic returns if it may match the node, and accumulates any changes that may affect its state.
*/
public abstract accumulateChanges(node: Node, map: ChangeAccumulator): boolean;
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortAsUniversal(base || this); }
}
export abstract class SimpleSelector extends SelectorCore {
public accumulateChanges(node: Node, map?: ChangeAccumulator): boolean {
if (!this.dynamic) {
return this.match(node);
} else if (this.mayMatch(node)) {
this.trackChanges(node, map);
return true;
}
return false;
}
public mayMatch(node: Node): boolean { return this.match(node); }
public trackChanges(node: Node, map: ChangeAccumulator): void {
// No-op, silence the tslint 'block is empty'.
// Some derived classes (dynamic) will actually fill the map with stuff here, some (static) won't do anything.
}
}
function wrap(text: string): string {
return text ? ` ${text} ` : "";
}
@SelectorProperties(Specificity.Invalid, Rarity.Invalid, Match.Static)
export class InvalidSelector extends SimpleSelector {
constructor(public e: Error) { super(); }
public toString(): string { return `<error: ${this.e}>`; }
public match(node: Node): boolean { return false; }
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
// No-op, silence the tslint 'block is empty'.
// It feels like tslint has problems with simple polymorphism...
// This selector is invalid and will never match so we won't bother sorting it to further appear in queries.
}
}
@SelectorProperties(Specificity.Universal, Rarity.Universal, Match.Static)
export class UniversalSelector extends SimpleSelector {
public toString(): string { return `*${wrap(this.combinator)}`; }
public match(node: Node): boolean { return true; }
}
@SelectorProperties(Specificity.Id, Rarity.Id, Match.Static)
export class IdSelector extends SimpleSelector {
constructor(public id: string) { super(); }
public toString(): string { return `#${this.id}${wrap(this.combinator)}`; }
public match(node: Node): boolean { return node.id === this.id; }
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortById(this.id, base || this); }
}
@SelectorProperties(Specificity.Type, Rarity.Type, Match.Static)
export class TypeSelector extends SimpleSelector {
constructor(public cssType: string) { super(); }
public toString(): string { return `${this.cssType}${wrap(this.combinator)}`; }
public match(node: Node): boolean { return node.cssType === this.cssType; }
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortByType(this.cssType, base || this); }
}
@SelectorProperties(Specificity.Class, Rarity.Class, Match.Static)
export class ClassSelector extends SimpleSelector {
constructor(public cssClass: string) { super(); }
public toString(): string { return `.${this.cssClass}${wrap(this.combinator)}`; }
public match(node: Node): boolean { return node.cssClasses && node.cssClasses.has(this.cssClass); }
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void { sorter.sortByClass(this.cssClass, base || this); }
}
declare type AttributeTest = "=" | "^=" | "$=" | "*=" | "=" | "~=" | "|=";
@SelectorProperties(Specificity.Attribute, Rarity.Attribute, Match.Dynamic)
export class AttributeSelector extends SimpleSelector {
constructor(public attribute: string, public test?: AttributeTest, public value?: string) {
super();
if (!test) {
// HasAttribute
this.match = node => !isNullOrUndefined(node[attribute]);
return;
}
if (!value) {
this.match = node => false;
}
let escapedValue = escapeRegexSymbols(value);
let regexp: RegExp = null;
switch (test) {
case "^=": // PrefixMatch
regexp = new RegExp("^" + escapedValue);
break;
case "$=": // SuffixMatch
regexp = new RegExp(escapedValue + "$");
break;
case "*=": // SubstringMatch
regexp = new RegExp(escapedValue);
break;
case "=": // Equals
regexp = new RegExp("^" + escapedValue + "$");
break;
case "~=": // Includes
if (/\s/.test(value)) {
this.match = node => false;
return;
}
regexp = new RegExp("(^|\\s)" + escapedValue + "(\\s|$)");
break;
case "|=": // DashMatch
regexp = new RegExp("^" + escapedValue + "(-|$)");
break;
}
if (regexp) {
this.match = node => regexp.test(node[attribute] + "");
return;
} else {
this.match = node => false;
return;
}
}
public toString(): string { return `[${this.attribute}${wrap(this.test)}${(this.test && this.value) || ""}]${wrap(this.combinator)}`; }
public match(node: Node): boolean { return false; }
public mayMatch(node: Node): boolean { return true; }
public trackChanges(node: Node, map: ChangeAccumulator): void { map.addAttribute(node, this.attribute); }
}
@SelectorProperties(Specificity.PseudoClass, Rarity.PseudoClass, Match.Dynamic)
export class PseudoClassSelector extends SimpleSelector {
constructor(public cssPseudoClass: string) { super(); }
public toString(): string { return `:${this.cssPseudoClass}${wrap(this.combinator)}`; }
public match(node: Node): boolean { return node.cssPseudoClasses && node.cssPseudoClasses.has(this.cssPseudoClass); }
public mayMatch(node: Node): boolean { return true; }
public trackChanges(node: Node, map: ChangeAccumulator): void { map.addPseudoClass(node, this.cssPseudoClass); }
}
export class SimpleSelectorSequence extends SimpleSelector {
private head: SimpleSelector;
constructor(public selectors: SimpleSelector[]) {
super();
this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0);
this.head = this.selectors.reduce((prev, curr) => !prev || (curr.rarity > prev.rarity) ? curr : prev, null);
this.dynamic = selectors.some(sel => sel.dynamic);
}
public toString(): string { return `${this.selectors.join("")}${wrap(this.combinator)}`; }
public match(node: Node): boolean { return this.selectors.every(sel => sel.match(node)); }
public mayMatch(node: Node): boolean {
return this.selectors.every(sel => sel.mayMatch(node));
}
public trackChanges(node, map): void {
this.selectors.forEach(sel => sel.trackChanges(node, map));
}
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
this.head.lookupSort(sorter, base || this);
}
}
export class Selector extends SelectorCore {
// Grouped by ancestor combinators, then by direct child combinators.
private groups: Selector.ChildGroup[];
private last: SelectorCore;
constructor(public selectors: SimpleSelector[]) {
super();
const supportedCombinator = [undefined, " ", ">", "+"];
let siblingGroup: SimpleSelector[];
let lastGroup: SimpleSelector[][];
let groups: SimpleSelector[][][] = [];
selectors.reverse().forEach(sel => {
if (supportedCombinator.indexOf(sel.combinator) === -1) {
throw new Error(`Unsupported combinator "${sel.combinator}".`);
}
if (sel.combinator === undefined || sel.combinator === " ") {
groups.push(lastGroup = [siblingGroup = []]);
}
if (sel.combinator === ">") {
lastGroup.push(siblingGroup = []);
}
siblingGroup.push(sel);
});
this.groups = groups.map(g =>
new Selector.ChildGroup(g.map(sg =>
new Selector.SiblingGroup(sg)
))
);
this.last = selectors[0];
this.specificity = selectors.reduce((sum, sel) => sel.specificity + sum, 0);
this.dynamic = selectors.some(sel => sel.dynamic);
}
public toString(): string { return this.selectors.join(""); }
public match(node: Node): boolean {
return this.groups.every((group, i) => {
if (i === 0) {
node = group.match(node);
return !!node;
} else {
let ancestor = node;
while (ancestor = ancestor.parent) {
if (node = group.match(ancestor)) {
return true;
}
}
return false;
}
});
}
public lookupSort(sorter: LookupSorter, base?: SelectorCore): void {
this.last.lookupSort(sorter, this);
}
public accumulateChanges(node: Node, map?: ChangeAccumulator): boolean {
if (!this.dynamic) {
return this.match(node);
}
let bounds: Selector.Bound[] = [];
let mayMatch = this.groups.every((group, i) => {
if (i === 0) {
let nextNode = group.mayMatch(node);
bounds.push({ left: node, right: node });
node = nextNode;
return !!node;
} else {
let ancestor = node;
while (ancestor = ancestor.parent) {
let nextNode = group.mayMatch(ancestor);
if (nextNode) {
bounds.push({ left: ancestor, right: null });
node = nextNode;
return true;
}
}
return false;
}
});
// Calculating the right bounds for each selectors won't save much
if (!mayMatch) {
return false;
}
if (!map) {
return mayMatch;
}
for (let i = 0; i < this.groups.length; i++) {
let group = this.groups[i];
if (!group.dynamic) {
continue;
}
let bound = bounds[i];
let node = bound.left;
do {
if (group.mayMatch(node)) {
group.trackChanges(node, map);
}
} while ((node !== bound.right) && (node = node.parent));
}
return mayMatch;
}
}
export namespace Selector {
// Non-spec. Selector sequences are grouped by ancestor then by child combinators for easier backtracking.
export class ChildGroup {
public dynamic: boolean;
constructor(private selectors: SiblingGroup[]) {
this.dynamic = selectors.some(sel => sel.dynamic);
}
public match(node: Node): Node {
return this.selectors.every((sel, i) => (i === 0 ? node : node = node.parent) && !!sel.match(node)) ? node : null;
}
public mayMatch(node: Node): Node {
return this.selectors.every((sel, i) => (i === 0 ? node : node = node.parent) && !!sel.mayMatch(node)) ? node : null;
}
public trackChanges(node: Node, map: ChangeAccumulator) {
this.selectors.forEach((sel, i) => (i === 0 ? node : node = node.parent) && sel.trackChanges(node, map));
}
}
export class SiblingGroup {
public dynamic: boolean;
constructor(private selectors: SimpleSelector[]) {
this.dynamic = selectors.some(sel => sel.dynamic);
}
public match(node: Node): Node {
return this.selectors.every((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && sel.match(node)) ? node : null;
}
public mayMatch(node: Node): Node {
return this.selectors.every((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && sel.mayMatch(node)) ? node : null;
}
public trackChanges(node: Node, map: ChangeAccumulator) {
this.selectors.forEach((sel, i) => (i === 0 ? node : node = getNodeDirectSibling(node)) && sel.trackChanges(node, map));
}
}
export interface Bound {
left: Node;
right: Node;
}
}
export class RuleSet {
constructor(public selectors: SelectorCore[], private declarations: Declaration[]) {
this.selectors.forEach(sel => sel.ruleset = this);
}
public toString(): string { return `${this.selectors.join(", ")} {${this.declarations.map((d, i) => `${i === 0 ? " " : ""}${d.property}: ${d.value}`).join("; ")} }`; }
public lookupSort(sorter: LookupSorter): void { this.selectors.forEach(sel => sel.lookupSort(sorter)); }
}
export function fromAstNodes(astRules: cssParser.Node[]): RuleSet[] {
return (<cssParser.Rule[]>astRules.filter(isRule)).map(rule => {
let declarations = rule.declarations.filter(isDeclaration).map(createDeclaration);
let selectors = rule.selectors.map(createSelector);
let ruleset = new RuleSet(selectors, declarations);
return ruleset;
});
}
function createDeclaration(decl: cssParser.Declaration): any {
return { property: decl.property.toLowerCase(), value: decl.value };
}
function createSimpleSelectorFromAst(ast: parser.SimpleSelector): SimpleSelector {
switch (ast.type) {
case "*": return new UniversalSelector();
case "#": return new IdSelector(ast.identifier);
case "": return new TypeSelector(ast.identifier.replace(/-/, "").toLowerCase());
case ".": return new ClassSelector(ast.identifier);
case ":": return new PseudoClassSelector(ast.identifier);
case "[]": return ast.test ? new AttributeSelector(ast.property, ast.test, ast.value) : new AttributeSelector(ast.property);
}
}
function createSimpleSelectorSequenceFromAst(ast: parser.SimpleSelectorSequence): SimpleSelectorSequence | SimpleSelector {
if (ast.length === 0) {
return new InvalidSelector(new Error("Empty simple selector sequence."));
} else if (ast.length === 1) {
return createSimpleSelectorFromAst(ast[0]);
} else {
return new SimpleSelectorSequence(ast.map(createSimpleSelectorFromAst));
}
}
function createSelectorFromAst(ast: parser.Selector): SimpleSelector | SimpleSelectorSequence | Selector {
if (ast.length === 0) {
return new InvalidSelector(new Error("Empty selector."));
} else if (ast.length === 1) {
return createSimpleSelectorSequenceFromAst(ast[0][0]);
} else {
let simpleSelectorSequences = [];
let simpleSelectorSequence: SimpleSelectorSequence | SimpleSelector;
let combinator: parser.Combinator;
for (let i = 0; i < ast.length; i++) {
simpleSelectorSequence = createSimpleSelectorSequenceFromAst(<parser.SimpleSelectorSequence>ast[i][0]);
combinator = <parser.Combinator>ast[i][1];
if (combinator) {
simpleSelectorSequence.combinator = combinator;
}
simpleSelectorSequences.push(simpleSelectorSequence);
}
return new Selector(simpleSelectorSequences);
}
}
export function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence | Selector {
try {
let parsedSelector = parser.parseSelector(sel);
if (!parsedSelector) {
return new InvalidSelector(new Error("Empty selector"));
}
return createSelectorFromAst(parsedSelector.value);
} catch (e) {
return new InvalidSelector(e);
}
}
function isRule(node: cssParser.Node): node is cssParser.Rule {
return node.type === "rule";
}
function isDeclaration(node: cssParser.Node): node is cssParser.Declaration {
return node.type === "declaration";
}
interface SelectorInDocument {
pos: number;
sel: SelectorCore;
}
interface SelectorMap {
[key: string]: SelectorInDocument[];
}
export class SelectorsMap<T extends Node> implements LookupSorter {
private id: SelectorMap = {};
private class: SelectorMap = {};
private type: SelectorMap = {};
private universal: SelectorInDocument[] = [];
private position = 0;
constructor(rulesets: RuleSet[]) {
rulesets.forEach(rule => rule.lookupSort(this));
}
query(node: T): SelectorsMatch<T> {
let selectorClasses = [
this.universal,
this.id[node.id],
this.type[node.cssType]
];
if (node.cssClasses) {
node.cssClasses.forEach(c => selectorClasses.push(this.class[c]));
}
let selectors = selectorClasses
.filter(arr => !!arr)
.reduce((cur, next) => cur.concat(next), []);
let selectorsMatch = new SelectorsMatch<T>();
selectorsMatch.selectors = selectors
.filter(sel => sel.sel.accumulateChanges(node, selectorsMatch))
.sort((a, b) => a.sel.specificity - b.sel.specificity || a.pos - b.pos)
.map(docSel => docSel.sel);
return selectorsMatch;
}
sortById(id: string, sel: SelectorCore): void { this.addToMap(this.id, id, sel); }
sortByClass(cssClass: string, sel: SelectorCore): void {
this.addToMap(this.class, cssClass, sel);
}
sortByType(cssType: string, sel: SelectorCore): void {
this.addToMap(this.type, cssType, sel);
}
sortAsUniversal(sel: SelectorCore): void { this.universal.push(this.makeDocSelector(sel)); }
private addToMap(map: SelectorMap, head: string, sel: SelectorCore): void {
this.position++;
let list = map[head];
if (list) {
list.push(this.makeDocSelector(sel));
} else {
map[head] = [this.makeDocSelector(sel)];
}
}
private makeDocSelector(sel: SelectorCore): SelectorInDocument {
return { sel, pos: this.position++ };
}
}
interface ChangeAccumulator {
addAttribute(node: Node, attribute: string): void;
addPseudoClass(node: Node, pseudoClass: string): void;
}
export class SelectorsMatch<T extends Node> implements ChangeAccumulator {
public changeMap: ChangeMap<T> = new Map<T, Changes>();
public selectors;
public addAttribute(node: T, attribute: string): void {
let deps: Changes = this.properties(node);
if (!deps.attributes) {
deps.attributes = new Set();
}
deps.attributes.add(attribute);
}
public addPseudoClass(node: T, pseudoClass: string): void {
let deps: Changes = this.properties(node);
if (!deps.pseudoClasses) {
deps.pseudoClasses = new Set();
}
deps.pseudoClasses.add(pseudoClass);
}
private properties(node: T): Changes {
let set = this.changeMap.get(node);
if (!set) {
this.changeMap.set(node, set = {});
}
return set;
}
}

View File

@@ -0,0 +1,5 @@
{
"name": "css-selector",
"main": "css-selector",
"types": "css-selector.d.ts"
}

View File

@@ -0,0 +1,172 @@
import { Font as FontDefinition } from "./font";
import { ParsedFont } from "./font-interfaces";
import { makeValidator, makeParser } from "../core/properties";
export * from "./font-interfaces";
export abstract class Font implements FontDefinition {
public static default = undefined;
get isItalic(): boolean {
return this.fontStyle === FontStyle.ITALIC;
}
get isBold(): boolean {
return this.fontWeight === FontWeight.SEMI_BOLD ||
this.fontWeight === FontWeight.BOLD ||
this.fontWeight === "700" ||
this.fontWeight === FontWeight.EXTRA_BOLD ||
this.fontWeight === FontWeight.BLACK;
}
protected constructor(
public readonly fontFamily: string,
public readonly fontSize: number,
public readonly fontStyle: FontStyle,
public readonly fontWeight: FontWeight) {
}
public abstract getAndroidTypeface(): any /* android.graphics.Typeface */;
public abstract getUIFont(defaultFont: any /* UIFont */): any /* UIFont */;
public abstract withFontFamily(family: string): Font;
public abstract withFontStyle(style: string): Font;
public abstract withFontWeight(weight: string): Font;
public abstract withFontSize(size: number): Font;
public static equals(value1: Font, value2: Font): boolean {
// both values are falsy
if (!value1 && !value2) {
return true;
}
// only one is falsy
if (!value1 || !value2) {
return false;
}
return value1.fontFamily === value2.fontFamily &&
value1.fontSize === value2.fontSize &&
value1.fontStyle === value2.fontStyle &&
value1.fontWeight === value2.fontWeight;
}
}
export type FontStyle = "normal" | "italic";
export namespace FontStyle {
export const NORMAL: "normal" = "normal";
export const ITALIC: "italic" = "italic";
export const isValid = makeValidator<FontStyle>(NORMAL, ITALIC);
export const parse = makeParser<FontStyle>(isValid);
}
export type FontWeight = "100" | "200" | "300" | "normal" | "400" | "500" | "600" | "bold" | "700" | "800" | "900";
export namespace FontWeight {
export const THIN: "100" = "100";
export const EXTRA_LIGHT: "200" = "200";
export const LIGHT: "300" = "300";
export const NORMAL: "normal" = "normal";
export const MEDIUM: "500" = "500";
export const SEMI_BOLD: "600" = "600";
export const BOLD: "bold" = "bold";
export const EXTRA_BOLD: "800" = "800";
export const BLACK: "900" = "900";
export const isValid = makeValidator<FontWeight>(THIN, EXTRA_LIGHT, LIGHT, NORMAL, "400", MEDIUM, SEMI_BOLD, BOLD, "700", EXTRA_BOLD, BLACK);
export const parse = makeParser<FontStyle>(isValid);
}
export function parseFontFamily(value: string): Array<string> {
const result = new Array<string>();
if (!value) {
return result;
}
const split = value.split(",");
for (let i = 0; i < split.length; i++) {
let str = split[i].trim().replace(/['"]+/g, "");
if (str) {
result.push(str);
}
}
return result;
}
export module genericFontFamilies {
export const serif = "serif";
export const sansSerif = "sans-serif";
export const monospace = "monospace";
export const system = "system";
}
const styles = new Set();
[
FontStyle.NORMAL,
FontStyle.ITALIC
].forEach((val, i, a) => styles.add(val));
// http://www.w3schools.com/cssref/pr_font_weight.asp
//- normal(same as 400)
//- bold(same as 700)
//- 100(Thin) (API16 -thin)
//- 200(Extra Light / Ultra Light) (API16 -light)
//- 300(Light) (API16 -light)
//- 400(Normal)
//- 500(Medium) (API21 -medium)
//- 600(Semi Bold / Demi Bold) (API21 -medium)
//- 700(Bold) (API16 -bold)
//- 800(Extra Bold / Ultra Bold) (API16 -bold)
//- 900(Black / Heavy) (API21 -black)
const weights = new Set();
[
FontWeight.THIN,
FontWeight.EXTRA_LIGHT,
FontWeight.LIGHT,
FontWeight.NORMAL,
"400",
FontWeight.MEDIUM,
FontWeight.SEMI_BOLD,
FontWeight.BOLD,
"700",
FontWeight.EXTRA_BOLD,
FontWeight.BLACK
].forEach((val, i, a) => weights.add(val));
export function parseFont(fontValue: string): ParsedFont {
let result: ParsedFont = {
fontStyle: "normal",
fontVariant: "normal",
fontWeight: "normal"
};
const parts = fontValue.split(/\s+/);
let part: string;
while (part = parts.shift()) {
if (part === "normal") {
// nothing to do here
}
else if (part === "small-caps") {
// The only supported font variant in shorthand font
result.fontVariant = part;
}
else if (styles.has(part)) {
result.fontStyle = <any>part;
}
else if (weights.has(part)) {
result.fontWeight = <any>part;
}
else if (!result.fontSize) {
let sizes = part.split("/");
result.fontSize = sizes[0];
result.lineHeight = sizes.length > 1 ? sizes[1] : undefined;
}
else {
result.fontFamily = part;
if (parts.length) {
result.fontFamily += " " + parts.join(" ");
}
break;
}
}
return result;
}

View File

@@ -0,0 +1,12 @@
export type FontStyle = "normal" | "italic";
export type FontWeight = "100" | "200" | "300" | "normal" | "400" | "500" | "600" | "bold" | "700" | "800" | "900";
export interface ParsedFont {
fontStyle?: FontStyle;
fontVariant?: string;
fontWeight?: FontWeight;
lineHeight?: string;
fontSize?: string;
fontFamily?: string;
}

View File

@@ -0,0 +1,163 @@
import { Font as FontBase, parseFontFamily, genericFontFamilies, FontWeight } from "./font-common";
import { isEnabled as traceEnabled, write as traceWrite, categories as traceCategories, messageType as traceMessageType } from "../../trace";
import * as application from "../../application";
import * as fs from "../../file-system";
export * from "./font-common";
const FONTS_BASE_PATH = "/fonts/";
const typefaceCache = new Map<string, android.graphics.Typeface>();
let appAssets: android.content.res.AssetManager;
export class Font extends FontBase {
public static default = new Font(undefined, undefined, "normal", "normal");
private _typeface: android.graphics.Typeface;
constructor(family: string, size: number, style: "normal" | "italic", weight: FontWeight) {
super(family, size, style, weight);
}
public withFontFamily(family: string): Font {
return new Font(family, this.fontSize, this.fontStyle, this.fontWeight);
}
public withFontStyle(style: "normal" | "italic"): Font {
return new Font(this.fontFamily, this.fontSize, style, this.fontWeight);
}
public withFontWeight(weight: FontWeight): Font {
return new Font(this.fontFamily, this.fontSize, this.fontStyle, weight);
}
public withFontSize(size: number): Font {
return new Font(this.fontFamily, size, this.fontStyle, this.fontWeight);
}
public getAndroidTypeface(): android.graphics.Typeface {
if (!this._typeface) {
this._typeface = createTypeface(this);
}
return this._typeface;
}
public getUIFont(defaultFont: UIFont): UIFont {
return undefined;
}
}
function loadFontFromFile(fontFamily: string): android.graphics.Typeface {
appAssets = appAssets || application.android.context.getAssets();
if (!appAssets) {
return null;
}
let result = typefaceCache.get(fontFamily);
// Check for undefined explicitly as null mean we tried to load the font, but failed.
if (result === undefined) {
result = null;
let fontAssetPath: string;
const basePath = fs.path.join(fs.knownFolders.currentApp().path, "fonts", fontFamily);
if (fs.File.exists(basePath + ".ttf")) {
fontAssetPath = FONTS_BASE_PATH + fontFamily + ".ttf";
}
else if (fs.File.exists(basePath + ".otf")) {
fontAssetPath = FONTS_BASE_PATH + fontFamily + ".otf";
}
else {
if (traceEnabled()) {
traceWrite("Could not find font file for " + fontFamily, traceCategories.Error, traceMessageType.error);
}
}
if (fontAssetPath) {
try {
fontAssetPath = fs.path.join(fs.knownFolders.currentApp().path, fontAssetPath);
result = android.graphics.Typeface.createFromFile(fontAssetPath);
} catch (e) {
if (traceEnabled()) {
traceWrite("Error loading font asset: " + fontAssetPath, traceCategories.Error, traceMessageType.error);
}
}
}
typefaceCache.set(fontFamily, result);
}
return result;
}
function createTypeface(font: Font): android.graphics.Typeface {
let fontStyle = 0;
if (font.isBold) {
fontStyle |= android.graphics.Typeface.BOLD;
}
if (font.isItalic) {
fontStyle |= android.graphics.Typeface.ITALIC;
}
//http://stackoverflow.com/questions/19691530/valid-values-for-androidfontfamily-and-what-they-map-to
const fonts = parseFontFamily(font.fontFamily);
let result = null;
for (let i = 0; i < fonts.length; i++) {
switch (fonts[i].toLowerCase()) {
case genericFontFamilies.serif:
result = android.graphics.Typeface.create("serif" + getFontWeightSuffix(font.fontWeight), fontStyle);
break;
case genericFontFamilies.sansSerif:
case genericFontFamilies.system:
result = android.graphics.Typeface.create("sans-serif" + getFontWeightSuffix(font.fontWeight), fontStyle);
break;
case genericFontFamilies.monospace:
result = android.graphics.Typeface.create("monospace" + getFontWeightSuffix(font.fontWeight), fontStyle);
break;
default:
result = loadFontFromFile(fonts[i]);
if (result && fontStyle) {
result = android.graphics.Typeface.create(result, fontStyle);
}
break;
}
if (result) {
// Found the font!
break;
}
}
if (!result) {
result = android.graphics.Typeface.create("sans-serif" + getFontWeightSuffix(font.fontWeight), fontStyle);
}
return result;
}
function getFontWeightSuffix(fontWeight: FontWeight): string {
switch (fontWeight) {
case FontWeight.THIN:
return android.os.Build.VERSION.SDK_INT >= 16 ? "-thin" : "";
case FontWeight.EXTRA_LIGHT:
case FontWeight.LIGHT:
return android.os.Build.VERSION.SDK_INT >= 16 ? "-light" : "";
case FontWeight.NORMAL:
case "400":
case undefined:
case null:
return "";
case FontWeight.MEDIUM:
case FontWeight.SEMI_BOLD:
return android.os.Build.VERSION.SDK_INT >= 21 ? "-medium" : "";
case FontWeight.BOLD:
case "700":
case FontWeight.EXTRA_BOLD:
return "";
case FontWeight.BLACK:
return android.os.Build.VERSION.SDK_INT >= 21 ? "-black" : "";
default:
throw new Error(`Invalid font weight: "${fontWeight}"`);
}
}

65
nativescript-core/ui/styling/font.d.ts vendored Normal file
View File

@@ -0,0 +1,65 @@
/**
* @module "ui/styling/font"
*/ /** */
export class Font {
public static default: Font;
public fontFamily: string;
public fontStyle: FontStyle;
public fontWeight: FontWeight;
public fontSize: number;
public isBold: boolean;
public isItalic: boolean;
constructor(family: string, size: number, style: FontStyle, weight: FontWeight);
public getAndroidTypeface(): any /* android.graphics.Typeface */;
public getUIFont(defaultFont: any /* UIFont */): any /* UIFont */;
public withFontFamily(family: string): Font;
public withFontStyle(style: string): Font;
public withFontWeight(weight: string): Font;
public withFontSize(size: number): Font;
public static equals(value1: Font, value2: Font): boolean;
}
export type FontStyle = "normal" | "italic";
export namespace FontStyle {
export const NORMAL: "normal";
export const ITALIC: "italic";
export function isValid(value: any): boolean;
export function parse(value: string): FontStyle;
}
export type FontWeight = "100" | "200" | "300" | "normal" | "400" | "500" | "600" | "bold" | "700" | "800" | "900";
export namespace FontWeight {
export const THIN: "100";
export const EXTRA_LIGHT: "200";
export const LIGHT: "300";
export const NORMAL: "normal";
export const MEDIUM: "500";
export const SEMI_BOLD: "600";
export const BOLD: "bold";
export const EXTRA_BOLD: "800";
export const BLACK: "900";
export function isValid(value: any): boolean;
export function parse(value: string): FontWeight;
}
export interface ParsedFont {
fontStyle?: FontStyle;
fontVariant?: string;
fontWeight?: FontWeight,
lineHeight?: string,
fontSize?: string,
fontFamily?: string
}
export function parseFont(fontValue: string): ParsedFont;
export module ios {
export function registerFont(fontFile: string);
}

View File

@@ -0,0 +1,226 @@
import { Font as FontBase, parseFontFamily, genericFontFamilies, FontStyle, FontWeight } from "./font-common";
import { isEnabled as traceEnabled, write as traceWrite, categories as traceCategories, messageType as traceMessageType } from "../../trace";
import { device } from "../../platform";
import * as fs from "../../file-system";
export * from "./font-common";
const EMULATE_OBLIQUE = true;
const OBLIQUE_TRANSFORM = CGAffineTransformMake(1, 0, 0.2, 1, 0, 0);
const DEFAULT_SERIF = "Times New Roman";
const DEFAULT_MONOSPACE = "Courier New";
const SUPPORT_FONT_WEIGHTS = parseFloat(device.osVersion) >= 10.0;
export class Font extends FontBase {
public static default = new Font(undefined, undefined, FontStyle.NORMAL, FontWeight.NORMAL);
private _uiFont: UIFont;
constructor(family: string, size: number, style: FontStyle, weight: FontWeight) {
super(family, size, style, weight);
}
public withFontFamily(family: string): Font {
return new Font(family, this.fontSize, this.fontStyle, this.fontWeight);
}
public withFontStyle(style: FontStyle): Font {
return new Font(this.fontFamily, this.fontSize, style, this.fontWeight);
}
public withFontWeight(weight: FontWeight): Font {
return new Font(this.fontFamily, this.fontSize, this.fontStyle, weight);
}
public withFontSize(size: number): Font {
return new Font(this.fontFamily, size, this.fontStyle, this.fontWeight);
}
public getUIFont(defaultFont: UIFont): UIFont {
if (!this._uiFont) {
this._uiFont = createUIFont(this, defaultFont);
}
return this._uiFont;
}
public getAndroidTypeface(): android.graphics.Typeface {
return undefined;
}
}
function getFontFamilyRespectingGenericFonts(fontFamily: string): string {
if (!fontFamily) {
return fontFamily;
}
switch (fontFamily.toLowerCase()) {
case genericFontFamilies.serif:
return DEFAULT_SERIF;
case genericFontFamilies.monospace:
return DEFAULT_MONOSPACE;
default:
return fontFamily;
}
}
function shouldUseSystemFont(fontFamily: string) {
return !fontFamily ||
fontFamily === genericFontFamilies.sansSerif ||
fontFamily === genericFontFamilies.system;
}
function getNativeFontWeight(fontWeight: FontWeight): number {
switch (fontWeight) {
case FontWeight.THIN:
return UIFontWeightUltraLight;
case FontWeight.EXTRA_LIGHT:
return UIFontWeightThin;
case FontWeight.LIGHT:
return UIFontWeightLight;
case FontWeight.NORMAL:
case "400":
case undefined:
case null:
return UIFontWeightRegular;
case FontWeight.MEDIUM:
return UIFontWeightMedium;
case FontWeight.SEMI_BOLD:
return UIFontWeightSemibold;
case FontWeight.BOLD:
case "700":
return UIFontWeightBold;
case FontWeight.EXTRA_BOLD:
return UIFontWeightHeavy;
case FontWeight.BLACK:
return UIFontWeightBlack;
default:
throw new Error(`Invalid font weight: "${fontWeight}"`);
}
}
function getSystemFont(size: number, nativeWeight: number, italic: boolean, symbolicTraits: number): UIFont {
let result = UIFont.systemFontOfSizeWeight(size, nativeWeight);
if (italic) {
let descriptor = result.fontDescriptor.fontDescriptorWithSymbolicTraits(symbolicTraits);
result = UIFont.fontWithDescriptorSize(descriptor, size);
}
return result;
}
function createUIFont(font: Font, defaultFont: UIFont): UIFont {
let result: UIFont;
const size = font.fontSize || defaultFont.pointSize;
const nativeWeight = getNativeFontWeight(font.fontWeight);
const fontFamilies = parseFontFamily(font.fontFamily);
let symbolicTraits: number = 0;
if (font.isBold) {
symbolicTraits |= UIFontDescriptorSymbolicTraits.TraitBold;
}
if (font.isItalic) {
symbolicTraits |= UIFontDescriptorSymbolicTraits.TraitItalic;
}
let fontDescriptorTraits = {
[UIFontSymbolicTrait]: symbolicTraits
};
// IOS versions below 10 will not return the correct font when UIFontWeightTrait is set
// In this case - rely on UIFontSymbolicTrait only
if (SUPPORT_FONT_WEIGHTS) {
fontDescriptorTraits[UIFontWeightTrait] = nativeWeight;
}
for (let i = 0; i < fontFamilies.length; i++) {
const fontFamily = getFontFamilyRespectingGenericFonts(fontFamilies[i]);
if (shouldUseSystemFont(fontFamily)) {
result = getSystemFont(size, nativeWeight, font.isItalic, symbolicTraits);
break;
} else {
const fontAttributes = {
[UIFontDescriptorFamilyAttribute]: fontFamily,
[UIFontDescriptorTraitsAttribute]: fontDescriptorTraits
};
let descriptor = UIFontDescriptor.fontDescriptorWithFontAttributes(<any>fontAttributes);
result = UIFont.fontWithDescriptorSize(descriptor, size);
let actualItalic = result.fontDescriptor.symbolicTraits & UIFontDescriptorSymbolicTraits.TraitItalic;
if (font.isItalic && !actualItalic && EMULATE_OBLIQUE) {
// The font we got is not actually italic so emulate that with a matrix
descriptor = descriptor.fontDescriptorWithMatrix(OBLIQUE_TRANSFORM);
result = UIFont.fontWithDescriptorSize(descriptor, size);
}
// Check if the resolved font has the correct font-family
// If not - fallback to the next font-family
if (result.familyName === fontFamily) {
break;
} else {
result = null;
}
}
}
// Couldn't resolve font - fallback to the system font
if (!result) {
result = getSystemFont(size, nativeWeight, font.isItalic, symbolicTraits);
}
return result;
}
export module ios {
export function registerFont(fontFile: string) {
let filePath = fs.path.join(fs.knownFolders.currentApp().path, "fonts", fontFile);
if (!fs.File.exists(filePath)) {
filePath = fs.path.join(fs.knownFolders.currentApp().path, fontFile);
}
const fontData = NSFileManager.defaultManager.contentsAtPath(filePath);
if (!fontData) {
throw new Error("Could not load font from: " + fontFile);
}
const provider = CGDataProviderCreateWithCFData(fontData);
const font = CGFontCreateWithDataProvider(provider);
if (!font) {
throw new Error("Could not load font from: " + fontFile);
}
const error = new interop.Reference<NSError>();
if (!CTFontManagerRegisterGraphicsFont(font, error)) {
if (traceEnabled()) {
traceWrite("Error occur while registering font: " + CFErrorCopyDescription(<NSError>error.value), traceCategories.Error, traceMessageType.error);
}
}
}
}
function registerFontsInFolder(fontsFolderPath) {
const fontsFolder = fs.Folder.fromPath(fontsFolderPath);
fontsFolder.eachEntity((fileEntity: fs.FileSystemEntity) => {
if (fs.Folder.exists(fs.path.join(fontsFolderPath, fileEntity.name))) {
return true;
}
if (fileEntity instanceof fs.File &&
((<fs.File>fileEntity).extension === ".ttf" || (<fs.File>fileEntity).extension === ".otf")) {
ios.registerFont(fileEntity.name);
}
return true;
});
}
function registerCustomFonts() {
const appDir = fs.knownFolders.currentApp().path;
const fontsDir = fs.path.join(appDir, "fonts");
if (fs.Folder.exists(fontsDir)) {
registerFontsInFolder(fontsDir);
}
}
registerCustomFonts();

View File

@@ -0,0 +1,21 @@
/**
* @module "ui/styling/gradient"
*/ /** */
import { LengthPercentUnit } from "./style-properties";
import { Color } from "../../color";
import { LinearGradient as LinearGradientDefinition } from "../../css/parser";
export class LinearGradient {
public angle: number;
public colorStops: ColorStop[];
public static parse(value: LinearGradientDefinition): LinearGradientDefinition;
public static equals(first: LinearGradientDefinition, second: LinearGradientDefinition): boolean;
}
export interface ColorStop {
color: Color;
offset?: LengthPercentUnit;
}

View File

@@ -0,0 +1,61 @@
import { LengthPercentUnit } from "./style-properties";
import { Color } from "../../color";
import { ColorStop } from "./gradient";
import * as parser from "../../css/parser";
export class LinearGradient {
public angle: number;
public colorStops: ColorStop[];
public static parse(value: parser.LinearGradient): LinearGradient {
const result = new LinearGradient();
result.angle = value.angle;
result.colorStops = value.colors.map(color => {
const offset = color.offset || null;
let offsetUnit: LengthPercentUnit;
if (offset && offset.unit === "%") {
offsetUnit = {
unit: "%",
value: offset.value
};
}
return {
color: new Color(color.argb),
offset: offsetUnit
};
});
return result;
}
public static equals(first: LinearGradient, second: LinearGradient): boolean {
if (!first && !second) {
return true;
} else if (!first || !second) {
return false;
}
if (first.angle !== second.angle) {
return false;
}
if (first.colorStops.length !== second.colorStops.length) {
return false;
}
for (let i = 0; i < first.colorStops.length; i++) {
const firstStop = first.colorStops[i];
const secondStop = second.colorStops[i];
if (firstStop.offset !== secondStop.offset) {
return false;
}
if (!Color.equals(firstStop.color, secondStop.color)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,117 @@
/**
* @module "ui/styling/style-properties"
*/ /** */
import { TransformFunctionsInfo } from "../animation/animation";
import { Color } from "../../color";
import { Style, CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from "../core/properties";
import { Font, FontStyle, FontWeight } from "./font";
import { Background } from "./background";
import { dip, px, percent } from "../core/view";
export type LengthDipUnit = { readonly unit: "dip", readonly value: dip };
export type LengthPxUnit = { readonly unit: "px", readonly value: px };
export type LengthPercentUnit = { readonly unit: "%", readonly value: percent };
export type Length = "auto" | dip | LengthDipUnit | LengthPxUnit;
export type PercentLength = "auto" | dip | LengthDipUnit | LengthPxUnit | LengthPercentUnit;
export namespace Length {
export function parse(text: string): Length;
export function equals(a: Length, b: Length): boolean;
/**
* Converts Length unit to device pixels.
* @param length The Length to convert.
* @param auto Value to use for conversion of "auto". By default is Math.NaN.
*/
export function toDevicePixels(length: Length, auto?: number): number;
export function convertToString(length: Length): string;
}
export namespace PercentLength {
export function parse(text: string): PercentLength;
export function equals(a: PercentLength, b: PercentLength): boolean;
/**
* Converts PercentLength unit to device pixels.
* @param length The PercentLength to convert.
* @param auto Value to use for conversion of "auto". By default is Math.NaN.
* @param parentAvailableWidth Value to use as base when converting percent unit. By default is Math.NaN.
*/
export function toDevicePixels(length: PercentLength, auto?: number, parentAvailableWidth?: px): number;
export function convertToString(length: PercentLength): string;
}
export const zeroLength: Length;
export const rotateProperty: CssAnimationProperty<Style, number>;
export const scaleXProperty: CssAnimationProperty<Style, number>;
export const scaleYProperty: CssAnimationProperty<Style, number>;
export const translateXProperty: CssAnimationProperty<Style, dip>;
export const translateYProperty: CssAnimationProperty<Style, dip>;
export function transformConverter(text: string): TransformFunctionsInfo;
export const clipPathProperty: CssProperty<Style, string>;
export const colorProperty: InheritedCssProperty<Style, Color>;
export const backgroundProperty: ShorthandProperty<Style, string>;
export const backgroundColorProperty: CssAnimationProperty<Style, Color>;
export const backgroundImageProperty: CssProperty<Style, string>;
export const backgroundRepeatProperty: CssProperty<Style, BackgroundRepeat>;
export const backgroundSizeProperty: CssProperty<Style, string>;
export const backgroundPositionProperty: CssProperty<Style, string>;
export const borderColorProperty: ShorthandProperty<Style, string | Color>;
export const borderTopColorProperty: CssProperty<Style, Color>;
export const borderRightColorProperty: CssProperty<Style, Color>;
export const borderBottomColorProperty: CssProperty<Style, Color>;
export const borderLeftColorProperty: CssProperty<Style, Color>;
export const borderWidthProperty: ShorthandProperty<Style, string | Length>;
export const borderTopWidthProperty: CssProperty<Style, Length>;
export const borderRightWidthProperty: CssProperty<Style, Length>;
export const borderBottomWidthProperty: CssProperty<Style, Length>;
export const borderLeftWidthProperty: CssProperty<Style, Length>;
export const borderRadiusProperty: ShorthandProperty<Style, string | Length>;
export const borderTopLeftRadiusProperty: CssProperty<Style, Length>;
export const borderTopRightRadiusProperty: CssProperty<Style, Length>;
export const borderBottomRightRadiusProperty: CssProperty<Style, Length>;
export const borderBottomLeftRadiusProperty: CssProperty<Style, Length>;
export const zIndexProperty: CssProperty<Style, number>;
export const visibilityProperty: CssProperty<Style, Visibility>;
export const opacityProperty: CssAnimationProperty<Style, number>;
export const minWidthProperty: CssProperty<Style, dip | LengthDipUnit | LengthPxUnit>;
export const minHeightProperty: CssProperty<Style, dip | LengthDipUnit | LengthPxUnit>;
export const widthProperty: CssAnimationProperty<Style, PercentLength>;
export const heightProperty: CssAnimationProperty<Style, PercentLength>;
export const lineHeightProperty: CssProperty<Style, number>;
export const marginProperty: ShorthandProperty<Style, string | PercentLength>;
export const marginLeftProperty: CssProperty<Style, PercentLength>;
export const marginRightProperty: CssProperty<Style, PercentLength>;
export const marginTopProperty: CssProperty<Style, PercentLength>;
export const marginBottomProperty: CssProperty<Style, PercentLength>;
export const paddingProperty: ShorthandProperty<Style, string | Length>;
export const paddingLeftProperty: CssProperty<Style, Length>;
export const paddingRightProperty: CssProperty<Style, Length>;
export const paddingTopProperty: CssProperty<Style, Length>;
export const paddingBottomProperty: CssProperty<Style, Length>;
export const horizontalAlignmentProperty: CssProperty<Style, HorizontalAlignment>;
export const verticalAlignmentProperty: CssProperty<Style, VerticalAlignment>;
export const fontSizeProperty: InheritedCssProperty<Style, number>;
export const fontFamilyProperty: InheritedCssProperty<Style, string>;
export const fontStyleProperty: InheritedCssProperty<Style, FontStyle>;
export const fontWeightProperty: InheritedCssProperty<Style, FontWeight>;
export const backgroundInternalProperty: CssProperty<Style, Background>;
export const fontInternalProperty: InheritedCssProperty<Style, Font>;
export type BackgroundRepeat = "repeat" | "repeat-x" | "repeat-y" | "no-repeat";
export type Visibility = "visible" | "hidden" | "collapse";
export type HorizontalAlignment = "left" | "center" | "right" | "stretch";
export type VerticalAlignment = "top" | "middle" | "bottom" | "stretch";

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
/**
* @module "ui/styling/style-scope"
* @private
*/ /** */
//@private
import { ViewBase } from "../core/view-base";
import { SyntaxTree } from "../../css";
import { RuleSet, Node, SelectorCore, ChangeMap } from "../styling/css-selector";
import { KeyframeAnimationInfo } from "../animation/keyframe-animation";
export class CssState {
/**
* Re-evaluate the selectors and apply any changes to the underlying view.
*/
public apply(): void;
/**
* Gets the static selectors that match the view and the dynamic selectors that may potentially match the view.
*/
public changeMap: ChangeMap<ViewBase>;
/**
* Checks whether style scope and CSS state selectors are in sync.
*/
public isSelectorsLatestVersionApplied(): boolean
}
export class StyleScope {
public css: string;
public addCss(cssString: string, cssFileName?: string): void;
public addCssFile(cssFileName: string): void;
public changeCssFile(cssFileName: string): void;
public static createSelectorsFromCss(css: string, cssFileName: string, keyframes: Object): RuleSet[];
public static createSelectorsFromImports(tree: SyntaxTree, keyframes: Object): RuleSet[];
public ensureSelectors(): number;
/**
* Increase the application CSS selector version.
*/
public _increaseApplicationCssSelectorVersion(): void;
public isApplicationCssSelectorsLatestVersionApplied(): boolean;
public isLocalCssSelectorsLatestVersionApplied(): boolean;
public applySelectors(view: ViewBase): void
public query(options: Node): SelectorCore[];
public getKeyframeAnimationWithName(animationName: string): KeyframeAnimationInfo;
public getAnimations(ruleset: RuleSet): KeyframeAnimationInfo[];
}
export function resolveFileNameFromUrl(url: string, appDirectory: string, fileExists: (string) => boolean, importSource?: string): string;
export function applyInlineStyle(view: ViewBase, style: string): void;

View File

@@ -0,0 +1,949 @@
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,
parse as parseCss,
Node as CssNode,
} from "../../css";
import {
CSS3Parser,
CSSNativeScript
} from "../../css/parser";
import {
cssTreeParse
} from "../../css/css-tree-parser";
import {
RuleSet,
SelectorsMap,
SelectorCore,
SelectorsMatch,
ChangeMap,
fromAstNodes,
Node,
} from "./css-selector";
import {
write as traceWrite,
categories as traceCategories,
messageType as traceMessageType,
} 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) {
traceWrite(`Failed to evaluate css-calc for property [${property}] for expression [${value}] to ${view}. ${e.stack}`, traceCategories.Error, traceMessageType.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 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) {
if (typeof cssOrAst === "string") {
// raw-loader
return CSSSource.fromSource(cssOrAst, keyframes, resolvedModuleName);
} else if (typeof cssOrAst === "object" && cssOrAst.type === "stylesheet" && cssOrAst.stylesheet && cssOrAst.stylesheet.rules) {
// css-loader
return CSSSource.fromAST(cssOrAst, keyframes, resolvedModuleName);
} else {
// css2json-loader
return CSSSource.fromSource(cssOrAst.toString(), keyframes, resolvedModuleName);
}
}
} catch (e) {
traceWrite(`Could not load CSS from ${uri}: ${e}`, traceCategories.Error, traceMessageType.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)) {
traceWrite(`${uri} does not start with ${appPath}`, traceCategories.Error, traceMessageType.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) {
traceWrite("Css styling failed: " + e, traceCategories.Error, traceMessageType.error);
this._selectors = [];
}
}
@profile
private parseCSSAst() {
if (this._source) {
switch (parser) {
case "css-tree":
this._ast = cssTreeParse(this._source, this._file);
return;
case "nativescript":
const cssparser = new CSS3Parser(this._source);
const stylesheet = cssparser.parseAStylesheet();
const cssNS = new CSSNativeScript();
this._ast = cssNS.parseStylesheet(stylesheet);
return;
case "rework":
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.fromSource(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);
}
});
function onLiveSync(args: application.CssChangedEventData): void {
loadCss(application.getCssFileName());
}
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();
}
});
application.on("cssChanged", onCssChanged);
application.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);
application.off("loadAppCss", loadAppCSS);
});
if (application.hasLaunched()) {
loadAppCSS({ eventName: "loadAppCss", object: <any>application, cssFile: application.getCssFileName() });
} else {
application.on("loadAppCss", 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() };
_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) {
traceWrite(`isSelectorsLatestVersionApplied returns default value "false" because "this.viewRef" cleared.`, traceCategories.Style, traceMessageType.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) {
traceWrite(`updateDynamicState not executed to view because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.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) {
traceWrite(`KeyframeAnimations cannot play because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.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 {
traceWrite(`KeyframeAnimations cannot be stopped because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.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) {
traceWrite(`${matchingSelectors} not set to view's property because ".viewRef" is cleared`, traceCategories.Style, traceMessageType.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) {
traceWrite(`Failed to apply property [${property}] with value [${value}] to ${view}. ${e.stack}`, traceCategories.Error, traceMessageType.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) {
traceWrite(`toString() of CssState cannot execute correctly because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.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;
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.filter(m => !!m).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;
}
private _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) {
traceWrite(`Failed to apply property [${d.property}] with value [${d.value}] to ${view}. ${e}`, traceCategories.Error, traceMessageType.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";
}

View File

@@ -0,0 +1,5 @@
{
"name": "style",
"main": "style",
"types": "style.d.ts"
}

View File

@@ -0,0 +1,234 @@
/**
* @module "ui/styling/style"
*/ /** */
import { Length, PercentLength, ViewBase, Observable, BackgroundRepeat, Visibility, HorizontalAlignment, VerticalAlignment, dip } from "../../core/view";
import { Color } from "../../../color";
import { Background } from "../background";
import { Font, FontStyle, FontWeight } from "../font";
import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from "../../text-base";
import {
FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent,
Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf
} from "../../layouts/flexbox-layout";
import { LinearGradient } from "../gradient";
export interface Thickness {
left: number;
top: number;
right: number;
bottom: number;
}
export interface BorderColor {
top: Color;
right: Color;
bottom: Color;
left: Color;
}
export interface CommonLayoutParams {
width: number;
height: number;
widthPercent: number;
heightPercent: number;
leftMargin: number;
topMargin: number;
rightMargin: number;
bottomMargin: number;
leftMarginPercent: number;
topMarginPercent: number;
rightMarginPercent: number;
bottomMarginPercent: number;
horizontalAlignment: HorizontalAlignment;
verticalAlignment: VerticalAlignment;
}
export class Style extends Observable {
public fontInternal: Font;
public backgroundInternal: Background;
public rotate: number;
public rotateX: number;
public rotateY: number;
public perspective: number;
public scaleX: number;
public scaleY: number;
public translateX: dip;
public translateY: dip;
public clipPath: string;
public color: Color;
public tintColor: Color;
public placeholderColor: Color;
public background: string;
public backgroundColor: Color;
public backgroundImage: string | LinearGradient;
public backgroundRepeat: BackgroundRepeat;
public backgroundSize: string;
public backgroundPosition: string;
public borderColor: string | Color;
public borderTopColor: Color;
public borderRightColor: Color;
public borderBottomColor: Color;
public borderLeftColor: Color;
public borderWidth: string | Length;
public borderTopWidth: Length;
public borderRightWidth: Length;
public borderBottomWidth: Length;
public borderLeftWidth: Length;
public borderRadius: string | Length;
public borderTopLeftRadius: Length;
public borderTopRightRadius: Length;
public borderBottomRightRadius: Length;
public borderBottomLeftRadius: Length;
public fontSize: number;
public fontFamily: string;
public fontStyle: FontStyle;
public fontWeight: FontWeight;
public font: string;
public androidElevation: number;
public androidDynamicElevationOffset: number;
public zIndex: number;
public opacity: number;
public visibility: Visibility;
public letterSpacing: number;
public lineHeight: number;
public textAlignment: TextAlignment;
public textDecoration: TextDecoration;
public textTransform: TextTransform;
public whiteSpace: WhiteSpace;
public minWidth: Length;
public minHeight: Length;
public width: PercentLength;
public height: PercentLength;
public margin: string | PercentLength;
public marginLeft: PercentLength;
public marginTop: PercentLength;
public marginRight: PercentLength;
public marginBottom: PercentLength;
public padding: string | Length;
public paddingLeft: Length;
public paddingTop: Length;
public paddingRight: Length;
public paddingBottom: Length;
public horizontalAlignment: HorizontalAlignment;
public verticalAlignment: VerticalAlignment;
// TabView-specific props
public tabTextFontSize: number;
public tabTextColor: Color;
public tabBackgroundColor: Color;
public selectedTabTextColor: Color;
public androidSelectedTabHighlightColor: Color;
// ListView-specific props
public separatorColor: Color;
// SegmentedBar-specific props
public selectedBackgroundColor: Color;
// Page-specific props
public statusBarStyle: "light" | "dark";
public androidStatusBarBackground: Color;
// Android ActionBar specific props
/**
* Gets or sets the content inset for the android actionbar.
* The content inset affects the valid area for ActionBar content; insets can be used to effectively align ActionBar content along well-known gridlines.
*
* This property is effective on Android API level 21 or later.
*/
public androidContentInset: string | Length;
/**
* Gets or sets the left content inset for the android actionbar.
* The content inset affects the valid area for ActionBar content; insets can be used to effectively align ActionBar content along well-known gridlines.
*
* This property is effective on Android API level 21 or later.
*/
public androidContentInsetLeft: Length;
/**
* Gets or sets the right content inset for the android actionbar.
* The content inset affects the valid area for ActionBar content; insets can be used to effectively align ActionBar content along well-known gridlines.
*
* This property is effective on Android API level 21 or later.
*/
public androidContentInsetRight: Length;
constructor(ownerView: ViewBase | WeakRef<ViewBase>);
public viewRef: WeakRef<ViewBase>;
/**
* @deprecated use `viewRef` instead.
*
* The `ViewBase` object associated with the Style!
*/
public view: ViewBase;
//flexbox layout properties
public flexDirection: FlexDirection;
public flexWrap: FlexWrap;
public justifyContent: JustifyContent;
public alignItems: AlignItems;
public alignContent: AlignContent;
public order: Order;
public flexGrow: FlexGrow;
public flexShrink: FlexShrink;
public flexWrapBefore: FlexWrapBefore;
public alignSelf: AlignSelf;
/**
* The property bag is a simple class that is paired with the Style class.
* Setting regular css properties on the PropertyBag should simply preserve their values.
* Setting shorthand css properties on the PropertyBag should decompose the provided value, and set each of the shorthand composite properties.
* The shorthand properties are defined as non-enumerable so it should be safe to for-in the keys that are set in the bag.
*/
public readonly PropertyBag: PropertyBagClass;
/**
* Set a scoped css-value. These are css-variables set from CssState
*/
public setScopedCssVariable(varName: string, value: string): void;
/**
* Set a unscoped css-value. These are css-variables set on view.style
*/
public setUnscopedCssVariable(varName: string, value: string): void;
/**
* Get value of the css-variable.
* If the value is not set on this style-object, try the parent view.
*/
public getCssVariable(varName: string): string | null;
/**
* Remove all scoped css-variables
*/
public resetScopedCssVariables(): void;
/**
* Remove all unscoped css-variables
*/
public resetUnscopedCssVariables(): void;
}
interface PropertyBagClass {
new(): PropertyBag;
prototype: PropertyBag;
}
interface PropertyBag {
[property: string]: string;
}

View File

@@ -0,0 +1,208 @@
import { Style as StyleDefinition } from ".";
import { Color } from "../../../color";
import { Font, FontStyle, FontWeight } from "../font";
import { Background } from "../background";
import {
Length, PercentLength, ViewBase, BackgroundRepeat, Visibility,
HorizontalAlignment, VerticalAlignment, dip, LinearGradient
} from "../../core/view";
import { Observable } from "../../../data/observable";
import {
FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent,
Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf
} from "../../layouts/flexbox-layout";
import {
write as traceWrite,
categories as traceCategories,
messageType as traceMessageType,
} from "../../../trace";
import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from "../../text-base";
export class Style extends Observable implements StyleDefinition {
private unscopedCssVariables = new Map<string, string>();
private scopedCssVariables = new Map<string, string>();
constructor(ownerView: ViewBase | WeakRef<ViewBase>) {
super();
// HACK: Could not find better way for cross platform WeakRef type checking.
if (ownerView.constructor.toString().indexOf("[native code]") !== -1) {
this.viewRef = <WeakRef<ViewBase>>ownerView;
} else {
this.viewRef = new WeakRef(<ViewBase>ownerView);
}
}
public setScopedCssVariable(varName: string, value: string): void {
this.scopedCssVariables.set(varName, value);
}
public setUnscopedCssVariable(varName: string, value: string): void {
this.unscopedCssVariables.set(varName, value);
}
public getCssVariable(varName: string): string | null {
const view = this.view;
if (!view) {
return null;
}
if (this.unscopedCssVariables.has(varName)) {
return this.unscopedCssVariables.get(varName);
}
if (this.scopedCssVariables.has(varName)) {
return this.scopedCssVariables.get(varName);
}
if (!view.parent || !view.parent.style) {
return null;
}
return view.parent.style.getCssVariable(varName);
}
public resetScopedCssVariables() {
this.scopedCssVariables.clear();
}
public resetUnscopedCssVariables() {
this.unscopedCssVariables.clear();
}
toString() {
const view = this.viewRef.get();
if (!view) {
traceWrite(`toString() of Style cannot execute correctly because ".viewRef" is cleared`, traceCategories.Animation, traceMessageType.warn);
return "";
}
return `${view}.style`;
}
public fontInternal: Font;
public backgroundInternal: Background;
public rotate: number;
public rotateX: number;
public rotateY: number;
public perspective: number;
public scaleX: number;
public scaleY: number;
public translateX: dip;
public translateY: dip;
public clipPath: string;
public color: Color;
public tintColor: Color;
public placeholderColor: Color;
public background: string;
public backgroundColor: Color;
public backgroundImage: string | LinearGradient;
public backgroundRepeat: BackgroundRepeat;
public backgroundSize: string;
public backgroundPosition: string;
public borderColor: string | Color;
public borderTopColor: Color;
public borderRightColor: Color;
public borderBottomColor: Color;
public borderLeftColor: Color;
public borderWidth: string | Length;
public borderTopWidth: Length;
public borderRightWidth: Length;
public borderBottomWidth: Length;
public borderLeftWidth: Length;
public borderRadius: string | Length;
public borderTopLeftRadius: Length;
public borderTopRightRadius: Length;
public borderBottomRightRadius: Length;
public borderBottomLeftRadius: Length;
public fontSize: number;
public fontFamily: string;
public fontStyle: FontStyle;
public fontWeight: FontWeight;
public font: string;
public androidElevation: number;
public androidDynamicElevationOffset: number;
public zIndex: number;
public opacity: number;
public visibility: Visibility;
public letterSpacing: number;
public lineHeight: number;
public textAlignment: TextAlignment;
public textDecoration: TextDecoration;
public textTransform: TextTransform;
public whiteSpace: WhiteSpace;
public minWidth: Length;
public minHeight: Length;
public width: PercentLength;
public height: PercentLength;
public margin: string | PercentLength;
public marginLeft: PercentLength;
public marginTop: PercentLength;
public marginRight: PercentLength;
public marginBottom: PercentLength;
public padding: string | Length;
public paddingLeft: Length;
public paddingTop: Length;
public paddingRight: Length;
public paddingBottom: Length;
public horizontalAlignment: HorizontalAlignment;
public verticalAlignment: VerticalAlignment;
// TabView-specific props
public tabTextFontSize: number;
public tabTextColor: Color;
public tabBackgroundColor: Color;
public selectedTabTextColor: Color;
public androidSelectedTabHighlightColor: Color;
// ListView-specific props
public separatorColor: Color;
//SegmentedBar-specific props
public selectedBackgroundColor: Color;
// Page-specific props
public statusBarStyle: "light" | "dark";
public androidStatusBarBackground: Color;
// ActionBar-specific props
public androidContentInset: string | Length;
public androidContentInsetLeft: Length;
public androidContentInsetRight: Length;
//flexbox layout properties
public flexDirection: FlexDirection;
public flexWrap: FlexWrap;
public justifyContent: JustifyContent;
public alignItems: AlignItems;
public alignContent: AlignContent;
public order: Order;
public flexGrow: FlexGrow;
public flexShrink: FlexShrink;
public flexWrapBefore: FlexWrapBefore;
public alignSelf: AlignSelf;
public PropertyBag: { new(): { [property: string]: string }, prototype: { [property: string]: string } };
public viewRef: WeakRef<ViewBase>;
public get view(): ViewBase {
if (this.viewRef) {
return this.viewRef.get();
}
return undefined;
}
}
Style.prototype.PropertyBag = class { [property: string]: string; };