mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
* feat(animation): support animating width/height properties - width/height can be specified in any valid PercentLength form that can be parsed. - make width/height properties be based on animatable CSS property. TODO: affectsLayout???? - add a few basic tests. Could probably use a few more? - fix a few null pointer exceptions in PercentLength helpers * test(ui): add animation examples to ui-tests-app - basic height animation - height animation in StackLayout - fix an issue where strings were not automatically converted to PercentLength when calling directly into `View.animate` * test(ui): cleanup and add summary/details layout example - use height transition to cover textview content. - when clicking on the summary view, animate the summary height up to a small header and show the text view. - fake animating the height on the textview by very subtly animating its translateY value while shrinking the header height. This tricks your mind into think that the text view is also vertically growing, even thought it's just slightly moving up along the Y axis. * test(ui): add animation curves test page - verify all built-in animation curve types work as expected. * test(ui): update animation curve example for multiple properties - add a segmented bar that allows choosing which properties to animate using the various curves. - interestingly, a whole bunch of properties fail with spring on iOS. - refactor width/height animations handlers to remove duplication on iOS. - implement proper spring animation for width/height on iOS. * test(ui): add stress example with 100 labels animating and fps meter - same curve/property selector as the curves example, but with 10x10 grid of items that stagger animate, and an FPS meter. - sadly it looks like width/height animations are considerably slower than the others when you have a bunch of them. I'm not sure that's entirely surprising since they interact with the layout system. - the better news is that even with the army example, my really old android 4 tablet manages ~30fps. On height/width animations from the curves example, the old tablet does fine with no noticeable FPS hit. * refactor: deduplicate existing droid width/height animations - stash to prep for replacing with LayoutTransition. * test(animation): unit tests for extent animation and PercentLength parse - update animation scaffold to allow specifying the parent stack layout height/width - test basic supported units, px, % - test basic percent length parser behaviors * chore: cleanup cruft and remove noise from diff - undo the import mangling that WebStorm helpfully applied - remove .editorconfig file - clean up in tests, remove cruft * chore: cleanup from review - more import changes * chore: remove .editorconfig
1103 lines
45 KiB
TypeScript
1103 lines
45 KiB
TypeScript
// Types
|
|
import { unsetValue, Style,
|
|
CssProperty, CssAnimationProperty,
|
|
ShorthandProperty, InheritedCssProperty,
|
|
makeValidator, makeParser } from "../core/properties";
|
|
import {
|
|
Transformation,
|
|
TransformationValue,
|
|
TransformFunctionsInfo,
|
|
} from "../animation/animation";
|
|
|
|
import { dip, px, percent } from "../core/view";
|
|
|
|
import { Color } from "../../color";
|
|
import { Font, parseFont, FontStyle, FontWeight } from "../../ui/styling/font";
|
|
import { layout, hasDuplicates } from "../../utils/utils";
|
|
import { Background } from "../../ui/styling/background";
|
|
import { isIOS } from "../../platform";
|
|
|
|
import { radiansToDegrees } from "../../utils/number-utils";
|
|
|
|
import {
|
|
decompose2DTransformMatrix,
|
|
getTransformMatrix,
|
|
matrixArrayToCssMatrix,
|
|
multiplyAffine2d,
|
|
} from "../../matrix";
|
|
|
|
import * as parser from "../../css/parser";
|
|
|
|
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;
|
|
|
|
function equalsCommon(a: Length, b: Length): boolean;
|
|
function equalsCommon(a: PercentLength, b: PercentLength): boolean;
|
|
function equalsCommon(a: PercentLength, b: PercentLength): boolean {
|
|
if (a == "auto") { // tslint:disable-line
|
|
return b == "auto"; // tslint:disable-line
|
|
}
|
|
if (typeof a === "number") {
|
|
if (b == "auto") { // tslint:disable-line
|
|
return false;
|
|
}
|
|
if (typeof b === "number") {
|
|
return a == b; // tslint:disable-line
|
|
}
|
|
if (!b) {
|
|
return false;
|
|
}
|
|
return b.unit == "dip" && a == b.value; // tslint:disable-line
|
|
}
|
|
if (b == "auto") { // tslint:disable-line
|
|
return false;
|
|
}
|
|
if (typeof b === "number") {
|
|
return a ? (a.unit == "dip" && a.value == b) : false; // tslint:disable-line
|
|
}
|
|
if (!a || !b) {
|
|
return false;
|
|
}
|
|
return a.value == b.value && a.unit == b.unit; // tslint:disable-line
|
|
}
|
|
|
|
function convertToStringCommon(length: Length | PercentLength): string {
|
|
if (length == "auto") { // tslint:disable-line
|
|
return "auto";
|
|
}
|
|
|
|
if (typeof length === "number") {
|
|
return length.toString();
|
|
}
|
|
|
|
let val = length.value;
|
|
if (length.unit === "%") {
|
|
val *= 100;
|
|
}
|
|
|
|
return val + length.unit;
|
|
}
|
|
|
|
function toDevicePixelsCommon(length: PercentLength, auto: number = Number.NaN, parentAvailableWidth: number = Number.NaN): number {
|
|
if (length == "auto") { // tslint:disable-line
|
|
return auto;
|
|
}
|
|
if (typeof length === "number") {
|
|
return layout.round(layout.toDevicePixels(length));
|
|
}
|
|
if (!length) {
|
|
return auto;
|
|
}
|
|
switch (length.unit) {
|
|
case "px":
|
|
return layout.round(length.value);
|
|
case "%":
|
|
return layout.round(parentAvailableWidth * length.value);
|
|
case "dip":
|
|
default:
|
|
return layout.round(layout.toDevicePixels(length.value));
|
|
}
|
|
}
|
|
|
|
export namespace PercentLength {
|
|
export function parse(fromValue: string | Length): PercentLength {
|
|
if (fromValue == "auto") { // tslint:disable-line
|
|
return "auto";
|
|
}
|
|
if (typeof fromValue === "string") {
|
|
let stringValue = fromValue.trim();
|
|
let percentIndex = stringValue.indexOf("%");
|
|
if (percentIndex !== -1) {
|
|
let value: percent;
|
|
// if only % or % is not last we treat it as invalid value.
|
|
if (percentIndex !== (stringValue.length - 1) || percentIndex === 0) {
|
|
value = Number.NaN;
|
|
} else {
|
|
// Normalize result to values between -1 and 1
|
|
value = parseFloat(stringValue.substring(0, stringValue.length - 1).trim()) / 100;
|
|
}
|
|
|
|
if (isNaN(value) || !isFinite(value)) {
|
|
throw new Error(`Invalid value: ${fromValue}`);
|
|
}
|
|
return { unit: "%", value }
|
|
} else if (stringValue.indexOf("px") !== -1) {
|
|
stringValue = stringValue.replace("px", "").trim();
|
|
let value: px = parseFloat(stringValue);
|
|
if (isNaN(value) || !isFinite(value)) {
|
|
throw new Error(`Invalid value: ${fromValue}`);
|
|
}
|
|
return { unit: "px", value };
|
|
} else {
|
|
let value: dip = parseFloat(stringValue);
|
|
if (isNaN(value) || !isFinite(value)) {
|
|
throw new Error(`Invalid value: ${fromValue}`);
|
|
}
|
|
return value;
|
|
}
|
|
} else {
|
|
return fromValue;
|
|
}
|
|
}
|
|
|
|
export const equals: { (a: PercentLength, b: PercentLength): boolean } = equalsCommon;
|
|
export const toDevicePixels: { (length: PercentLength, auto: number, parentAvailableWidth: number): number } = toDevicePixelsCommon;
|
|
export const convertToString: { (length: PercentLength): string } = convertToStringCommon;
|
|
}
|
|
|
|
export namespace Length {
|
|
export function parse(fromValue: string | Length): Length {
|
|
if (fromValue == "auto") { // tslint:disable-line
|
|
return "auto";
|
|
}
|
|
if (typeof fromValue === "string") {
|
|
let stringValue = fromValue.trim();
|
|
if (stringValue.indexOf("px") !== -1) {
|
|
stringValue = stringValue.replace("px", "").trim();
|
|
let value: px = parseFloat(stringValue);
|
|
if (isNaN(value) || !isFinite(value)) {
|
|
throw new Error(`Invalid value: ${stringValue}`);
|
|
}
|
|
return { unit: "px", value };
|
|
} else {
|
|
let value: dip = parseFloat(stringValue);
|
|
if (isNaN(value) || !isFinite(value)) {
|
|
throw new Error(`Invalid value: ${stringValue}`);
|
|
}
|
|
return value;
|
|
}
|
|
} else {
|
|
return fromValue;
|
|
}
|
|
}
|
|
export const equals: { (a: Length, b: Length): boolean } = equalsCommon;
|
|
export const toDevicePixels: { (length: Length, auto?: number): number } = toDevicePixelsCommon;
|
|
export const convertToString: { (length: Length): string } = convertToStringCommon;
|
|
}
|
|
|
|
export const zeroLength: Length = { value: 0, unit: "px" };
|
|
|
|
export const minWidthProperty = new CssProperty<Style, Length>({
|
|
name: "minWidth", cssName: "min-width", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
target.view.effectiveMinWidth = Length.toDevicePixels(newValue, 0);
|
|
}, valueConverter: Length.parse
|
|
});
|
|
minWidthProperty.register(Style);
|
|
|
|
export const minHeightProperty = new CssProperty<Style, Length>({
|
|
name: "minHeight", cssName: "min-height", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
target.view.effectiveMinHeight = Length.toDevicePixels(newValue, 0);
|
|
}, valueConverter: Length.parse
|
|
});
|
|
minHeightProperty.register(Style);
|
|
|
|
export const widthProperty = new CssAnimationProperty<Style, PercentLength>({
|
|
name: "width", cssName: "width", defaultValue: "auto", equalityComparer: Length.equals,
|
|
// TODO: CSSAnimationProperty was needed for keyframe (copying other impls), but `affectsLayout` does not exist
|
|
// on the animation property, so fake it here. x_x
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
if (isIOS) {
|
|
target.view.requestLayout();
|
|
}
|
|
}, valueConverter: PercentLength.parse });
|
|
widthProperty.register(Style);
|
|
|
|
export const heightProperty = new CssAnimationProperty<Style, PercentLength>({
|
|
name: "height", cssName: "height", defaultValue: "auto", equalityComparer: Length.equals,
|
|
// TODO: CSSAnimationProperty was needed for keyframe (copying other impls), but `affectsLayout` does not exist
|
|
// on the animation property, so fake it here. -_-
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
if (isIOS) {
|
|
target.view.requestLayout();
|
|
}
|
|
}, valueConverter: PercentLength.parse,
|
|
|
|
});
|
|
heightProperty.register(Style);
|
|
|
|
const marginProperty = new ShorthandProperty<Style, string | PercentLength>({
|
|
name: "margin", cssName: "margin",
|
|
getter: function (this: Style) {
|
|
if (PercentLength.equals(this.marginTop, this.marginRight) &&
|
|
PercentLength.equals(this.marginTop, this.marginBottom) &&
|
|
PercentLength.equals(this.marginTop, this.marginLeft)) {
|
|
return this.marginTop;
|
|
}
|
|
return `${PercentLength.convertToString(this.marginTop)} ${PercentLength.convertToString(this.marginRight)} ${PercentLength.convertToString(this.marginBottom)} ${PercentLength.convertToString(this.marginLeft)}`;
|
|
},
|
|
converter: convertToMargins
|
|
});
|
|
marginProperty.register(Style);
|
|
|
|
export const marginLeftProperty = new CssProperty<Style, PercentLength>({ name: "marginLeft", cssName: "margin-left", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
|
|
marginLeftProperty.register(Style);
|
|
|
|
export const marginRightProperty = new CssProperty<Style, PercentLength>({ name: "marginRight", cssName: "margin-right", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
|
|
marginRightProperty.register(Style);
|
|
|
|
export const marginTopProperty = new CssProperty<Style, PercentLength>({ name: "marginTop", cssName: "margin-top", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
|
|
marginTopProperty.register(Style);
|
|
|
|
export const marginBottomProperty = new CssProperty<Style, PercentLength>({ name: "marginBottom", cssName: "margin-bottom", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
|
|
marginBottomProperty.register(Style);
|
|
|
|
const paddingProperty = new ShorthandProperty<Style, string | Length>({
|
|
name: "padding", cssName: "padding",
|
|
getter: function (this: Style) {
|
|
if (Length.equals(this.paddingTop, this.paddingRight) &&
|
|
Length.equals(this.paddingTop, this.paddingBottom) &&
|
|
Length.equals(this.paddingTop, this.paddingLeft)) {
|
|
return this.paddingTop;
|
|
}
|
|
return `${Length.convertToString(this.paddingTop)} ${Length.convertToString(this.paddingRight)} ${Length.convertToString(this.paddingBottom)} ${Length.convertToString(this.paddingLeft)}`;
|
|
},
|
|
converter: convertToPaddings
|
|
});
|
|
paddingProperty.register(Style);
|
|
|
|
export const paddingLeftProperty = new CssProperty<Style, Length>({
|
|
name: "paddingLeft", cssName: "padding-left", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
target.view.effectivePaddingLeft = Length.toDevicePixels(newValue, 0);
|
|
}, valueConverter: Length.parse
|
|
});
|
|
paddingLeftProperty.register(Style);
|
|
|
|
export const paddingRightProperty = new CssProperty<Style, Length>({
|
|
name: "paddingRight", cssName: "padding-right", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
target.view.effectivePaddingRight = Length.toDevicePixels(newValue, 0);
|
|
}, valueConverter: Length.parse
|
|
});
|
|
paddingRightProperty.register(Style);
|
|
|
|
export const paddingTopProperty = new CssProperty<Style, Length>({
|
|
name: "paddingTop", cssName: "padding-top", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
target.view.effectivePaddingTop = Length.toDevicePixels(newValue, 0);
|
|
}, valueConverter: Length.parse
|
|
});
|
|
paddingTopProperty.register(Style);
|
|
|
|
export const paddingBottomProperty = new CssProperty<Style, Length>({
|
|
name: "paddingBottom", cssName: "padding-bottom", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
target.view.effectivePaddingBottom = Length.toDevicePixels(newValue, 0);
|
|
}, valueConverter: Length.parse
|
|
});
|
|
paddingBottomProperty.register(Style);
|
|
|
|
export type HorizontalAlignment = "left" | "center" | "right" | "stretch";
|
|
export namespace HorizontalAlignment {
|
|
export const LEFT: "left" = "left";
|
|
export const CENTER: "center" = "center";
|
|
export const RIGHT: "right" = "right";
|
|
export const STRETCH: "stretch" = "stretch";
|
|
export const isValid = makeValidator<HorizontalAlignment>(LEFT, CENTER, RIGHT, STRETCH);
|
|
export const parse = makeParser<HorizontalAlignment>(isValid);
|
|
}
|
|
|
|
export const horizontalAlignmentProperty = new CssProperty<Style, HorizontalAlignment>({ name: "horizontalAlignment", cssName: "horizontal-align", defaultValue: HorizontalAlignment.STRETCH, affectsLayout: isIOS, valueConverter: HorizontalAlignment.parse });
|
|
horizontalAlignmentProperty.register(Style);
|
|
|
|
export type VerticalAlignment = "top" | "middle" | "bottom" | "stretch";
|
|
export namespace VerticalAlignment {
|
|
export const TOP: "top" = "top";
|
|
export const MIDDLE: "middle" = "middle";
|
|
export const BOTTOM: "bottom" = "bottom";
|
|
export const STRETCH: "stretch" = "stretch";
|
|
export const isValid = makeValidator<VerticalAlignment>(TOP, MIDDLE, BOTTOM, STRETCH);
|
|
export const parse = (value: string) => value.toLowerCase() === "center" ? MIDDLE : parseStrict(value);
|
|
const parseStrict = makeParser<VerticalAlignment>(isValid);
|
|
}
|
|
|
|
export const verticalAlignmentProperty = new CssProperty<Style, VerticalAlignment>({ name: "verticalAlignment", cssName: "vertical-align", defaultValue: VerticalAlignment.STRETCH, affectsLayout: isIOS, valueConverter: VerticalAlignment.parse });
|
|
verticalAlignmentProperty.register(Style);
|
|
|
|
interface Thickness {
|
|
top: string;
|
|
right: string;
|
|
bottom: string;
|
|
left: string;
|
|
}
|
|
|
|
function parseThickness(value: string): Thickness {
|
|
if (typeof value === "string") {
|
|
let arr = value.split(/[ ,]+/);
|
|
|
|
let top: string;
|
|
let right: string;
|
|
let bottom: string;
|
|
let left: string;
|
|
|
|
if (arr.length === 1) {
|
|
top = arr[0];
|
|
right = arr[0];
|
|
bottom = arr[0];
|
|
left = arr[0];
|
|
}
|
|
else if (arr.length === 2) {
|
|
top = arr[0];
|
|
bottom = arr[0];
|
|
right = arr[1];
|
|
left = arr[1];
|
|
}
|
|
else if (arr.length === 3) {
|
|
top = arr[0];
|
|
right = arr[1];
|
|
left = arr[1];
|
|
bottom = arr[2];
|
|
}
|
|
else if (arr.length === 4) {
|
|
top = arr[0];
|
|
right = arr[1];
|
|
bottom = arr[2];
|
|
left = arr[3];
|
|
}
|
|
else {
|
|
throw new Error("Expected 1, 2, 3 or 4 parameters. Actual: " + value);
|
|
}
|
|
|
|
return {
|
|
top: top,
|
|
right: right,
|
|
bottom: bottom,
|
|
left: left
|
|
};
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
function convertToMargins(this: void, value: string | PercentLength): [CssProperty<any, any>, any][] {
|
|
if (typeof value === "string" && value !== "auto") {
|
|
let thickness = parseThickness(value);
|
|
return [
|
|
[marginTopProperty, PercentLength.parse(thickness.top)],
|
|
[marginRightProperty, PercentLength.parse(thickness.right)],
|
|
[marginBottomProperty, PercentLength.parse(thickness.bottom)],
|
|
[marginLeftProperty, PercentLength.parse(thickness.left)]
|
|
];
|
|
}
|
|
else {
|
|
return [
|
|
[marginTopProperty, value],
|
|
[marginRightProperty, value],
|
|
[marginBottomProperty, value],
|
|
[marginLeftProperty, value]
|
|
];
|
|
}
|
|
}
|
|
|
|
function convertToPaddings(this: void, value: string | Length): [CssProperty<any, any>, any][] {
|
|
if (typeof value === "string" && value !== "auto") {
|
|
let thickness = parseThickness(value);
|
|
return [
|
|
[paddingTopProperty, Length.parse(thickness.top)],
|
|
[paddingRightProperty, Length.parse(thickness.right)],
|
|
[paddingBottomProperty, Length.parse(thickness.bottom)],
|
|
[paddingLeftProperty, Length.parse(thickness.left)]
|
|
];
|
|
}
|
|
else {
|
|
return [
|
|
[paddingTopProperty, value],
|
|
[paddingRightProperty, value],
|
|
[paddingBottomProperty, value],
|
|
[paddingLeftProperty, value]
|
|
];
|
|
}
|
|
}
|
|
|
|
export const rotateProperty = new CssAnimationProperty<Style, number>({ name: "rotate", cssName: "rotate", defaultValue: 0, valueConverter: parseFloat });
|
|
rotateProperty.register(Style);
|
|
|
|
export const scaleXProperty = new CssAnimationProperty<Style, number>({ name: "scaleX", cssName: "scaleX", defaultValue: 1, valueConverter: parseFloat });
|
|
scaleXProperty.register(Style);
|
|
|
|
export const scaleYProperty = new CssAnimationProperty<Style, number>({ name: "scaleY", cssName: "scaleY", defaultValue: 1, valueConverter: parseFloat });
|
|
scaleYProperty.register(Style);
|
|
|
|
function parseDIPs(value: string): dip {
|
|
if (value.indexOf("px") !== -1) {
|
|
return layout.toDeviceIndependentPixels(parseFloat(value.replace("px", "").trim()));
|
|
} else {
|
|
return parseFloat(value.replace("dip", "").trim());
|
|
}
|
|
}
|
|
|
|
export const translateXProperty = new CssAnimationProperty<Style, dip>({ name: "translateX", cssName: "translateX", defaultValue: 0, valueConverter: parseDIPs });
|
|
translateXProperty.register(Style);
|
|
|
|
export const translateYProperty = new CssAnimationProperty<Style, dip>({ name: "translateY", cssName: "translateY", defaultValue: 0, valueConverter: parseDIPs });
|
|
translateYProperty.register(Style);
|
|
|
|
const transformProperty = new ShorthandProperty<Style, string>({
|
|
name: "transform", cssName: "transform",
|
|
getter: function (this: Style) {
|
|
let scaleX = this.scaleX;
|
|
let scaleY = this.scaleY;
|
|
let translateX = this.translateX;
|
|
let translateY = this.translateY;
|
|
let rotate = this.rotate;
|
|
let result = "";
|
|
if (translateX !== 0 || translateY !== 0) {
|
|
result += `translate(${translateX}, ${translateY}) `;
|
|
}
|
|
if (scaleX !== 1 || scaleY !== 1) {
|
|
result += `scale(${scaleX}, ${scaleY}) `;
|
|
}
|
|
if (rotate !== 0) {
|
|
result += `rotate (${rotate})`;
|
|
}
|
|
|
|
return result.trim();
|
|
},
|
|
converter: convertToTransform
|
|
});
|
|
transformProperty.register(Style);
|
|
|
|
const IDENTITY_TRANSFORMATION = {
|
|
translate: { x: 0, y: 0 },
|
|
rotate: 0,
|
|
scale: { x: 1, y: 1 },
|
|
};
|
|
|
|
const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g);
|
|
const TRANSFORMATIONS = Object.freeze([
|
|
"rotate",
|
|
"translate",
|
|
"translate3d",
|
|
"translateX",
|
|
"translateY",
|
|
"scale",
|
|
"scale3d",
|
|
"scaleX",
|
|
"scaleY",
|
|
]);
|
|
|
|
const STYLE_TRANSFORMATION_MAP = Object.freeze({
|
|
"scale": value => ({ property: "scale", value }),
|
|
"scale3d": value => ({ property: "scale", value }),
|
|
"scaleX": ({ x }) => ({ property: "scale", value: { x, y: IDENTITY_TRANSFORMATION.scale.y } }),
|
|
"scaleY": ({ y }) => ({ property: "scale", value: { y, x: IDENTITY_TRANSFORMATION.scale.x } }),
|
|
|
|
"translate": value => ({ property: "translate", value }),
|
|
"translate3d": value => ({ property: "translate", value }),
|
|
"translateX": ({ x }) => ({ property: "translate", value: { x, y: IDENTITY_TRANSFORMATION.translate.y } }),
|
|
"translateY": ({ y }) => ({ property: "translate", value: { y, x: IDENTITY_TRANSFORMATION.translate.x } }),
|
|
|
|
"rotate": value => ({ property: "rotate", value }),
|
|
});
|
|
|
|
function convertToTransform(value: string): [CssProperty<any, any>, any][] {
|
|
if (value === unsetValue) {
|
|
value = "none";
|
|
}
|
|
|
|
const { translate, rotate, scale } = transformConverter(value);
|
|
return [
|
|
[translateXProperty, translate.x],
|
|
[translateYProperty, translate.y],
|
|
|
|
[scaleXProperty, scale.x],
|
|
[scaleYProperty, scale.y],
|
|
|
|
[rotateProperty, rotate],
|
|
];
|
|
}
|
|
|
|
export function transformConverter(text: string): TransformFunctionsInfo {
|
|
const transformations = parseTransformString(text);
|
|
|
|
if (text === "none" || text === "" || !transformations.length) {
|
|
return IDENTITY_TRANSFORMATION;
|
|
}
|
|
|
|
const usedTransforms = transformations.map(t => t.property);
|
|
if (!hasDuplicates(usedTransforms)) {
|
|
const fullTransformations = Object.assign({}, IDENTITY_TRANSFORMATION);
|
|
transformations.forEach(transform => {
|
|
fullTransformations[transform.property] = transform.value;
|
|
});
|
|
|
|
return fullTransformations;
|
|
}
|
|
|
|
const affineMatrix = transformations
|
|
.map(getTransformMatrix)
|
|
.reduce(multiplyAffine2d)
|
|
const cssMatrix = matrixArrayToCssMatrix(affineMatrix)
|
|
|
|
return decompose2DTransformMatrix(cssMatrix);
|
|
}
|
|
|
|
// using general regex and manually checking the matched
|
|
// properties is faster than using more specific regex
|
|
// https://jsperf.com/cssparse
|
|
function parseTransformString(text: string): Transformation[] {
|
|
let matches: Transformation[] = [];
|
|
let match;
|
|
|
|
while ((match = TRANSFORM_SPLITTER.exec(text)) !== null) {
|
|
const property = match[1];
|
|
const value = convertTransformValue(property, match[2]);
|
|
|
|
if (TRANSFORMATIONS.indexOf(property) !== -1) {
|
|
matches.push(normalizeTransformation({ property, value }));
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
function normalizeTransformation({ property, value }: Transformation): Transformation {
|
|
return <any>STYLE_TRANSFORMATION_MAP[property](value);
|
|
}
|
|
|
|
function convertTransformValue(property: string, stringValue: string)
|
|
: TransformationValue {
|
|
|
|
const [x, y = x] = stringValue.split(",").map(parseFloat);
|
|
|
|
if (property === "rotate") {
|
|
return stringValue.slice(-3) === "rad" ? radiansToDegrees(x) : x;
|
|
}
|
|
|
|
return { x, y };
|
|
}
|
|
|
|
// Background properties.
|
|
const backgroundProperty = new ShorthandProperty<Style, string | Color>({
|
|
name: "background", cssName: "background",
|
|
getter: function (this: Style) {
|
|
return `${this.backgroundColor} ${this.backgroundImage} ${this.backgroundRepeat} ${this.backgroundPosition}`;
|
|
},
|
|
converter: convertToBackgrounds
|
|
});
|
|
backgroundProperty.register(Style);
|
|
|
|
export const backgroundInternalProperty = new CssProperty<Style, Background>({
|
|
name: "backgroundInternal",
|
|
cssName: "_backgroundInternal",
|
|
defaultValue: Background.default
|
|
});
|
|
backgroundInternalProperty.register(Style);
|
|
|
|
// const pattern: RegExp = /url\(('|")(.*?)\1\)/;
|
|
export const backgroundImageProperty = new CssProperty<Style, string>({
|
|
name: "backgroundImage", cssName: "background-image", valueChanged: (target, oldValue, newValue) => {
|
|
const background = target.backgroundInternal.withImage(newValue);
|
|
target.backgroundInternal = background;
|
|
}
|
|
});
|
|
backgroundImageProperty.register(Style);
|
|
|
|
export const backgroundColorProperty = new CssAnimationProperty<Style, Color>({
|
|
name: "backgroundColor", cssName: "background-color", valueChanged: (target, oldValue, newValue) => {
|
|
const background = target.backgroundInternal.withColor(newValue);
|
|
target.backgroundInternal = background;
|
|
}, equalityComparer: Color.equals, valueConverter: (value) => new Color(value)
|
|
});
|
|
backgroundColorProperty.register(Style);
|
|
|
|
export type BackgroundRepeat = "repeat" | "repeat-x" | "repeat-y" | "no-repeat";
|
|
export namespace BackgroundRepeat {
|
|
export const REPEAT: "repeat" = "repeat";
|
|
export const REPEAT_X: "repeat-x" = "repeat-x";
|
|
export const REPEAT_Y: "repeat-y" = "repeat-y";
|
|
export const NO_REPEAT: "no-repeat" = "no-repeat";
|
|
export const isValid = makeValidator<BackgroundRepeat>(REPEAT, REPEAT_X, REPEAT_Y, NO_REPEAT);
|
|
export const parse = makeParser<BackgroundRepeat>(isValid);
|
|
}
|
|
|
|
export const backgroundRepeatProperty = new CssProperty<Style, BackgroundRepeat>({
|
|
name: "backgroundRepeat", cssName: "background-repeat", valueConverter: BackgroundRepeat.parse,
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
const background = target.backgroundInternal.withRepeat(newValue);
|
|
target.backgroundInternal = background;
|
|
}
|
|
});
|
|
backgroundRepeatProperty.register(Style);
|
|
|
|
export const backgroundSizeProperty = new CssProperty<Style, string>({
|
|
name: "backgroundSize", cssName: "background-size", valueChanged: (target, oldValue, newValue) => {
|
|
const background = target.backgroundInternal.withSize(newValue);
|
|
target.backgroundInternal = background;
|
|
}
|
|
});
|
|
backgroundSizeProperty.register(Style);
|
|
|
|
export const backgroundPositionProperty = new CssProperty<Style, string>({
|
|
name: "backgroundPosition", cssName: "background-position", valueChanged: (target, oldValue, newValue) => {
|
|
const background = target.backgroundInternal.withPosition(newValue);
|
|
target.backgroundInternal = background;
|
|
}
|
|
});
|
|
backgroundPositionProperty.register(Style);
|
|
|
|
function convertToBackgrounds(this: void, value: string): [CssProperty<any, any>, any][] {
|
|
if (typeof value === "string") {
|
|
const backgrounds = parser.parseBackground(value).value;
|
|
const backgroundColor = backgrounds.color ? new Color(backgrounds.color) : unsetValue;
|
|
const backgroundImage = backgrounds.image || unsetValue;
|
|
const backgroundRepeat = backgrounds.repeat || unsetValue;
|
|
const backgroundPosition = backgrounds.position ? backgrounds.position.text : unsetValue;
|
|
|
|
return [
|
|
[backgroundColorProperty, backgroundColor],
|
|
[backgroundImageProperty, backgroundImage],
|
|
[backgroundRepeatProperty, backgroundRepeat],
|
|
[backgroundPositionProperty, backgroundPosition]
|
|
];
|
|
} else {
|
|
return [
|
|
[backgroundColorProperty, unsetValue],
|
|
[backgroundImageProperty, unsetValue],
|
|
[backgroundRepeatProperty, unsetValue],
|
|
[backgroundPositionProperty, unsetValue]
|
|
];
|
|
}
|
|
}
|
|
|
|
function parseBorderColor(value: string): { top: Color, right: Color, bottom: Color, left: Color } {
|
|
let result: { top: Color, right: Color, bottom: Color, left: Color } = { top: undefined, right: undefined, bottom: undefined, left: undefined };
|
|
if (value.indexOf("rgb") === 0) {
|
|
result.top = result.right = result.bottom = result.left = new Color(value);
|
|
return result;
|
|
}
|
|
|
|
let arr = value.split(/[ ,]+/);
|
|
if (arr.length === 1) {
|
|
let arr0 = new Color(arr[0]);
|
|
result.top = arr0;
|
|
result.right = arr0;
|
|
result.bottom = arr0;
|
|
result.left = arr0;
|
|
}
|
|
else if (arr.length === 2) {
|
|
let arr0 = new Color(arr[0]);
|
|
let arr1 = new Color(arr[1]);
|
|
result.top = arr0;
|
|
result.right = arr1;
|
|
result.bottom = arr0;
|
|
result.left = arr1;
|
|
}
|
|
else if (arr.length === 3) {
|
|
let arr0 = new Color(arr[0]);
|
|
let arr1 = new Color(arr[1]);
|
|
let arr2 = new Color(arr[2]);
|
|
result.top = arr0;
|
|
result.right = arr1;
|
|
result.bottom = arr2;
|
|
result.left = arr1;
|
|
}
|
|
else if (arr.length === 4) {
|
|
let arr0 = new Color(arr[0]);
|
|
let arr1 = new Color(arr[1]);
|
|
let arr2 = new Color(arr[2]);
|
|
let arr3 = new Color(arr[3]);
|
|
result.top = arr0;
|
|
result.right = arr1;
|
|
result.bottom = arr2;
|
|
result.left = arr3;
|
|
}
|
|
else {
|
|
throw new Error(`Expected 1, 2, 3 or 4 parameters. Actual: ${value}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Border Color properties.
|
|
const borderColorProperty = new ShorthandProperty<Style, string | Color>({
|
|
name: "borderColor", cssName: "border-color",
|
|
getter: function (this: Style) {
|
|
if (Color.equals(this.borderTopColor, this.borderRightColor) &&
|
|
Color.equals(this.borderTopColor, this.borderBottomColor) &&
|
|
Color.equals(this.borderTopColor, this.borderLeftColor)) {
|
|
return this.borderTopColor;
|
|
}
|
|
else {
|
|
return `${this.borderTopColor} ${this.borderRightColor} ${this.borderBottomColor} ${this.borderLeftColor}`;
|
|
}
|
|
},
|
|
converter: function (value) {
|
|
if (typeof value === "string") {
|
|
let fourColors = parseBorderColor(value);
|
|
return [
|
|
[borderTopColorProperty, fourColors.top],
|
|
[borderRightColorProperty, fourColors.right],
|
|
[borderBottomColorProperty, fourColors.bottom],
|
|
[borderLeftColorProperty, fourColors.left]
|
|
];
|
|
}
|
|
else {
|
|
return [
|
|
[borderTopColorProperty, value],
|
|
[borderRightColorProperty, value],
|
|
[borderBottomColorProperty, value],
|
|
[borderLeftColorProperty, value]
|
|
];
|
|
}
|
|
}
|
|
});
|
|
borderColorProperty.register(Style);
|
|
|
|
export const borderTopColorProperty = new CssProperty<Style, Color>({
|
|
name: "borderTopColor", cssName: "border-top-color", valueChanged: (target, oldValue, newValue) => {
|
|
const background = target.backgroundInternal.withBorderTopColor(newValue);
|
|
target.backgroundInternal = background;
|
|
}, equalityComparer: Color.equals, valueConverter: (value) => new Color(value)
|
|
});
|
|
borderTopColorProperty.register(Style);
|
|
|
|
export const borderRightColorProperty = new CssProperty<Style, Color>({
|
|
name: "borderRightColor", cssName: "border-right-color", valueChanged: (target, oldValue, newValue) => {
|
|
const background = target.backgroundInternal.withBorderRightColor(newValue);
|
|
target.backgroundInternal = background;
|
|
}, equalityComparer: Color.equals, valueConverter: (value) => new Color(value)
|
|
});
|
|
borderRightColorProperty.register(Style);
|
|
|
|
export const borderBottomColorProperty = new CssProperty<Style, Color>({
|
|
name: "borderBottomColor", cssName: "border-bottom-color", valueChanged: (target, oldValue, newValue) => {
|
|
const background = target.backgroundInternal.withBorderBottomColor(newValue);
|
|
target.backgroundInternal = background;
|
|
}, equalityComparer: Color.equals, valueConverter: (value) => new Color(value)
|
|
});
|
|
borderBottomColorProperty.register(Style);
|
|
|
|
export const borderLeftColorProperty = new CssProperty<Style, Color>({
|
|
name: "borderLeftColor", cssName: "border-left-color", valueChanged: (target, oldValue, newValue) => {
|
|
const background = target.backgroundInternal.withBorderLeftColor(newValue);
|
|
target.backgroundInternal = background;
|
|
}, equalityComparer: Color.equals, valueConverter: (value) => new Color(value)
|
|
});
|
|
borderLeftColorProperty.register(Style);
|
|
|
|
// Border Width properties.
|
|
const borderWidthProperty = new ShorthandProperty<Style, string | Length>({
|
|
name: "borderWidth", cssName: "border-width",
|
|
getter: function (this: Style) {
|
|
if (Length.equals(this.borderTopWidth, this.borderRightWidth) &&
|
|
Length.equals(this.borderTopWidth, this.borderBottomWidth) &&
|
|
Length.equals(this.borderTopWidth, this.borderLeftWidth)) {
|
|
return this.borderTopWidth;
|
|
}
|
|
else {
|
|
return `${Length.convertToString(this.borderTopWidth)} ${Length.convertToString(this.borderRightWidth)} ${Length.convertToString(this.borderBottomWidth)} ${Length.convertToString(this.borderLeftWidth)}`;
|
|
}
|
|
},
|
|
converter: function (value) {
|
|
if (typeof value === "string" && value !== "auto") {
|
|
let borderWidths = parseThickness(value);
|
|
return [
|
|
[borderTopWidthProperty, borderWidths.top],
|
|
[borderRightWidthProperty, borderWidths.right],
|
|
[borderBottomWidthProperty, borderWidths.bottom],
|
|
[borderLeftWidthProperty, borderWidths.left]
|
|
];
|
|
}
|
|
else {
|
|
return [
|
|
[borderTopWidthProperty, value],
|
|
[borderRightWidthProperty, value],
|
|
[borderBottomWidthProperty, value],
|
|
[borderLeftWidthProperty, value]
|
|
];
|
|
}
|
|
}
|
|
});
|
|
borderWidthProperty.register(Style);
|
|
|
|
export const borderTopWidthProperty = new CssProperty<Style, Length>({
|
|
name: "borderTopWidth", cssName: "border-top-width", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
let value = Length.toDevicePixels(newValue, 0);
|
|
if (!isNonNegativeFiniteNumber(value)) {
|
|
throw new Error(`border-top-width should be Non-Negative Finite number. Value: ${value}`);
|
|
}
|
|
|
|
target.view.effectiveBorderTopWidth = value;
|
|
const background = target.backgroundInternal.withBorderTopWidth(value);
|
|
target.backgroundInternal = background;
|
|
}, valueConverter: Length.parse
|
|
});
|
|
borderTopWidthProperty.register(Style);
|
|
|
|
export const borderRightWidthProperty = new CssProperty<Style, Length>({
|
|
name: "borderRightWidth", cssName: "border-right-width", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
let value = Length.toDevicePixels(newValue, 0);
|
|
if (!isNonNegativeFiniteNumber(value)) {
|
|
throw new Error(`border-right-width should be Non-Negative Finite number. Value: ${value}`);
|
|
}
|
|
|
|
target.view.effectiveBorderRightWidth = value;
|
|
const background = target.backgroundInternal.withBorderRightWidth(value);
|
|
target.backgroundInternal = background;
|
|
}, valueConverter: Length.parse
|
|
});
|
|
borderRightWidthProperty.register(Style);
|
|
|
|
export const borderBottomWidthProperty = new CssProperty<Style, Length>({
|
|
name: "borderBottomWidth", cssName: "border-bottom-width", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
let value = Length.toDevicePixels(newValue, 0);
|
|
if (!isNonNegativeFiniteNumber(value)) {
|
|
throw new Error(`border-bottom-width should be Non-Negative Finite number. Value: ${value}`);
|
|
}
|
|
|
|
target.view.effectiveBorderBottomWidth = value;
|
|
const background = target.backgroundInternal.withBorderBottomWidth(value);
|
|
target.backgroundInternal = background;
|
|
}, valueConverter: Length.parse
|
|
});
|
|
borderBottomWidthProperty.register(Style);
|
|
|
|
export const borderLeftWidthProperty = new CssProperty<Style, Length>({
|
|
name: "borderLeftWidth", cssName: "border-left-width", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
|
|
valueChanged: (target, oldValue, newValue) => {
|
|
let value = Length.toDevicePixels(newValue, 0);
|
|
if (!isNonNegativeFiniteNumber(value)) {
|
|
throw new Error(`border-left-width should be Non-Negative Finite number. Value: ${value}`);
|
|
}
|
|
|
|
target.view.effectiveBorderLeftWidth = value;
|
|
const background = target.backgroundInternal.withBorderLeftWidth(value);
|
|
target.backgroundInternal = background;
|
|
}, valueConverter: Length.parse
|
|
});
|
|
borderLeftWidthProperty.register(Style);
|
|
|
|
// Border Radius properties.
|
|
const borderRadiusProperty = new ShorthandProperty<Style, string | Length>({
|
|
name: "borderRadius", cssName: "border-radius",
|
|
getter: function (this: Style) {
|
|
if (Length.equals(this.borderTopLeftRadius, this.borderTopRightRadius) &&
|
|
Length.equals(this.borderTopLeftRadius, this.borderBottomRightRadius) &&
|
|
Length.equals(this.borderTopLeftRadius, this.borderBottomLeftRadius)) {
|
|
return this.borderTopLeftRadius;
|
|
}
|
|
return `${Length.convertToString(this.borderTopLeftRadius)} ${Length.convertToString(this.borderTopRightRadius)} ${Length.convertToString(this.borderBottomRightRadius)} ${Length.convertToString(this.borderBottomLeftRadius)}`;
|
|
},
|
|
converter: function (value) {
|
|
if (typeof value === "string") {
|
|
let borderRadius = parseThickness(value);
|
|
return [
|
|
[borderTopLeftRadiusProperty, borderRadius.top],
|
|
[borderTopRightRadiusProperty, borderRadius.right],
|
|
[borderBottomRightRadiusProperty, borderRadius.bottom],
|
|
[borderBottomLeftRadiusProperty, borderRadius.left]
|
|
];
|
|
}
|
|
else {
|
|
return [
|
|
[borderTopLeftRadiusProperty, value],
|
|
[borderTopRightRadiusProperty, value],
|
|
[borderBottomRightRadiusProperty, value],
|
|
[borderBottomLeftRadiusProperty, value]
|
|
];
|
|
}
|
|
}
|
|
});
|
|
borderRadiusProperty.register(Style);
|
|
|
|
export const borderTopLeftRadiusProperty = new CssProperty<Style, Length>({
|
|
name: "borderTopLeftRadius", cssName: "border-top-left-radius", defaultValue: 0, affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
|
|
let value = Length.toDevicePixels(newValue, 0);
|
|
if (!isNonNegativeFiniteNumber(value)) {
|
|
throw new Error(`border-top-left-radius should be Non-Negative Finite number. Value: ${value}`);
|
|
}
|
|
const background = target.backgroundInternal.withBorderTopLeftRadius(value);
|
|
target.backgroundInternal = background;
|
|
}, valueConverter: Length.parse
|
|
});
|
|
borderTopLeftRadiusProperty.register(Style);
|
|
|
|
export const borderTopRightRadiusProperty = new CssProperty<Style, Length>({
|
|
name: "borderTopRightRadius", cssName: "border-top-right-radius", defaultValue: 0, affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
|
|
let value = Length.toDevicePixels(newValue, 0);
|
|
if (!isNonNegativeFiniteNumber(value)) {
|
|
throw new Error(`border-top-right-radius should be Non-Negative Finite number. Value: ${value}`);
|
|
}
|
|
const background = target.backgroundInternal.withBorderTopRightRadius(value);
|
|
target.backgroundInternal = background;
|
|
}, valueConverter: Length.parse
|
|
});
|
|
borderTopRightRadiusProperty.register(Style);
|
|
|
|
export const borderBottomRightRadiusProperty = new CssProperty<Style, Length>({
|
|
name: "borderBottomRightRadius", cssName: "border-bottom-right-radius", defaultValue: 0, affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
|
|
let value = Length.toDevicePixels(newValue, 0);
|
|
if (!isNonNegativeFiniteNumber(value)) {
|
|
throw new Error(`border-bottom-right-radius should be Non-Negative Finite number. Value: ${value}`);
|
|
}
|
|
const background = target.backgroundInternal.withBorderBottomRightRadius(value);
|
|
target.backgroundInternal = background;
|
|
}, valueConverter: Length.parse
|
|
});
|
|
borderBottomRightRadiusProperty.register(Style);
|
|
|
|
export const borderBottomLeftRadiusProperty = new CssProperty<Style, Length>({
|
|
name: "borderBottomLeftRadius", cssName: "border-bottom-left-radius", defaultValue: 0, affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
|
|
let value = Length.toDevicePixels(newValue, 0);
|
|
if (!isNonNegativeFiniteNumber(value)) {
|
|
throw new Error(`border-bottom-left-radius should be Non-Negative Finite number. Value: ${value}`);
|
|
}
|
|
const background = target.backgroundInternal.withBorderBottomLeftRadius(value);
|
|
target.backgroundInternal = background;
|
|
}, valueConverter: Length.parse
|
|
});
|
|
borderBottomLeftRadiusProperty.register(Style);
|
|
|
|
function isNonNegativeFiniteNumber(value: number): boolean {
|
|
return isFinite(value) && !isNaN(value) && value >= 0;
|
|
}
|
|
|
|
let supportedPaths = ["rect", "circle", "ellipse", "polygon", "inset"];
|
|
function isClipPathValid(value: string): boolean {
|
|
if (!value) {
|
|
return true;
|
|
}
|
|
let functionName = value.substring(0, value.indexOf("(")).trim();
|
|
return supportedPaths.indexOf(functionName) !== -1;
|
|
}
|
|
|
|
export const clipPathProperty = new CssProperty<Style, string>({
|
|
name: "clipPath", cssName: "clip-path", valueChanged: (target, oldValue, newValue) => {
|
|
if (!isClipPathValid(newValue)) {
|
|
throw new Error("clip-path is not valid.");
|
|
}
|
|
|
|
const background = target.backgroundInternal.withClipPath(newValue);
|
|
target.backgroundInternal = background;
|
|
}
|
|
});
|
|
clipPathProperty.register(Style);
|
|
|
|
function isFloatValueConverter(value: string): number {
|
|
let newValue = parseFloat(value);
|
|
if (isNaN(newValue)) {
|
|
throw new Error(`Invalid value: ${newValue}`);
|
|
}
|
|
|
|
return newValue;
|
|
}
|
|
|
|
export const zIndexProperty = new CssProperty<Style, number>({ name: "zIndex", cssName: "z-index", valueConverter: isFloatValueConverter });
|
|
zIndexProperty.register(Style);
|
|
|
|
function opacityConverter(value: any): number {
|
|
let newValue = parseFloat(value);
|
|
if (!isNaN(newValue) && 0 <= newValue && newValue <= 1) {
|
|
return newValue;
|
|
}
|
|
|
|
throw new Error(`Opacity should be between [0, 1]. Value: ${newValue}`);
|
|
}
|
|
|
|
export const opacityProperty = new CssAnimationProperty<Style, number>({ name: "opacity", cssName: "opacity", defaultValue: 1, valueConverter: opacityConverter });
|
|
opacityProperty.register(Style);
|
|
|
|
export const colorProperty = new InheritedCssProperty<Style, Color>({ name: "color", cssName: "color", equalityComparer: Color.equals, valueConverter: (v) => new Color(v) });
|
|
colorProperty.register(Style);
|
|
|
|
export const fontInternalProperty = new CssProperty<Style, Font>({ name: "fontInternal", cssName: "_fontInternal", defaultValue: Font.default });
|
|
fontInternalProperty.register(Style);
|
|
|
|
export const fontFamilyProperty = new InheritedCssProperty<Style, string>({
|
|
name: "fontFamily", cssName: "font-family", affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
|
|
let currentFont = target.fontInternal;
|
|
if (currentFont.fontFamily !== newValue) {
|
|
const newFont = currentFont.withFontFamily(newValue);
|
|
target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
|
|
}
|
|
}
|
|
});
|
|
fontFamilyProperty.register(Style);
|
|
|
|
export const fontSizeProperty = new InheritedCssProperty<Style, number>({
|
|
name: "fontSize", cssName: "font-size", affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
|
|
let currentFont = target.fontInternal;
|
|
if (currentFont.fontSize !== newValue) {
|
|
const newFont = currentFont.withFontSize(newValue);
|
|
target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
|
|
}
|
|
},
|
|
valueConverter: (v) => parseFloat(v)
|
|
});
|
|
fontSizeProperty.register(Style);
|
|
|
|
export const fontStyleProperty = new InheritedCssProperty<Style, FontStyle>({
|
|
name: "fontStyle", cssName: "font-style", affectsLayout: isIOS, defaultValue: FontStyle.NORMAL, valueConverter: FontStyle.parse, valueChanged: (target, oldValue, newValue) => {
|
|
let currentFont = target.fontInternal;
|
|
if (currentFont.fontStyle !== newValue) {
|
|
const newFont = currentFont.withFontStyle(newValue);
|
|
target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
|
|
}
|
|
}
|
|
});
|
|
fontStyleProperty.register(Style);
|
|
|
|
export const fontWeightProperty = new InheritedCssProperty<Style, FontWeight>({
|
|
name: "fontWeight", cssName: "font-weight", affectsLayout: isIOS, defaultValue: FontWeight.NORMAL, valueConverter: FontWeight.parse, valueChanged: (target, oldValue, newValue) => {
|
|
let currentFont = target.fontInternal;
|
|
if (currentFont.fontWeight !== newValue) {
|
|
const newFont = currentFont.withFontWeight(newValue);
|
|
target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
|
|
}
|
|
}
|
|
});
|
|
fontWeightProperty.register(Style);
|
|
|
|
const fontProperty = new ShorthandProperty<Style, string>({
|
|
name: "font", cssName: "font",
|
|
getter: function (this: Style) {
|
|
return `${this.fontStyle} ${this.fontWeight} ${this.fontSize} ${this.fontFamily}`;
|
|
},
|
|
converter: function (value) {
|
|
if (value === unsetValue) {
|
|
return [
|
|
[fontStyleProperty, unsetValue],
|
|
[fontWeightProperty, unsetValue],
|
|
[fontSizeProperty, unsetValue],
|
|
[fontFamilyProperty, unsetValue]
|
|
];
|
|
} else {
|
|
const font = parseFont(value);
|
|
const fontSize = parseFloat(font.fontSize);
|
|
|
|
return [
|
|
[fontStyleProperty, font.fontStyle],
|
|
[fontWeightProperty, font.fontWeight],
|
|
[fontSizeProperty, fontSize],
|
|
[fontFamilyProperty, font.fontFamily]
|
|
];
|
|
}
|
|
}
|
|
});
|
|
fontProperty.register(Style);
|
|
|
|
export type Visibility = "visible" | "hidden" | "collapse";
|
|
export namespace Visibility {
|
|
export const VISIBLE: "visible" = "visible";
|
|
export const HIDDEN: "hidden" = "hidden";
|
|
export const COLLAPSE: "collapse" = "collapse";
|
|
export const isValid = makeValidator<Visibility>(VISIBLE, HIDDEN, COLLAPSE);
|
|
export const parse = (value: string) => value.toLowerCase() === "collapsed" ? COLLAPSE : parseStrict(value);
|
|
const parseStrict = makeParser<Visibility>(isValid);
|
|
}
|
|
|
|
export const visibilityProperty = new CssProperty<Style, Visibility>({
|
|
name: "visibility", cssName: "visibility", defaultValue: Visibility.VISIBLE, affectsLayout: isIOS, valueConverter: Visibility.parse, valueChanged: (target, oldValue, newValue) => {
|
|
target.view.isCollapsed = (newValue === Visibility.COLLAPSE);
|
|
}
|
|
});
|
|
visibilityProperty.register(Style); |