From f23bdb9f62005ae8e194d23c29e4dc6155842d72 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Thu, 30 Jan 2025 18:20:22 +0200 Subject: [PATCH 1/3] feat(core): TextBase Span interaction and styling improvements --- .../ui/editable-text-base/index.android.ts | 10 ++-- packages/core/ui/html-view/index.ios.ts | 2 +- packages/core/ui/label/index.ios.ts | 14 ++++- packages/core/ui/scroll-view/index.android.ts | 1 - packages/core/ui/search-bar/index.ios.ts | 35 ++++++------ packages/core/ui/text-base/index.android.ts | 34 ++++++++--- packages/core/ui/text-base/index.ios.ts | 56 +++++++++++++------ .../core/ui/text-base/text-base-common.ts | 4 +- packages/core/ui/text-field/index.ios.ts | 15 ++--- packages/core/ui/text-view/index.ios.ts | 3 +- 10 files changed, 113 insertions(+), 61 deletions(-) diff --git a/packages/core/ui/editable-text-base/index.android.ts b/packages/core/ui/editable-text-base/index.android.ts index cab7db0e16..6f5a1e9726 100644 --- a/packages/core/ui/editable-text-base/index.android.ts +++ b/packages/core/ui/editable-text-base/index.android.ts @@ -84,12 +84,10 @@ function initializeEditTextListeners(): void { } export abstract class EditableTextBase extends EditableTextBaseCommon { - /* tslint:disable */ - _dirtyTextAccumulator: string; - /* tslint:enable */ - nativeViewProtected: android.widget.EditText; nativeTextViewProtected: android.widget.EditText; + + private _dirtyTextAccumulator: string; private _keyListenerCache: android.text.method.KeyListener; private _inputType: number; @@ -120,12 +118,16 @@ export abstract class EditableTextBase extends EditableTextBaseCommon { public disposeNativeView(): void { const editText = this.nativeTextViewProtected; + editText.removeTextChangedListener((editText).listener); editText.setOnFocusChangeListener(null); editText.setOnEditorActionListener(null); (editText).listener.owner = null; (editText).listener = null; this._keyListenerCache = null; + this._dirtyTextAccumulator = undefined; + this._inputType = 0; + super.disposeNativeView(); } diff --git a/packages/core/ui/html-view/index.ios.ts b/packages/core/ui/html-view/index.ios.ts index 177b0f3b02..2c135020ad 100644 --- a/packages/core/ui/html-view/index.ios.ts +++ b/packages/core/ui/html-view/index.ios.ts @@ -29,7 +29,7 @@ export class HtmlView extends HtmlViewBase { // Remove extra padding this.nativeViewProtected.textContainer.lineFragmentPadding = 0; - this.nativeViewProtected.textContainerInset = (UIEdgeInsets as any).zero; + this.nativeViewProtected.textContainerInset = UIEdgeInsetsZero; } // @ts-ignore diff --git a/packages/core/ui/label/index.ios.ts b/packages/core/ui/label/index.ios.ts index bf35b32697..e3cf12f543 100644 --- a/packages/core/ui/label/index.ios.ts +++ b/packages/core/ui/label/index.ios.ts @@ -19,6 +19,8 @@ enum FixedSize { @CSSType('Label') export class Label extends TextBase implements LabelDefinition { nativeViewProtected: TNSLabel; + nativeTextViewProtected: TNSLabel; + private _fixedSize: FixedSize; public createNativeView() { @@ -28,6 +30,11 @@ export class Label extends TextBase implements LabelDefinition { return view; } + public disposeNativeView(): void { + super.disposeNativeView(); + this._fixedSize = null; + } + // @ts-ignore get ios(): TNSLabel { return this.nativeTextViewProtected; @@ -102,7 +109,7 @@ export class Label extends TextBase implements LabelDefinition { } private _measureNativeView(width: number, widthMode: number, height: number, heightMode: number): { width: number; height: number } { - const view = this.nativeTextViewProtected; + const view = this.nativeTextViewProtected; const nativeSize = view.textRectForBoundsLimitedToNumberOfLines(CGRectMake(0, 0, widthMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : layout.toDeviceIndependentPixels(width), heightMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : layout.toDeviceIndependentPixels(height)), view.numberOfLines).size; @@ -123,7 +130,8 @@ export class Label extends TextBase implements LabelDefinition { private adjustLineBreak() { const whiteSpace = this.whiteSpace; const textOverflow = this.textOverflow; - const nativeView = this.nativeViewProtected; + const nativeView = this.nativeTextViewProtected; + switch (whiteSpace) { case 'normal': nativeView.lineBreakMode = NSLineBreakMode.ByWordWrapping; @@ -158,7 +166,7 @@ export class Label extends TextBase implements LabelDefinition { const cgColor = color ? color.CGColor : null; nativeView.layer.backgroundColor = cgColor; }, - true + true, ); } } diff --git a/packages/core/ui/scroll-view/index.android.ts b/packages/core/ui/scroll-view/index.android.ts index 5b0b2b1e67..a6cd9f82a9 100644 --- a/packages/core/ui/scroll-view/index.android.ts +++ b/packages/core/ui/scroll-view/index.android.ts @@ -9,7 +9,6 @@ export class ScrollView extends ScrollViewBase { nativeViewProtected: org.nativescript.widgets.VerticalScrollView | org.nativescript.widgets.HorizontalScrollView; private _androidViewId = -1; private handler: android.view.ViewTreeObserver.OnScrollChangedListener; - private scrollChangeHandler: androidx.core.widget.NestedScrollView.OnScrollChangeListener; get horizontalOffset(): number { const nativeView = this.nativeViewProtected; diff --git a/packages/core/ui/search-bar/index.ios.ts b/packages/core/ui/search-bar/index.ios.ts index 051d4d8b7d..4441315917 100644 --- a/packages/core/ui/search-bar/index.ios.ts +++ b/packages/core/ui/search-bar/index.ios.ts @@ -73,7 +73,7 @@ class UISearchBarImpl extends UISearchBar { export class SearchBar extends SearchBarBase { nativeViewProtected: UISearchBar; private _delegate; - private __textField: UITextField; + private _textField: UITextField; createNativeView() { return UISearchBarImpl.new(); @@ -87,6 +87,7 @@ export class SearchBar extends SearchBarBase { disposeNativeView() { this._delegate = null; + this._textField = null; super.disposeNativeView(); } @@ -94,26 +95,26 @@ export class SearchBar extends SearchBarBase { (this.ios).resignFirstResponder(); } + private _getTextField(): UITextField { + if (!this._textField) { + this._textField = this.ios.valueForKey('searchField'); + } + + return this._textField; + } + // @ts-ignore get ios(): UISearchBar { return this.nativeViewProtected; } - get _textField(): UITextField { - if (!this.__textField) { - this.__textField = this.ios.valueForKey('searchField'); - } - - return this.__textField; - } - [isEnabledProperty.setNative](value: boolean) { const nativeView = this.nativeViewProtected; if (nativeView instanceof UIControl) { nativeView.enabled = value; } - const textField = this._textField; + const textField = this._getTextField(); if (textField) { textField.enabled = value; } @@ -128,7 +129,7 @@ export class SearchBar extends SearchBarBase { } [colorProperty.getDefault](): UIColor { - const sf = this._textField; + const sf = this._getTextField(); if (sf) { return sf.textColor; } @@ -136,7 +137,7 @@ export class SearchBar extends SearchBarBase { return null; } [colorProperty.setNative](value: UIColor | Color) { - const sf = this._textField; + const sf = this._getTextField(); const color = value instanceof Color ? value.ios : value; if (sf) { sf.textColor = color; @@ -145,12 +146,12 @@ export class SearchBar extends SearchBarBase { } [fontInternalProperty.getDefault](): UIFont { - const sf = this._textField; + const sf = this._getTextField(); return sf ? sf.font : null; } [fontInternalProperty.setNative](value: UIFont | Font) { - const sf = this._textField; + const sf = this._getTextField(); if (sf) { sf.font = value instanceof Font ? value.getUIFont(sf.font) : value; } @@ -179,7 +180,7 @@ export class SearchBar extends SearchBarBase { } [textFieldBackgroundColorProperty.getDefault](): UIColor { - const textField = this._textField; + const textField = this._getTextField(); if (textField) { return textField.backgroundColor; } @@ -188,7 +189,7 @@ export class SearchBar extends SearchBarBase { } [textFieldBackgroundColorProperty.setNative](value: Color | UIColor) { const color = value instanceof Color ? value.ios : value; - const textField = this._textField; + const textField = this._getTextField(); if (textField) { textField.backgroundColor = color; } @@ -219,6 +220,6 @@ export class SearchBar extends SearchBarBase { attributes[NSForegroundColorAttributeName] = this.textFieldHintColor.ios; } const attributedPlaceholder = NSAttributedString.alloc().initWithStringAttributes(stringValue, attributes); - this._textField.attributedPlaceholder = attributedPlaceholder; + this._getTextField().attributedPlaceholder = attributedPlaceholder; } } diff --git a/packages/core/ui/text-base/index.android.ts b/packages/core/ui/text-base/index.android.ts index 9136abfe32..59aa76add6 100644 --- a/packages/core/ui/text-base/index.android.ts +++ b/packages/core/ui/text-base/index.android.ts @@ -23,7 +23,7 @@ export * from './text-base-common'; let TextTransformation: TextTransformation; export interface TextTransformation { - new (owner: TextBase): any /* android.text.method.TransformationMethod */; + new (owner: TextBase): android.text.method.TransformationMethod; } function initializeTextTransformation(): void { @@ -169,9 +169,8 @@ function initializeBaselineAdjustedSpan(): void { } export class TextBase extends TextBaseCommon { - nativeViewProtected: org.nativescript.widgets.StyleableTextView; - // @ts-ignore - nativeTextViewProtected: org.nativescript.widgets.StyleableTextView; + public nativeViewProtected: org.nativescript.widgets.StyleableTextView; + private _defaultTransformationMethod: android.text.method.TransformationMethod; private _paintFlags: number; private _minHeight: number; @@ -181,6 +180,10 @@ export class TextBase extends TextBaseCommon { private _tappable = false; private _defaultMovementMethod: android.text.method.MovementMethod; + get nativeTextViewProtected(): org.nativescript.widgets.StyleableTextView { + return super.nativeTextViewProtected; + } + public initNativeView(): void { super.initNativeView(); initializeTextTransformation(); @@ -193,6 +196,19 @@ export class TextBase extends TextBaseCommon { this._maxLines = nativeView.getMaxLines(); } + public disposeNativeView(): void { + super.disposeNativeView(); + + this._tappable = false; + this._defaultTransformationMethod = null; + this._defaultMovementMethod = null; + this._paintFlags = 0; + this._minHeight = 0; + this._maxHeight = 0; + this._minLines = 0; + this._maxLines = 0; + } + public resetNativeView(): void { super.resetNativeView(); const nativeView = this.nativeTextViewProtected; @@ -502,13 +518,13 @@ export class TextBase extends TextBaseCommon { } if (this.style?.textStroke) { - this.nativeViewProtected.setTextStroke(Length.toDevicePixels(this.style.textStroke.width), this.style.textStroke.color.android, this.style.color.android); - } else if (this.nativeViewProtected.setTextStroke) { + this.nativeTextViewProtected.setTextStroke(Length.toDevicePixels(this.style.textStroke.width), this.style.textStroke.color.android, this.style.color.android); + } else if (this.nativeTextViewProtected.setTextStroke) { // reset - this.nativeViewProtected.setTextStroke(0, 0, 0); + this.nativeTextViewProtected.setTextStroke(0, 0, 0); } - this.nativeTextViewProtected.setText(transformedText); + this.nativeTextViewProtected.setText(transformedText); } _setTappableState(tappable: boolean) { @@ -608,7 +624,7 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span, ssb.setSpan(new android.text.style.ForegroundColorSpan(color.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - const backgroundColor: Color = getClosestPropertyValue(backgroundColorProperty, span); + const backgroundColor: Color = getClosestPropertyValue(backgroundColorProperty, span); if (backgroundColor) { ssb.setSpan(new android.text.style.BackgroundColorSpan(backgroundColor.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/packages/core/ui/text-base/index.ios.ts b/packages/core/ui/text-base/index.ios.ts index fd0d42dd74..1b3aecab93 100644 --- a/packages/core/ui/text-base/index.ios.ts +++ b/packages/core/ui/text-base/index.ios.ts @@ -34,7 +34,7 @@ class UILabelClickHandlerImpl extends NSObject { public linkTap(tapGesture: UITapGestureRecognizer) { const owner = this._owner?.deref(); if (owner) { - const label = owner.nativeTextViewProtected; + const nativeView = owner.nativeTextViewProtected instanceof UIButton ? owner.nativeTextViewProtected.titleLabel : owner.nativeTextViewProtected; // This offset along with setting paragraph style alignment will achieve perfect horizontal alignment for NSTextContainer let offsetXMultiplier: number; @@ -53,18 +53,27 @@ class UILabelClickHandlerImpl extends NSObject { const layoutManager = NSLayoutManager.alloc().init(); const textContainer = NSTextContainer.alloc().initWithSize(CGSizeZero); - const textStorage = NSTextStorage.alloc().initWithAttributedString(owner.nativeTextViewProtected['attributedText']); + const textStorage = NSTextStorage.alloc().initWithAttributedString(nativeView.attributedText); layoutManager.addTextContainer(textContainer); textStorage.addLayoutManager(layoutManager); textContainer.lineFragmentPadding = 0; - textContainer.lineBreakMode = label.lineBreakMode; - textContainer.maximumNumberOfLines = label.numberOfLines; - const labelSize = label.bounds.size; + + if (nativeView instanceof UITextView) { + textContainer.lineBreakMode = nativeView.textContainer.lineBreakMode; + textContainer.maximumNumberOfLines = nativeView.textContainer.maximumNumberOfLines; + } else { + if (!(nativeView instanceof UITextField)) { + textContainer.lineBreakMode = nativeView.lineBreakMode; + textContainer.maximumNumberOfLines = nativeView.numberOfLines; + } + } + + const labelSize = nativeView.bounds.size; textContainer.size = labelSize; - const locationOfTouchInLabel = tapGesture.locationInView(label); + const locationOfTouchInLabel = tapGesture.locationInView(nativeView); const textBoundingBox = layoutManager.usedRectForTextContainer(textContainer); const textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * offsetXMultiplier - textBoundingBox.origin.x, (labelSize.height - textBoundingBox.size.height) * offsetYMultiplier - textBoundingBox.origin.y); @@ -117,31 +126,46 @@ class UILabelClickHandlerImpl extends NSObject { export class TextBase extends TextBaseCommon { public nativeViewProtected: UITextField | UITextView | UILabel | UIButton; - // @ts-ignore - public nativeTextViewProtected: UITextField | UITextView | UILabel | UIButton; + public _spanRanges: NSRange[]; + private _tappable = false; + private _linkTapHandler: UILabelClickHandlerImpl; private _tapGestureRecognizer: UITapGestureRecognizer; - public _spanRanges: NSRange[]; + + get nativeTextViewProtected(): UITextField | UITextView | UILabel | UIButton { + return super.nativeTextViewProtected; + } public initNativeView(): void { super.initNativeView(); this._setTappableState(false); } + public disposeNativeView(): void { + super.disposeNativeView(); + + this._tappable = false; + this._linkTapHandler = null; + this._tapGestureRecognizer = null; + } + _setTappableState(tappable: boolean) { if (this._tappable !== tappable) { + const nativeTextView = this.nativeTextViewProtected; + this._tappable = tappable; if (this._tappable) { const tapHandler = UILabelClickHandlerImpl.initWithOwner(new WeakRef(this)); - // associate handler with menuItem or it will get collected by JSC. - (this).handler = tapHandler; + // Associate handler with menuItem or it will get collected by JSC + this._linkTapHandler = tapHandler; - this._tapGestureRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(tapHandler, 'linkTap'); - this.nativeViewProtected.userInteractionEnabled = true; - this.nativeViewProtected.addGestureRecognizer(this._tapGestureRecognizer); + this._tapGestureRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(this._linkTapHandler, 'linkTap'); + nativeTextView.addGestureRecognizer(this._tapGestureRecognizer); } else { - this.nativeViewProtected.userInteractionEnabled = false; - this.nativeViewProtected.removeGestureRecognizer(this._tapGestureRecognizer); + nativeTextView.removeGestureRecognizer(this._tapGestureRecognizer); + + this._linkTapHandler = null; + this._tapGestureRecognizer = null; } } } diff --git a/packages/core/ui/text-base/text-base-common.ts b/packages/core/ui/text-base/text-base-common.ts index d26b66e347..7ce26a264c 100644 --- a/packages/core/ui/text-base/text-base-common.ts +++ b/packages/core/ui/text-base/text-base-common.ts @@ -7,7 +7,7 @@ import { FontStyleType, FontWeightType } from '../styling/font-interfaces'; import { FormattedString } from './formatted-string'; import { Span } from './span'; import { View } from '../core/view'; -import { Property, CssProperty, InheritedCssProperty, makeValidator, makeParser } from '../core/properties'; +import { Property, CssAnimationProperty, CssProperty, InheritedCssProperty, makeValidator, makeParser } from '../core/properties'; import { Style } from '../styling/style'; import { Observable } from '../../data/observable'; import { CoreTypes } from '../../core-types'; @@ -269,7 +269,7 @@ function onFormattedTextPropertyChanged(textBase: TextBaseCommon, oldValue: Form } } -export function getClosestPropertyValue(property: CssProperty, span: Span): T { +export function getClosestPropertyValue(property: CssProperty | CssAnimationProperty, span: Span): T { if (property.isSet(span.style)) { return span.style[property.name]; } else if (property.isSet(span.parent.style)) { diff --git a/packages/core/ui/text-field/index.ios.ts b/packages/core/ui/text-field/index.ios.ts index a6866dbaf8..4239f56379 100644 --- a/packages/core/ui/text-field/index.ios.ts +++ b/packages/core/ui/text-field/index.ios.ts @@ -114,7 +114,9 @@ class UITextFieldImpl extends UITextField { export class TextField extends TextFieldBase { nativeViewProtected: UITextField; + private _delegate: UITextFieldDelegateImpl; + private _firstEdit: boolean; createNativeView() { return UITextFieldImpl.initWithOwner(new WeakRef(this)); @@ -128,6 +130,7 @@ export class TextField extends TextFieldBase { disposeNativeView() { this._delegate = null; + this._firstEdit = false; super.disposeNativeView(); } @@ -136,10 +139,8 @@ export class TextField extends TextFieldBase { return this.nativeViewProtected; } - private firstEdit: boolean; - public textFieldShouldBeginEditing(textField: UITextField): boolean { - this.firstEdit = true; + this._firstEdit = true; return this.editable; } @@ -157,7 +158,7 @@ export class TextField extends TextFieldBase { } public textFieldShouldClear(textField: UITextField) { - this.firstEdit = false; + this._firstEdit = false; textProperty.nativeValueChange(this, ''); return true; @@ -192,7 +193,7 @@ export class TextField extends TextFieldBase { if (this.updateTextTrigger === 'textChanged') { if (this.valueFormatter) { // format/replace - let currentValue = textField.text; + const currentValue = textField.text; let nativeValueChange = `${textField.text}${replacementString}`; if (replacementString === '') { // clearing when empty @@ -207,7 +208,7 @@ export class TextField extends TextFieldBase { // 1. secureTextEntry with firstEdit should not replace // 2. emoji's should not replace value // 3. convenient keyboard shortcuts should not replace value (eg, '.com') - const shouldReplaceString = (textField.secureTextEntry && this.firstEdit) || (delta > 1 && !isEmoji(replacementString) && delta !== replacementString.length); + const shouldReplaceString = (textField.secureTextEntry && this._firstEdit) || (delta > 1 && !isEmoji(replacementString) && delta !== replacementString.length); if (shouldReplaceString) { textProperty.nativeValueChange(this, replacementString); } else { @@ -226,7 +227,7 @@ export class TextField extends TextFieldBase { // if the textfield is in auto size we need to request a layout to take the new text width into account this.requestLayout(); } - this.firstEdit = false; + this._firstEdit = false; return true; } diff --git a/packages/core/ui/text-view/index.ios.ts b/packages/core/ui/text-view/index.ios.ts index 7b44166d6d..f26280a66d 100644 --- a/packages/core/ui/text-view/index.ios.ts +++ b/packages/core/ui/text-view/index.ios.ts @@ -88,7 +88,8 @@ export class TextView extends TextViewBaseCommon { nativeViewProtected: UITextView; nativeTextViewProtected: UITextView; private _delegate: UITextViewDelegateImpl; - _isShowingHint: boolean; + private _isShowingHint: boolean; + public _isEditing: boolean; private _hintColor = majorVersion <= 12 || !UIColor.placeholderTextColor ? UIColor.blackColor.colorWithAlphaComponent(0.22) : UIColor.placeholderTextColor; From 8aa6ee320de76daa130b823524c880cad6c1c736 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Fri, 31 Jan 2025 01:41:20 +0200 Subject: [PATCH 2/3] fix: Android span having different background color than view --- packages/core/ui/text-base/index.android.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/ui/text-base/index.android.ts b/packages/core/ui/text-base/index.android.ts index 59aa76add6..8c3c9e82b4 100644 --- a/packages/core/ui/text-base/index.android.ts +++ b/packages/core/ui/text-base/index.android.ts @@ -624,10 +624,8 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span, ssb.setSpan(new android.text.style.ForegroundColorSpan(color.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - const backgroundColor: Color = getClosestPropertyValue(backgroundColorProperty, span); - - if (backgroundColor) { - ssb.setSpan(new android.text.style.BackgroundColorSpan(backgroundColor.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if (spanStyle.backgroundColor) { + ssb.setSpan(new android.text.style.BackgroundColorSpan(spanStyle.backgroundColor.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } const textDecoration: CoreTypes.TextDecorationType = getClosestPropertyValue(textDecorationProperty, span); From 5f389053f1cd2f45e2fbcfa6aa1359b16ed99910 Mon Sep 17 00:00:00 2001 From: Dimitris - Rafail Katsampas Date: Fri, 31 Jan 2025 01:56:09 +0200 Subject: [PATCH 3/3] chore: Removed unneeded type --- packages/core/ui/text-base/text-base-common.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/ui/text-base/text-base-common.ts b/packages/core/ui/text-base/text-base-common.ts index 7ce26a264c..a0ec13ca2e 100644 --- a/packages/core/ui/text-base/text-base-common.ts +++ b/packages/core/ui/text-base/text-base-common.ts @@ -7,12 +7,11 @@ import { FontStyleType, FontWeightType } from '../styling/font-interfaces'; import { FormattedString } from './formatted-string'; import { Span } from './span'; import { View } from '../core/view'; -import { Property, CssAnimationProperty, CssProperty, InheritedCssProperty, makeValidator, makeParser } from '../core/properties'; +import { Property, CssProperty, InheritedCssProperty, makeValidator, makeParser } from '../core/properties'; import { Style } from '../styling/style'; import { Observable } from '../../data/observable'; import { CoreTypes } from '../../core-types'; import { TextBase as TextBaseDefinition } from '.'; -import { Color } from '../../color'; import { ShadowCSSValues, parseCSSShadow } from '../styling/css-shadow'; import { StrokeCSSValues, parseCSSStroke } from '../styling/css-stroke'; @@ -269,7 +268,7 @@ function onFormattedTextPropertyChanged(textBase: TextBaseCommon, oldValue: Form } } -export function getClosestPropertyValue(property: CssProperty | CssAnimationProperty, span: Span): T { +export function getClosestPropertyValue(property: CssProperty, span: Span): T { if (property.isSet(span.style)) { return span.style[property.name]; } else if (property.isSet(span.parent.style)) {