feat(core): style properties module improvements and organization (#10685)

This commit is contained in:
Dimitris-Rafail Katsampas
2025-02-04 23:26:21 +02:00
committed by GitHub
parent 56af0b2f7e
commit 3a7206fc3b
13 changed files with 525 additions and 469 deletions

View File

@ -29,8 +29,9 @@ export namespace CoreTypes {
export type LengthPxUnit = { readonly unit: 'px'; readonly value: px };
export type LengthPercentUnit = { readonly unit: '%'; readonly value: percent };
export type LengthType = 'auto' | dip | LengthDipUnit | LengthPxUnit;
export type PercentLengthType = 'auto' | dip | LengthDipUnit | LengthPxUnit | LengthPercentUnit;
export type FixedLengthType = dip | LengthDipUnit | LengthPxUnit;
export type LengthType = 'auto' | FixedLengthType;
export type PercentLengthType = 'auto' | FixedLengthType | LengthPercentUnit;
export const zeroLength: LengthType = {
value: 0,

View File

@ -8,13 +8,19 @@ export type Transformation = {
value: TransformationValue;
};
export type TransformationType = 'rotate' | 'translate' | 'translateX' | 'translateY' | 'scale' | 'scaleX' | 'scaleY';
export type TransformationType = 'rotate' | 'rotate3d' | 'rotateX' | 'rotateY' | 'translate' | 'translate3d' | 'translateX' | 'translateY' | 'scale' | 'scale3d' | 'scaleX' | 'scaleY';
export type TransformationValue = Pair | number;
export type TransformationValue = Point3D | Pair | number;
export interface Point3D {
x: number;
y: number;
z: number;
}
export type TransformFunctionsInfo = {
translate: Pair;
rotate: number;
rotate: Point3D;
scale: Pair;
};
@ -55,7 +61,7 @@ export interface AnimationDefinition {
scale?: Pair;
height?: CoreTypes.PercentLengthType | string;
width?: CoreTypes.PercentLengthType | string;
rotate?: number;
rotate?: Point3D;
duration?: number;
delay?: number;
iterations?: number;

View File

@ -83,7 +83,7 @@ export type Transformation = {
/**
* Defines possible css transformations
*/
export type TransformationType = 'rotate' | 'rotateX' | 'rotateY' | 'translate' | 'translateX' | 'translateY' | 'scale' | 'scaleX' | 'scaleY';
export type TransformationType = 'rotate' | 'rotate3d' | 'rotateX' | 'rotateY' | 'translate' | 'translate3d' | 'translateX' | 'translateY' | 'scale' | 'scale3d' | 'scaleX' | 'scaleY';
/**
* Defines possible css transformation values

View File

@ -3,6 +3,7 @@ import { LinearGradient } from './linear-gradient';
// Types.
import { Color } from '../../color';
import { BoxShadow } from './box-shadow';
import { ClipPathFunction } from './clip-path-function';
/**
* Flags used to hint the background handler if it has to clear a specific property
@ -39,7 +40,7 @@ export class Background {
public borderTopRightRadius = 0;
public borderBottomLeftRadius = 0;
public borderBottomRightRadius = 0;
public clipPath: string;
public clipPath: string | ClipPathFunction;
public boxShadow: BoxShadow;
public clearFlags: number = BackgroundClearFlags.NONE;
@ -192,7 +193,7 @@ export class Background {
return clone;
}
public withClipPath(value: string): Background {
public withClipPath(value: string | ClipPathFunction): Background {
const clone = this.clone();
clone.clipPath = value;
@ -224,16 +225,23 @@ export class Background {
return false;
}
let imagesEqual = false;
let isImageEqual = false;
if (value1 instanceof LinearGradient && value2 instanceof LinearGradient) {
imagesEqual = LinearGradient.equals(value1, value2);
isImageEqual = LinearGradient.equals(value1, value2);
} else {
imagesEqual = value1.image === value2.image;
isImageEqual = value1.image === value2.image;
}
let isClipPathEqual = false;
if (value1.clipPath instanceof ClipPathFunction && value2.clipPath instanceof ClipPathFunction) {
isClipPathEqual = ClipPathFunction.equals(value1.clipPath, value2.clipPath);
} else {
isClipPathEqual = value1.clipPath === value2.clipPath;
}
return (
Color.equals(value1.color, value2.color) &&
imagesEqual &&
isImageEqual &&
value1.position === value2.position &&
value1.repeat === value2.repeat &&
value1.size === value2.size &&
@ -249,7 +257,7 @@ export class Background {
value1.borderTopRightRadius === value2.borderTopRightRadius &&
value1.borderBottomRightRadius === value2.borderBottomRightRadius &&
value1.borderBottomLeftRadius === value2.borderBottomLeftRadius &&
value1.clipPath === value2.clipPath
isClipPathEqual
// && value1.clearFlags === value2.clearFlags
);
}

View File

@ -1,5 +1,6 @@
import { View } from '../core/view';
import { LinearGradient } from './linear-gradient';
import { ClipPathFunction } from './clip-path-function';
import { isDataURI, isFileOrResourcePath, RESOURCE_PREFIX, FILE_PREFIX } from '../../utils';
import { parse } from '../../css-value';
import { path, knownFolders } from '../../file-system';
@ -92,7 +93,7 @@ export function refreshBorderDrawable(view: View, borderDrawable: org.nativescri
background.borderBottomRightRadius,
background.borderBottomLeftRadius,
background.clipPath,
background.clipPath instanceof ClipPathFunction ? background.clipPath.toString() : background.clipPath,
background.color ? background.color.android : 0,
imageUri,
@ -103,7 +104,7 @@ export function refreshBorderDrawable(view: View, borderDrawable: org.nativescri
background.position,
backgroundPositionParsedCSSValues,
background.size,
backgroundSizeParsedCSSValues
backgroundSizeParsedCSSValues,
);
//console.log(`>>> ${borderDrawable.toDebugString()}`);
}

View File

@ -1,7 +1,8 @@
import { Color } from '../../color';
import { View } from '../core/view';
import { BackgroundRepeat } from '../../css/parser';
import { LinearGradient } from '../styling/linear-gradient';
import { LinearGradient } from './linear-gradient';
import { ClipPathFunction } from './clip-path-function';
import { BoxShadow } from './box-shadow';
import { Background as BackgroundDefinition } from './background-common';
@ -32,7 +33,7 @@ export enum CacheMode {
// public borderTopRightRadius: number;
// public borderBottomRightRadius: number;
// public borderBottomLeftRadius: number;
// public clipPath: string;
// public clipPath: string | ClipPathFunction;
// public boxShadow: string | BoxShadow;
// public clearFlags: number;
@ -80,12 +81,12 @@ export namespace ios {
export function drawBackgroundVisualEffects(view: View): void;
export function clearBackgroundVisualEffects(view: View): void;
export function createUIImageFromURI(view: View, imageURI: string, flip: boolean, callback: (image: any) => void): void;
export function generateClipPath(view: View, bounds: CGRect): any;
export function generateShadowLayerPaths(view: View, bounds: CGRect): { maskPath: any; shadowPath: any };
export function getUniformBorderRadius(view: View, bounds: CGRect): number;
export function generateNonUniformBorderInnerClipRoundedPath(view: View, bounds: CGRect): any;
export function generateNonUniformBorderOuterClipRoundedPath(view: View, bounds: CGRect): any;
export function generateNonUniformMultiColorBorderRoundedPaths(view: View, bounds: CGRect): Array<any>;
export function generateClipPath(view: View, bounds: any /* CGRect */): any;
export function generateShadowLayerPaths(view: View, bounds: any /* CGRect */): { maskPath: any; shadowPath: any };
export function getUniformBorderRadius(view: View, bounds: any /* CGRect */): number;
export function generateNonUniformBorderInnerClipRoundedPath(view: View, bounds: any /* CGRect */): any;
export function generateNonUniformBorderOuterClipRoundedPath(view: View, bounds: any /* CGRect */): any;
export function generateNonUniformMultiColorBorderRoundedPaths(view: View, bounds: any /* CGRect */): Array<any>;
}
export namespace ad {

View File

@ -11,6 +11,7 @@ import { parse as cssParse } from '../../css-value/reworkcss-value.js';
import { BoxShadow } from './box-shadow';
import { Length } from './style-properties';
import { BackgroundClearFlags } from './background-common';
import { ClipPathFunction } from './clip-path-function';
export * from './background-common';
@ -303,30 +304,28 @@ export namespace ios {
let path: UIBezierPath;
const clipPath = background.clipPath;
const functionName: string = clipPath.substring(0, clipPath.indexOf('('));
const value: string = clipPath.replace(`${functionName}(`, '').replace(')', '');
switch (functionName) {
case 'rect':
path = rectPath(value, position);
break;
case 'inset':
path = insetPath(value, position);
break;
case 'circle':
path = circlePath(value, position);
break;
case 'ellipse':
path = ellipsePath(value, position);
break;
case 'polygon':
path = polygonPath(value, position);
break;
if (clipPath instanceof ClipPathFunction) {
switch (clipPath.shape) {
case 'rect':
path = rectPath(clipPath.rule, position);
break;
case 'inset':
path = insetPath(clipPath.rule, position);
break;
case 'circle':
path = circlePath(clipPath.rule, position);
break;
case 'ellipse':
path = ellipsePath(clipPath.rule, position);
break;
case 'polygon':
path = polygonPath(clipPath.rule, position);
break;
}
} else {
path = null;
}
return path;
}

View File

@ -0,0 +1,39 @@
type ClipPathShape = 'rect' | 'circle' | 'ellipse' | 'polygon' | 'inset';
interface IClipPathFunction {
shape: ClipPathShape;
rule: string;
}
export class ClipPathFunction implements IClipPathFunction {
private readonly _shape: ClipPathShape;
private readonly _rule: string;
constructor(shape: ClipPathShape, rule: string) {
this._shape = shape;
this._rule = rule;
}
get shape(): ClipPathShape {
return this._shape;
}
get rule(): string {
return this._rule;
}
public static equals(value1: ClipPathFunction, value2: ClipPathFunction): boolean {
return value1.shape === value2.shape && value1.rule === value2.rule;
}
toJSON(): IClipPathFunction {
return {
shape: this._shape,
rule: this._rule,
};
}
toString(): string {
return `${this._shape}(${this._rule})`;
}
}

View File

@ -3,7 +3,7 @@ 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';
import { transformConverter } from '../styling/css-transform';
import { cleanupImportantFlags } from './css-utils';
const ANIMATION_PROPERTY_HANDLERS = Object.freeze({

View File

@ -0,0 +1,133 @@
import { Pair, Transformation, TransformationType, TransformationValue, TransformFunctionsInfo } from '../animation';
import { radiansToDegrees } from '../../utils/number-utils';
import { decompose2DTransformMatrix, getTransformMatrix, matrixArrayToCssMatrix, multiplyAffine2d } from '../../matrix';
import { hasDuplicates } from '../../utils';
type TransformationStyleMap = {
[key: string]: (value: TransformationValue) => Transformation;
};
const IDENTITY_TRANSFORMATION = {
translate: { x: 0, y: 0 },
rotate: { x: 0, y: 0, z: 0 },
scale: { x: 1, y: 1 },
};
const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g);
const TRANSFORMATIONS = Object.freeze<TransformationType[]>(['rotate', 'rotateX', 'rotateY', 'rotate3d', 'translate', 'translate3d', 'translateX', 'translateY', 'scale', 'scale3d', 'scaleX', 'scaleY']);
const STYLE_TRANSFORMATION_MAP: TransformationStyleMap = Object.freeze<TransformationStyleMap>({
scale: (value: number) => ({ property: 'scale', value }),
scale3d: (value: number) => ({ property: 'scale', value }),
scaleX: ({ x }: Pair) => ({
property: 'scale',
value: { x, y: IDENTITY_TRANSFORMATION.scale.y },
}),
scaleY: ({ y }: Pair) => ({
property: 'scale',
value: { y, x: IDENTITY_TRANSFORMATION.scale.x },
}),
translate: (value) => ({ property: 'translate', value }),
translate3d: (value) => ({ property: 'translate', value }),
translateX: ({ x }: Pair) => ({
property: 'translate',
value: { x, y: IDENTITY_TRANSFORMATION.translate.y },
}),
translateY: ({ y }: Pair) => ({
property: 'translate',
value: { y, x: IDENTITY_TRANSFORMATION.translate.x },
}),
rotate3d: (value) => ({ property: 'rotate', value }),
rotateX: (x: number) => ({
property: 'rotate',
value: {
x,
y: IDENTITY_TRANSFORMATION.rotate.y,
z: IDENTITY_TRANSFORMATION.rotate.z,
},
}),
rotateY: (y: number) => ({
property: 'rotate',
value: {
x: IDENTITY_TRANSFORMATION.rotate.x,
y,
z: IDENTITY_TRANSFORMATION.rotate.z,
},
}),
rotate: (z: number) => ({
property: 'rotate',
value: {
x: IDENTITY_TRANSFORMATION.rotate.x,
y: IDENTITY_TRANSFORMATION.rotate.y,
z,
},
}),
});
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 = { ...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);
}
function isTransformType(propertyName: string): propertyName is TransformationType {
return (TRANSFORMATIONS as string[]).indexOf(propertyName) !== -1;
}
// 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[] {
const matches: Transformation[] = [];
let match: RegExpExecArray;
while ((match = TRANSFORM_SPLITTER.exec(text)) !== null) {
const property = match[1];
if (isTransformType(property)) {
const value = convertTransformValue(property, match[2]);
matches.push(STYLE_TRANSFORMATION_MAP[property](value));
}
}
return matches;
}
function convertTransformValue(property: TransformationType, rawValue: string): TransformationValue {
const values = rawValue.split(',').map(parseFloat);
const x = values[0];
let y = values[1];
let z = values[2];
if (property === 'translate') {
y ??= IDENTITY_TRANSFORMATION.translate.y;
} else {
y ??= x;
z ??= y;
}
if (property === 'rotate' || property === 'rotateX' || property === 'rotateY') {
return rawValue.slice(-3) === 'rad' ? radiansToDegrees(x) : x;
}
return { x, y, z };
}

View File

@ -1,17 +1,29 @@
import { TransformFunctionsInfo } from '../animation';
import { CoreTypes } from '../../core-types';
import { Color } from '../../color';
import { CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from '../core/properties';
import { Style } from './style';
import { Font, FontStyleType, FontWeightType, FontVariationSettingsType } from './font';
import { Background } from './background';
import { ClipPathFunction } from './clip-path-function';
import { LinearGradient } from './linear-gradient';
export namespace FixedLength {
export function parse(text: string): CoreTypes.FixedLengthType;
export function equals(a: CoreTypes.FixedLengthType, b: CoreTypes.FixedLengthType): boolean;
/**
* Converts FixedLengthType unit to device pixels.
* @param length The FixedLengthType to convert.
*/
export function toDevicePixels(length: CoreTypes.FixedLengthType): number;
export function convertToString(length: CoreTypes.FixedLengthType): string;
}
export namespace Length {
export function parse(text: string): CoreTypes.LengthType;
export function equals(a: CoreTypes.LengthType, b: CoreTypes.LengthType): boolean;
/**
* Converts Length unit to device pixels.
* @param length The Length to convert.
* Converts LengthType unit to device pixels.
* @param length The LengthType to convert.
* @param auto Value to use for conversion of "auto". By default is Math.NaN.
*/
export function toDevicePixels(length: CoreTypes.LengthType, auto?: number): number;
@ -39,14 +51,12 @@ export const scaleYProperty: CssAnimationProperty<Style, number>;
export const translateXProperty: CssAnimationProperty<Style, CoreTypes.dip>;
export const translateYProperty: CssAnimationProperty<Style, CoreTypes.dip>;
export function transformConverter(text: string): TransformFunctionsInfo;
export const clipPathProperty: CssProperty<Style, string>;
export const clipPathProperty: CssProperty<Style, string | ClipPathFunction>;
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 backgroundImageProperty: CssProperty<Style, string | LinearGradient>;
export const backgroundRepeatProperty: CssProperty<Style, CoreTypes.BackgroundRepeatType>;
export const backgroundSizeProperty: CssProperty<Style, string>;
export const backgroundPositionProperty: CssProperty<Style, string>;

View File

@ -1,59 +1,59 @@
// Types
import { unsetValue, CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from '../core/properties';
import { Style } from './style';
import { Transformation, TransformationValue, TransformFunctionsInfo } from '../animation';
import { Color } from '../../color';
import { Font, parseFont, FontStyle, FontStyleType, FontWeight, FontWeightType, FontVariationSettings, FontVariationSettingsType } from './font';
import { Background } from './background';
import { layout, hasDuplicates } from '../../utils';
import { layout } from '../../utils';
import { radiansToDegrees } from '../../utils/number-utils';
import { decompose2DTransformMatrix, getTransformMatrix, matrixArrayToCssMatrix, multiplyAffine2d } from '../../matrix';
import { Trace } from '../../trace';
import { CoreTypes } from '../../core-types';
import { parseBackground } from '../../css/parser';
import { LinearGradient } from './linear-gradient';
import { parseCSSShadow, ShadowCSSValues } from './css-shadow';
import { transformConverter } from './css-transform';
import { ClipPathFunction } from './clip-path-function';
interface ShorthandPositioning {
top: string;
right: string;
bottom: string;
left: string;
}
function equalsCommon(a: CoreTypes.LengthType, b: CoreTypes.LengthType): boolean;
function equalsCommon(a: CoreTypes.PercentLengthType, b: CoreTypes.PercentLengthType): boolean;
function equalsCommon(a: CoreTypes.PercentLengthType, b: CoreTypes.PercentLengthType): boolean {
if (a == 'auto') {
// tslint:disable-line
return b == 'auto'; // tslint:disable-line
return b == 'auto';
}
if (b == 'auto') {
return false;
}
if (typeof a === 'number') {
if (b == 'auto') {
// tslint:disable-line
return false;
}
if (typeof b === 'number') {
return a == b; // tslint:disable-line
return a == b;
}
if (!b) {
return false;
}
return b.unit == 'dip' && a == b.value; // tslint:disable-line
}
if (b == 'auto') {
// tslint:disable-line
return false;
return b.unit == 'dip' && a == b.value;
}
if (typeof b === 'number') {
return a ? a.unit == 'dip' && a.value == b : false; // tslint:disable-line
return a ? a.unit == 'dip' && a.value == b : false;
}
if (!a || !b) {
return false;
}
return a.value == b.value && a.unit == b.unit; // tslint:disable-line
return a.value == b.value && a.unit == b.unit;
}
function convertToStringCommon(length: CoreTypes.LengthType | CoreTypes.PercentLengthType): string {
if (length == 'auto') {
// tslint:disable-line
return 'auto';
}
@ -71,7 +71,6 @@ function convertToStringCommon(length: CoreTypes.LengthType | CoreTypes.PercentL
function toDevicePixelsCommon(length: CoreTypes.PercentLengthType, auto: number = Number.NaN, parentAvailableWidth: number = Number.NaN): number {
if (length == 'auto') {
// tslint:disable-line
return auto;
}
if (typeof length === 'number') {
@ -94,9 +93,9 @@ function toDevicePixelsCommon(length: CoreTypes.PercentLengthType, auto: number
export namespace PercentLength {
export function parse(fromValue: string | CoreTypes.LengthType): CoreTypes.PercentLengthType {
if (fromValue == 'auto') {
// tslint:disable-line
return 'auto';
}
if (typeof fromValue === 'string') {
let stringValue = fromValue.trim();
const percentIndex = stringValue.indexOf('%');
@ -147,12 +146,8 @@ export namespace PercentLength {
} = convertToStringCommon;
}
export namespace Length {
export function parse(fromValue: string | CoreTypes.LengthType): CoreTypes.LengthType {
if (fromValue == 'auto') {
// tslint:disable-line
return 'auto';
}
export namespace FixedLength {
export function parse(fromValue: string | CoreTypes.FixedLengthType): CoreTypes.FixedLengthType {
if (typeof fromValue === 'string') {
let stringValue = fromValue.trim();
if (stringValue.indexOf('px') !== -1) {
@ -175,6 +170,23 @@ export namespace Length {
return fromValue;
}
}
export const equals: { (a: CoreTypes.FixedLengthType, b: CoreTypes.FixedLengthType): boolean } = equalsCommon;
export const toDevicePixels: {
(length: CoreTypes.FixedLengthType): number;
} = toDevicePixelsCommon;
export const convertToString: {
(length: CoreTypes.FixedLengthType): string;
} = convertToStringCommon;
}
export namespace Length {
export function parse(fromValue: string | CoreTypes.LengthType): CoreTypes.LengthType {
if (fromValue == 'auto') {
return 'auto';
}
return FixedLength.parse(fromValue);
}
export const equals: { (a: CoreTypes.LengthType, b: CoreTypes.LengthType): boolean } = equalsCommon;
export const toDevicePixels: {
(length: CoreTypes.LengthType, auto?: number): number;
@ -184,6 +196,185 @@ export namespace Length {
} = convertToStringCommon;
}
function isNonNegativeFiniteNumber(value: number): boolean {
return isFinite(value) && !isNaN(value) && value >= 0;
}
function parseClipPath(value: string): string | ClipPathFunction {
const functionStartIndex = value.indexOf('(');
if (functionStartIndex > -1) {
const functionName = value.substring(0, functionStartIndex).trim();
switch (functionName) {
case 'rect':
case 'circle':
case 'ellipse':
case 'polygon':
case 'inset': {
const rule: string = value.replace(`${functionName}(`, '').replace(')', '');
return new ClipPathFunction(functionName, rule);
}
default:
throw new Error(`Clip-path function ${functionName} is not valid.`);
}
} else {
if (value === 'none') {
return null;
}
// Only shape functions and none are supported for now
throw new Error(`Clip-path value ${value} is not valid.`);
}
}
function parseShorthandPositioning(value: string): ShorthandPositioning {
const 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,
};
}
function parseBorderColorPositioning(value: string): ShorthandPositioning {
if (value.indexOf('rgb') === 0 || value.indexOf('hsl') === 0) {
return {
top: value,
right: value,
bottom: value,
left: value,
};
}
return parseShorthandPositioning(value);
}
function convertToBackgrounds(value: string): [CssProperty<any, any> | CssAnimationProperty<any, any>, any][] {
if (typeof value === 'string') {
const backgrounds = parseBackground(value).value;
let backgroundColor = unsetValue;
if (backgrounds.color) {
backgroundColor = backgrounds.color instanceof Color ? backgrounds.color : new Color(backgrounds.color);
}
let backgroundImage: string | LinearGradient;
if (typeof backgrounds.image === 'object' && backgrounds.image) {
backgroundImage = LinearGradient.parse(backgrounds.image);
} else {
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 convertToMargins(value: string | CoreTypes.PercentLengthType): [CssProperty<Style, CoreTypes.PercentLengthType>, CoreTypes.PercentLengthType][] {
if (typeof value === 'string' && value !== 'auto') {
const thickness = parseShorthandPositioning(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(value: string | CoreTypes.LengthType): [CssProperty<Style, CoreTypes.LengthType>, CoreTypes.LengthType][] {
if (typeof value === 'string' && value !== 'auto') {
const thickness = parseShorthandPositioning(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],
];
}
}
function convertToTransform(value: string): [CssAnimationProperty<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.z],
[rotateXProperty, rotate.x],
[rotateYProperty, rotate.y],
];
}
export const minWidthProperty = new CssProperty<Style, CoreTypes.LengthType>({
name: 'minWidth',
cssName: 'min-width',
@ -416,97 +607,6 @@ export const verticalAlignmentProperty = new CssProperty<Style, CoreTypes.Vertic
});
verticalAlignmentProperty.register(Style);
interface Thickness {
top: string;
right: string;
bottom: string;
left: string;
}
function parseThickness(value: string): Thickness {
if (typeof value === 'string') {
const 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 | CoreTypes.PercentLengthType): [CssProperty<any, any>, any][] {
if (typeof value === 'string' && value !== 'auto') {
const 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 | CoreTypes.LengthType): [CssProperty<any, any>, any][] {
if (typeof value === 'string' && value !== 'auto') {
const 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',
@ -555,27 +655,21 @@ export const scaleYProperty = new CssAnimationProperty<Style, number>({
});
scaleYProperty.register(Style);
function parseDIPs(value: string): CoreTypes.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, CoreTypes.dip>({
export const translateXProperty = new CssAnimationProperty<Style, CoreTypes.FixedLengthType>({
name: 'translateX',
cssName: 'translateX',
defaultValue: 0,
valueConverter: parseDIPs,
equalityComparer: FixedLength.equals,
valueConverter: FixedLength.parse,
});
translateXProperty.register(Style);
export const translateYProperty = new CssAnimationProperty<Style, CoreTypes.dip>({
export const translateYProperty = new CssAnimationProperty<Style, CoreTypes.FixedLengthType>({
name: 'translateY',
cssName: 'translateY',
defaultValue: 0,
valueConverter: parseDIPs,
equalityComparer: FixedLength.equals,
valueConverter: FixedLength.parse,
});
translateYProperty.register(Style);
@ -608,148 +702,6 @@ const transformProperty = new ShorthandProperty<Style, string>({
});
transformProperty.register(Style);
const IDENTITY_TRANSFORMATION = {
translate: { x: 0, y: 0 },
rotate: { x: 0, y: 0, z: 0 },
scale: { x: 1, y: 1 },
};
const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g);
const TRANSFORMATIONS = Object.freeze(['rotate', 'rotateX', 'rotateY', 'rotate3d', '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 },
}),
rotate3d: (value) => ({ property: 'rotate', value }),
rotateX: (x) => ({
property: 'rotate',
value: {
x,
y: IDENTITY_TRANSFORMATION.rotate.y,
z: IDENTITY_TRANSFORMATION.rotate.z,
},
}),
rotateY: (y) => ({
property: 'rotate',
value: {
x: IDENTITY_TRANSFORMATION.rotate.x,
y,
z: IDENTITY_TRANSFORMATION.rotate.z,
},
}),
rotate: (z) => ({
property: 'rotate',
value: {
x: IDENTITY_TRANSFORMATION.rotate.x,
y: IDENTITY_TRANSFORMATION.rotate.y,
z,
},
}),
});
function convertToTransform(value: string): [CssProperty<any, any>, any][] {
if (value === unsetValue) {
value = 'none';
}
const { translate, rotate, scale } = transformConverter(value);
return [
[<any>translateXProperty, translate.x],
[<any>translateYProperty, translate.y],
[<any>scaleXProperty, scale.x],
[<any>scaleYProperty, scale.y],
[<any>rotateProperty, rotate.z],
[<any>rotateXProperty, rotate.x],
[<any>rotateYProperty, rotate.y],
];
}
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 = { ...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[] {
const 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 {
// eslint-disable-next-line prefer-const
let [x, y, z] = stringValue.split(',').map(parseFloat);
if (property === 'translate') {
y ??= IDENTITY_TRANSFORMATION.translate.y;
} else {
y ??= x;
z ??= y;
}
if (property === 'rotate' || property === 'rotateX' || property === 'rotateY') {
return stringValue.slice(-3) === 'rad' ? radiansToDegrees(x) : x;
}
return { x, y, z };
}
// Background properties.
const backgroundProperty = new ShorthandProperty<Style, string | Color>({
name: 'background',
@ -768,7 +720,6 @@ export const backgroundInternalProperty = new CssProperty<Style, Background>({
});
backgroundInternalProperty.register(Style);
// const pattern: RegExp = /url\(('|")(.*?)\1\)/;
export const backgroundImageProperty = new CssProperty<Style, string | LinearGradient>({
name: 'backgroundImage',
cssName: 'background-image',
@ -834,91 +785,6 @@ export const backgroundPositionProperty = new CssProperty<Style, string>({
});
backgroundPositionProperty.register(Style);
function convertToBackgrounds(this: void, value: string): [CssProperty<any, any>, any][] {
if (typeof value === 'string') {
const backgrounds = parseBackground(value).value;
let backgroundColor = unsetValue;
if (backgrounds.color) {
backgroundColor = backgrounds.color instanceof Color ? backgrounds.color : new Color(backgrounds.color);
}
let backgroundImage: string | LinearGradient;
if (typeof backgrounds.image === 'object' && backgrounds.image) {
backgroundImage = LinearGradient.parse(backgrounds.image);
} else {
backgroundImage = backgrounds.image || unsetValue;
}
const backgroundRepeat = backgrounds.repeat || unsetValue;
const backgroundPosition = backgrounds.position ? backgrounds.position.text : unsetValue;
return [
[<any>backgroundColorProperty, backgroundColor],
[backgroundImageProperty, backgroundImage],
[backgroundRepeatProperty, backgroundRepeat],
[backgroundPositionProperty, backgroundPosition],
];
} else {
return [
[<any>backgroundColorProperty, unsetValue],
[backgroundImageProperty, unsetValue],
[backgroundRepeatProperty, unsetValue],
[backgroundPositionProperty, unsetValue],
];
}
}
function parseBorderColor(value: string): { top: Color; right: Color; bottom: Color; left: Color } {
const result: { top: Color; right: Color; bottom: Color; left: Color } = {
top: undefined,
right: undefined,
bottom: undefined,
left: undefined,
};
if (value.indexOf('rgb') === 0 || value.indexOf('hsl') === 0) {
result.top = result.right = result.bottom = result.left = new Color(value);
return result;
}
const arr = value.split(/[ ,]+/);
if (arr.length === 1) {
const arr0 = new Color(arr[0]);
result.top = arr0;
result.right = arr0;
result.bottom = arr0;
result.left = arr0;
} else if (arr.length === 2) {
const arr0 = new Color(arr[0]);
const arr1 = new Color(arr[1]);
result.top = arr0;
result.right = arr1;
result.bottom = arr0;
result.left = arr1;
} else if (arr.length === 3) {
const arr0 = new Color(arr[0]);
const arr1 = new Color(arr[1]);
const arr2 = new Color(arr[2]);
result.top = arr0;
result.right = arr1;
result.bottom = arr2;
result.left = arr1;
} else if (arr.length === 4) {
const arr0 = new Color(arr[0]);
const arr1 = new Color(arr[1]);
const arr2 = new Color(arr[2]);
const 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',
@ -932,13 +798,13 @@ const borderColorProperty = new ShorthandProperty<Style, string | Color>({
},
converter: function (value) {
if (typeof value === 'string') {
const fourColors = parseBorderColor(value);
const colors = parseBorderColorPositioning(value);
return [
[borderTopColorProperty, fourColors.top],
[borderRightColorProperty, fourColors.right],
[borderBottomColorProperty, fourColors.bottom],
[borderLeftColorProperty, fourColors.left],
[borderTopColorProperty, new Color(colors.top)],
[borderRightColorProperty, new Color(colors.right)],
[borderBottomColorProperty, new Color(colors.bottom)],
[borderLeftColorProperty, new Color(colors.left)],
];
} else {
return [
@ -1009,13 +875,13 @@ const borderWidthProperty = new ShorthandProperty<Style, string | CoreTypes.Leng
},
converter: function (value) {
if (typeof value === 'string' && value !== 'auto') {
const borderWidths = parseThickness(value);
const borderWidths = parseShorthandPositioning(value);
return [
[borderTopWidthProperty, borderWidths.top],
[borderRightWidthProperty, borderWidths.right],
[borderBottomWidthProperty, borderWidths.bottom],
[borderLeftWidthProperty, borderWidths.left],
[borderTopWidthProperty, Length.parse(borderWidths.top)],
[borderRightWidthProperty, Length.parse(borderWidths.right)],
[borderBottomWidthProperty, Length.parse(borderWidths.bottom)],
[borderLeftWidthProperty, Length.parse(borderWidths.left)],
];
} else {
return [
@ -1138,13 +1004,13 @@ const borderRadiusProperty = new ShorthandProperty<Style, string | CoreTypes.Len
},
converter: function (value) {
if (typeof value === 'string') {
const borderRadius = parseThickness(value);
const borderRadius = parseShorthandPositioning(value);
return [
[borderTopLeftRadiusProperty, borderRadius.top],
[borderTopRightRadiusProperty, borderRadius.right],
[borderBottomRightRadiusProperty, borderRadius.bottom],
[borderBottomLeftRadiusProperty, borderRadius.left],
[borderTopLeftRadiusProperty, Length.parse(borderRadius.top)],
[borderTopRightRadiusProperty, Length.parse(borderRadius.right)],
[borderBottomRightRadiusProperty, Length.parse(borderRadius.bottom)],
[borderBottomLeftRadiusProperty, Length.parse(borderRadius.left)],
];
} else {
return [
@ -1249,63 +1115,54 @@ const boxShadowProperty = new CssProperty<Style, ShadowCSSValues>({
});
boxShadowProperty.register(Style);
function isNonNegativeFiniteNumber(value: number): boolean {
return isFinite(value) && !isNaN(value) && value >= 0;
}
const supportedPaths = ['rect', 'circle', 'ellipse', 'polygon', 'inset'];
function isClipPathValid(value: string): boolean {
if (!value) {
return true;
}
const functionName = value.substring(0, value.indexOf('(')).trim();
return supportedPaths.indexOf(functionName) !== -1;
}
export const clipPathProperty = new CssProperty<Style, string>({
export const clipPathProperty = new CssProperty<Style, string | ClipPathFunction>({
name: 'clipPath',
cssName: 'clip-path',
valueChanged: (target, oldValue, newValue) => {
if (!isClipPathValid(newValue)) {
throw new Error('clip-path is not valid.');
target.backgroundInternal = target.backgroundInternal.withClipPath(newValue);
},
equalityComparer: (value1, value2) => {
if (value1 instanceof ClipPathFunction && value2 instanceof ClipPathFunction) {
return ClipPathFunction.equals(value1, value2);
}
return value1 === value2;
},
valueConverter(value: string | ClipPathFunction) {
if (typeof value === 'string') {
return parseClipPath(value);
}
target.backgroundInternal = target.backgroundInternal.withClipPath(newValue);
return value;
},
});
clipPathProperty.register(Style);
function isFloatValueConverter(value: string): number {
const 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,
valueConverter: (value: string): number => {
const newValue = parseFloat(value);
if (isNaN(newValue)) {
throw new Error(`Invalid value: ${newValue}`);
}
return newValue;
},
});
zIndexProperty.register(Style);
function opacityConverter(value: any): number {
const 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,
valueConverter: (value: string): number => {
const newValue = parseFloat(value);
if (!isNonNegativeFiniteNumber(newValue) || newValue > 1) {
throw new Error(`Opacity should be between [0, 1]. Value: ${newValue}`);
}
return newValue;
},
});
opacityProperty.register(Style);

View File

@ -12,6 +12,7 @@ import { CoreTypes } from '../../../core-types';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types';
import { ShadowCSSValues } from '../css-shadow';
import { StrokeCSSValues } from '../css-stroke';
import { ClipPathFunction } from '../clip-path-function';
export interface CommonLayoutParams {
width: number;
@ -122,7 +123,7 @@ export class Style extends Observable implements StyleDefinition {
public translateX: CoreTypes.dip;
public translateY: CoreTypes.dip;
public clipPath: string;
public clipPath: string | ClipPathFunction;
public color: Color;
public tintColor: Color;
public placeholderColor: Color;