feat: improved css-shadow parser

This commit is contained in:
Igor Randjelovic
2021-02-20 00:20:35 +01:00
committed by Nathan Walker
parent 673387cf99
commit d2f50e50bb
7 changed files with 94 additions and 77 deletions

View File

@ -1,6 +1,15 @@
import { EventData, TextBase } from '@nativescript/core'; import { EventData, TextBase } from '@nativescript/core';
const possibleValues = ['2 10 4 rgb(255, 100, 100)', '2 10 2 rgba(10, 10, 10, 0.5)', '1 1 1 #55a', '2 2 2 #aaa', '0 0 1 yellow', '-1 -1 1 #aaa', '']; // prettier-ignore
const possibleValues = [
'2 10 4 rgb(255, 100, 100)',
'2 10 2 rgba(10, 10, 10, 0.5)',
'1 1 1 #55a',
'2 2 2 #333',
'0 0 1 yellow',
'-1 -1 1 #333',
''
];
let currentIndex = 0; let currentIndex = 0;
export function butonTap(args: EventData) { export function butonTap(args: EventData) {

View File

@ -1,9 +1,68 @@
import { Color } from '../../color'; import { Color } from '../../color';
import { Length, zeroLength } from './style-properties';
export class CSSShadow { export interface CSSShadow {
public offsetX: number; inset: boolean;
public offsetY: number; offsetX: Length;
public blurRadius: number; offsetY: Length;
public spreadRadius: number; blurRadius?: Length;
public color: Color; spreadRadius?: Length;
color: Color;
}
/**
* Matches whitespace except if the whitespace is contained in parenthesis - ex. rgb(a), hsl color.
*/
const PARTS_RE = /\s(?![^(]*\))/;
/**
* Matches a Length value with or without a unit
*/
const LENGTH_RE = /^-?[0-9]+[a-zA-Z%]*?$/;
/**
* Checks if the value is a Length or 0
*/
const isLength = (v) => v === '0' || LENGTH_RE.test(v);
/**
* Parse a string into a CSSShadow
* Supports any valid css box/text shadow combination.
*
* inspired by https://github.com/jxnblk/css-box-shadow/blob/master/index.js (MIT License)
*
* @param value
*/
export function parseCSSShadow(value: string): CSSShadow {
const parts = value.split(PARTS_RE);
const inset = parts.includes('inset');
const first = parts[0];
const last = parts[parts.length - 1];
let colorRaw = 'black';
if (!isLength(first) && first !== 'inset') {
colorRaw = first;
} else if (!isLength(last)) {
colorRaw = last;
}
const nums = parts
.filter((n) => n !== 'inset')
.filter((n) => n !== colorRaw)
.map((val) => {
try {
return Length.parse(val);
} catch (err) {
return zeroLength;
}
});
const [offsetX, offsetY, blurRadius, spreadRadius] = nums;
return {
inset,
offsetX: offsetX,
offsetY: offsetY,
blurRadius: blurRadius,
spreadRadius: spreadRadius,
color: new Color(colorRaw),
};
} }

View File

@ -17,7 +17,7 @@ import { Trace } from '../../trace';
import * as parser from '../../css/parser'; import * as parser from '../../css/parser';
import { LinearGradient } from './linear-gradient'; import { LinearGradient } from './linear-gradient';
import { CSSShadow } from './css-shadow'; import { CSSShadow, parseCSSShadow } from './css-shadow';
export type LengthDipUnit = { readonly unit: 'dip'; readonly value: dip }; export type LengthDipUnit = { readonly unit: 'dip'; readonly value: dip };
export type LengthPxUnit = { readonly unit: 'px'; readonly value: px }; export type LengthPxUnit = { readonly unit: 'px'; readonly value: px };
@ -452,53 +452,6 @@ export const verticalAlignmentProperty = new CssProperty<Style, VerticalAlignmen
}); });
verticalAlignmentProperty.register(Style); verticalAlignmentProperty.register(Style);
export function parseShadowProperites(value: string): CSSShadow {
if (typeof value === 'string') {
let arr;
let colorRaw = 'black';
if (value.indexOf('rgb') > -1) {
arr = value.split(' ');
colorRaw = arr.pop();
} else {
arr = value.split(/[ ,]+/);
colorRaw = arr.pop();
}
let offsetX = 0;
let offsetY = 0;
let blurRadius = 0; // not currently in use
let spreadRadius = 0; // maybe rename this to just radius
let color: Color = new Color(colorRaw);
if (arr.length === 1) {
Trace.write('Expected 3, 4 or 5 parameters. Actual: ' + value, Trace.categories.Error, Trace.messageType.error);
}
if (arr.length > 1) {
offsetX = parseFloat(arr[0]);
offsetY = parseFloat(arr[1]);
}
if (arr.length > 2) {
blurRadius = parseFloat(arr[2]);
}
if (arr.length > 3) {
spreadRadius = parseFloat(arr[3]);
}
return {
offsetX: offsetX,
offsetY: offsetY,
blurRadius: blurRadius,
spreadRadius: spreadRadius,
color: color,
};
} else {
return value;
}
}
interface Thickness { interface Thickness {
top: string; top: string;
right: string; right: string;
@ -1330,7 +1283,7 @@ const boxShadowProperty = new CssProperty<Style, CSSShadow>({
target.boxShadow = newValue; target.boxShadow = newValue;
}, },
valueConverter: (value) => { valueConverter: (value) => {
return parseShadowProperites(value); return parseCSSShadow(value);
}, },
}); });
boxShadowProperty.register(Style); boxShadowProperty.register(Style);

View File

@ -10,7 +10,7 @@ import { Observable } from '../../../data/observable';
import { FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent, Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from '../../layouts/flexbox-layout'; import { FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent, Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from '../../layouts/flexbox-layout';
import { Trace } from '../../../trace'; import { Trace } from '../../../trace';
import { TextAlignment, TextDecoration, TextTransform, WhiteSpace, TextShadow } from '../../text-base'; import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from '../../text-base';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types'; import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types';
import { CSSShadow } from '../css-shadow'; import { CSSShadow } from '../css-shadow';
@ -159,7 +159,7 @@ export class Style extends Observable implements StyleDefinition {
public textAlignment: TextAlignment; public textAlignment: TextAlignment;
public textDecoration: TextDecoration; public textDecoration: TextDecoration;
public textTransform: TextTransform; public textTransform: TextTransform;
public textShadow: TextShadow; public textShadow: CSSShadow;
public whiteSpace: WhiteSpace; public whiteSpace: WhiteSpace;
public minWidth: Length; public minWidth: Length;

View File

@ -3,6 +3,7 @@ import { FormattedString } from './formatted-string';
import { Style } from '../styling/style'; import { Style } from '../styling/style';
import { Length } from '../styling/style-properties'; import { Length } from '../styling/style-properties';
import { Property, CssProperty, InheritedCssProperty } from '../core/properties'; import { Property, CssProperty, InheritedCssProperty } from '../core/properties';
import { CSSShadow } from '../styling/css-shadow';
export class TextBase extends View implements AddChildFromBuilder { export class TextBase extends View implements AddChildFromBuilder {
/** /**
@ -54,7 +55,7 @@ export class TextBase extends View implements AddChildFromBuilder {
/** /**
* Gets or sets text shadow style property. * Gets or sets text shadow style property.
*/ */
textShadow: TextShadow; textShadow: CSSShadow;
/** /**
* Gets or sets white space style property. * Gets or sets white space style property.
@ -125,12 +126,6 @@ export type WhiteSpace = 'initial' | 'normal' | 'nowrap';
export type TextAlignment = 'initial' | 'left' | 'center' | 'right'; export type TextAlignment = 'initial' | 'left' | 'center' | 'right';
export type TextTransform = 'initial' | 'none' | 'capitalize' | 'uppercase' | 'lowercase'; export type TextTransform = 'initial' | 'none' | 'capitalize' | 'uppercase' | 'lowercase';
export type TextDecoration = 'none' | 'underline' | 'line-through' | 'underline line-through'; export type TextDecoration = 'none' | 'underline' | 'line-through' | 'underline line-through';
export type TextShadow = {
offsetX: Length;
offsetY: Length;
blurRadius: Length;
color: Color;
};
export const textAlignmentProperty: InheritedCssProperty<Style, TextAlignment>; export const textAlignmentProperty: InheritedCssProperty<Style, TextAlignment>;
export const textDecorationProperty: CssProperty<Style, TextDecoration>; export const textDecorationProperty: CssProperty<Style, TextDecoration>;

View File

@ -1,5 +1,6 @@
// Types // Types
import { TextDecoration, TextAlignment, TextTransform, TextShadow, getClosestPropertyValue } from './text-base-common'; import { TextDecoration, TextAlignment, TextTransform, getClosestPropertyValue } from './text-base-common';
import { CSSShadow } from '../styling/css-shadow';
// Requires // Requires
import { Font } from '../styling/font'; import { Font } from '../styling/font';
@ -8,7 +9,7 @@ import { Color } from '../../color';
import { FormattedString } from './formatted-string'; import { FormattedString } from './formatted-string';
import { Span } from './span'; import { Span } from './span';
import { colorProperty, fontInternalProperty, Length, VerticalAlignment } from '../styling/style-properties'; import { colorProperty, fontInternalProperty, Length, VerticalAlignment } from '../styling/style-properties';
import { isString, isDefined, isNullOrUndefined } from '../../utils/types'; import { isString, isNullOrUndefined } from '../../utils/types';
import { iOSNativeHelper } from '../../utils'; import { iOSNativeHelper } from '../../utils';
import { Trace } from '../../trace'; import { Trace } from '../../trace';
@ -152,8 +153,7 @@ export class TextBase extends TextBaseCommon {
if (!(value instanceof Font) || !this.formattedText) { if (!(value instanceof Font) || !this.formattedText) {
let nativeView = this.nativeTextViewProtected; let nativeView = this.nativeTextViewProtected;
nativeView = nativeView instanceof UIButton ? nativeView.titleLabel : nativeView; nativeView = nativeView instanceof UIButton ? nativeView.titleLabel : nativeView;
const font = value instanceof Font ? value.getUIFont(nativeView.font) : value; nativeView.font = value instanceof Font ? value.getUIFont(nativeView.font) : value;
nativeView.font = font;
} }
} }
@ -189,7 +189,7 @@ export class TextBase extends TextBaseCommon {
this._setNativeText(); this._setNativeText();
} }
[textShadowProperty.setNative](value: TextShadow) { [textShadowProperty.setNative](value: CSSShadow) {
this._setShadow(value); this._setShadow(value);
} }
@ -348,7 +348,7 @@ export class TextBase extends TextBaseCommon {
} }
} }
_setShadow(value: TextShadow): void { _setShadow(value: CSSShadow): void {
const layer = getShadowLayer(this); const layer = getShadowLayer(this);
if (!layer) { if (!layer) {
Trace.write('text-shadow not applied, no layer.', Trace.categories.Style, Trace.messageType.info); Trace.write('text-shadow not applied, no layer.', Trace.categories.Style, Trace.messageType.info);
@ -377,7 +377,7 @@ export class TextBase extends TextBaseCommon {
// layer.shadowOffset = CGSizeMake(Length.toDevicePixels(value.offsetX), Length.toDevicePixels(value.offsetY)); // layer.shadowOffset = CGSizeMake(Length.toDevicePixels(value.offsetX), Length.toDevicePixels(value.offsetY));
layer.masksToBounds = false; layer.masksToBounds = false;
// NOTE: generally should not need shouldRaterize // NOTE: generally should not need shouldRasterize
// however for various detailed animation work which involves text-shadow applicable layers, we may want to give users the control of enabling this with text-shadow // however for various detailed animation work which involves text-shadow applicable layers, we may want to give users the control of enabling this with text-shadow
// if (!(this.nativeTextViewProtected instanceof UITextView)) { // if (!(this.nativeTextViewProtected instanceof UITextView)) {
// layer.shouldRasterize = true; // layer.shouldRasterize = true;
@ -504,6 +504,7 @@ export function getTransformedText(text: string, textTransform: TextTransform):
} }
} }
// todo: clean up nesting & logs
export function getShadowLayer(view: TextBase): CALayer { export function getShadowLayer(view: TextBase): CALayer {
let layer: CALayer; let layer: CALayer;
const name = 'shadow-layer'; const name = 'shadow-layer';

View File

@ -9,13 +9,13 @@ import { Span } from './span';
import { View } from '../core/view'; import { View } from '../core/view';
import { Property, CssProperty, InheritedCssProperty, makeValidator, makeParser } from '../core/properties'; import { Property, CssProperty, InheritedCssProperty, makeValidator, makeParser } from '../core/properties';
import { Style } from '../styling/style'; import { Style } from '../styling/style';
import { Length, parseShadowProperites } from '../styling/style-properties'; import { Length } from '../styling/style-properties';
import { Observable } from '../../data/observable'; import { Observable } from '../../data/observable';
import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from './text-base-interfaces'; import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from './text-base-interfaces';
import { TextBase as TextBaseDefinition } from '.'; import { TextBase as TextBaseDefinition } from '.';
import { Color } from '../../color'; import { Color } from '../../color';
import { CSSShadow } from '../styling/css-shadow'; import { CSSShadow, parseCSSShadow } from '../styling/css-shadow';
export * from './text-base-interfaces'; export * from './text-base-interfaces';
@ -115,10 +115,10 @@ export abstract class TextBaseCommon extends View implements TextBaseDefinition
this.style.textTransform = value; this.style.textTransform = value;
} }
get textShadow(): TextShadow { get textShadow(): CSSShadow {
return this.style.textShadow; return this.style.textShadow;
} }
set textShadow(value: TextShadow) { set textShadow(value: CSSShadow) {
this.style.textShadow = value; this.style.textShadow = value;
} }
@ -273,7 +273,7 @@ export const textShadowProperty = new CssProperty<Style, string | CSSShadow>({
cssName: 'text-shadow', cssName: 'text-shadow',
affectsLayout: global.isIOS, affectsLayout: global.isIOS,
valueConverter: (value) => { valueConverter: (value) => {
return parseShadowProperites(value); return parseCSSShadow(value);
}, },
}); });
textShadowProperty.register(Style); textShadowProperty.register(Style);