Skip to content

feat(core): TextBase Span interaction and styling improvements #10682

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/core/ui/editable-text-base/index.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -120,12 +118,16 @@ export abstract class EditableTextBase extends EditableTextBaseCommon {

public disposeNativeView(): void {
const editText = this.nativeTextViewProtected;

editText.removeTextChangedListener((<any>editText).listener);
editText.setOnFocusChangeListener(null);
editText.setOnEditorActionListener(null);
(<any>editText).listener.owner = null;
(<any>editText).listener = null;
this._keyListenerCache = null;
this._dirtyTextAccumulator = undefined;
this._inputType = 0;

super.disposeNativeView();
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/ui/html-view/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions packages/core/ui/label/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ enum FixedSize {
@CSSType('Label')
export class Label extends TextBase implements LabelDefinition {
nativeViewProtected: TNSLabel;
nativeTextViewProtected: TNSLabel;

private _fixedSize: FixedSize;

public createNativeView() {
Expand All @@ -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;
Expand Down Expand Up @@ -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 = <UILabel>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;

Expand All @@ -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;
Expand Down Expand Up @@ -158,7 +166,7 @@ export class Label extends TextBase implements LabelDefinition {
const cgColor = color ? color.CGColor : null;
nativeView.layer.backgroundColor = cgColor;
},
true
true,
);
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/core/ui/scroll-view/index.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
35 changes: 18 additions & 17 deletions packages/core/ui/search-bar/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -87,33 +87,34 @@ export class SearchBar extends SearchBarBase {

disposeNativeView() {
this._delegate = null;
this._textField = null;
super.disposeNativeView();
}

public dismissSoftInput() {
(<UIResponder>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;
}
Expand All @@ -128,15 +129,15 @@ export class SearchBar extends SearchBarBase {
}

[colorProperty.getDefault](): UIColor {
const sf = this._textField;
const sf = this._getTextField();
if (sf) {
return sf.textColor;
}

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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
}
38 changes: 26 additions & 12 deletions packages/core/ui/text-base/index.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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;
Expand Down Expand Up @@ -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(<any>transformedText);
this.nativeTextViewProtected.setText(transformedText);
}

_setTappableState(tappable: boolean) {
Expand Down Expand Up @@ -608,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(<any>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);
Expand Down
56 changes: 40 additions & 16 deletions packages/core/ui/text-base/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class UILabelClickHandlerImpl extends NSObject {
public linkTap(tapGesture: UITapGestureRecognizer) {
const owner = this._owner?.deref();
if (owner) {
const label = <UILabel>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;
Expand All @@ -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);
Expand Down Expand Up @@ -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.
(<any>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;
}
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/core/ui/text-base/text-base-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ 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';

Expand Down
Loading
Loading