FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat: TappableSpan support by vtrifonov · Pull Request #8256 · NativeScript/NativeScript · GitHub

Repository navigation

8 changes: 6 additions & 2 deletions api-reports/NativeScript.api.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -1936,14 +1936,18 @@ export class Span extends ViewBase {

public fontWeight: FontWeight;

public static linkTapEvent: string;

// (undocumented)
_setTextInternal(value: string): void;

public readonly tappable: boolean;

public text: string;

public textDecoration: TextDecoration;
//@endprivate
}
}

// @public
export class StackLayout extends LayoutBase {
Expand Down Expand Up @@ -2422,7 +2426,7 @@ export interface TapGestureEventData extends GestureEventData {
getPointerCount(): number;

getX(): number;

getY(): number;
}

Expand Down
1 change: 1 addition & 0 deletions e2e/ui-tests-app/app/button/main-page.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function loadExamples() {
examples.set("issue-4287", "button/issue-4287-page");
examples.set("issue-4385", "button/issue-4385-page");
examples.set("highlight-4740", "button/highlight-4740/highlight-4740-page");
examples.set("tappable-span", "button/tappable-span-page");

return examples;
}
11 changes: 10 additions & 1 deletion nativescript-core/ui/text-base/span.d.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,20 @@ export class Span extends ViewBase {
* Gets or sets the text for the span.
*/
public text: string;
/**
* String value used when hooking to linkTap event.
*/
public static linkTapEvent: string;

/**
* Gets if the span is tappable or not.
*/
public readonly tappable: boolean;

//@private
/**
* @private
*/
_setTextInternal(value: string): void;
//@endprivate
}
}
25 changes: 24 additions & 1 deletion nativescript-core/ui/text-base/span.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
import { Span as SpanDefinition } from "./span";
import { ViewBase } from "../core/view";
import { FontStyle, FontWeight, } from "../styling/font";
import { TextDecoration } from "../text-base";
import { TextDecoration, EventData } from "../text-base";

export class Span extends ViewBase implements SpanDefinition {
static linkTapEvent = "linkTap";
private _text: string;
private _tappable: boolean = false;

get fontFamily(): string {
return this.style.fontFamily;
Expand Down Expand Up @@ -68,7 +70,28 @@ export class Span extends ViewBase implements SpanDefinition {
}
}

get tappable(): boolean {
return this._tappable;
}

addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any) {
super.addEventListener(arg, callback, thisArg);
this._setTappable(this.hasListeners(Span.linkTapEvent));
}

removeEventListener(arg: string, callback?: any, thisArg?: any) {
super.removeEventListener(arg, callback, thisArg);
this._setTappable(this.hasListeners(Span.linkTapEvent));
}

_setTextInternal(value: string): void {
this._text = value;
}

private _setTappable(value: boolean): void {
if (this._tappable !== value) {
this._tappable = value;
this.notifyPropertyChange("tappable", value);
}
}
}
75 changes: 75 additions & 0 deletions nativescript-core/ui/text-base/text-base.android.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,42 @@ function initializeTextTransformation(): void {
TextTransformation = TextTransformationImpl;
}

interface ClickableSpan {
new (owner: Span): android.text.style.ClickableSpan;
}

let ClickableSpan: ClickableSpan;

function initializeClickableSpan(): void {
if (ClickableSpan) {
return;
}

class ClickableSpanImpl extends android.text.style.ClickableSpan {
owner: WeakRef<Span>;

constructor(owner: Span) {
super();
this.owner = new WeakRef(owner);

return global.__native(this);
}
onClick(view: android.view.View): void {
const owner = this.owner.get();
if (owner) {
owner._emit(Span.linkTapEvent);
}
view.clearFocus();
view.invalidate();
}
updateDrawState(tp: android.text.TextPaint): void {
// don't style as link
}
}

ClickableSpan = ClickableSpanImpl;
}

export class TextBase extends TextBaseCommon {
nativeViewProtected: android.widget.TextView;
nativeTextViewProtected: android.widget.TextView;
Expand All @@ -60,12 +96,15 @@ export class TextBase extends TextBaseCommon {
private _maxHeight: number;
private _minLines: number;
private _maxLines: number;
private _tappable: boolean = false;
private _defaultMovementMethod: android.text.method.MovementMethod;

public initNativeView(): void {
super.initNativeView();
initializeTextTransformation();
const nativeView = this.nativeTextViewProtected;
this._defaultTransformationMethod = nativeView.getTransformationMethod();
this._defaultMovementMethod = this.nativeView.getMovementMethod();
this._minHeight = nativeView.getMinHeight();
this._maxHeight = nativeView.getMaxHeight();
this._minLines = nativeView.getMinLines();
Expand Down Expand Up @@ -112,6 +151,8 @@ export class TextBase extends TextBaseCommon {
return;
}

this._setTappableState(false);

this._setNativeText(reset);
}

Expand All @@ -131,6 +172,7 @@ export class TextBase extends TextBaseCommon {

const spannableStringBuilder = createSpannableStringBuilder(value);
nativeView.setText(<any>spannableStringBuilder);
this._setTappableState(isStringTappable(value));

textProperty.nativeValueChange(this, (value === null || value === undefined) ? "" : value.toString());

Expand Down Expand Up @@ -315,6 +357,19 @@ export class TextBase extends TextBaseCommon {

this.nativeTextViewProtected.setText(<any>transformedText);
}

_setTappableState(tappable: boolean) {
if (this._tappable !== tappable) {
this._tappable = tappable;
if (this._tappable) {
this.nativeViewProtected.setMovementMethod(android.text.method.LinkMovementMethod.getInstance());
this.nativeViewProtected.setHighlightColor(null);
}
else {
this.nativeViewProtected.setMovementMethod(this._defaultMovementMethod);
}
}
}
}

function getCapitalizedString(str: string): string {
Expand Down Expand Up @@ -346,6 +401,20 @@ export function getTransformedText(text: string, textTransform: TextTransform):
}
}

function isStringTappable(formattedString: FormattedString) {
if (!formattedString) {
return false;
}
for (let i = 0, length = formattedString.spans.length; i < length; i++) {
const span = formattedString.spans.getItem(i);
if (span.tappable) {
return true;
}
}

return false;
}

function createSpannableStringBuilder(formattedString: FormattedString): android.text.SpannableStringBuilder {
if (!formattedString || !formattedString.parent) {
return null;
Expand Down Expand Up @@ -444,6 +513,12 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span,
}
}

const tappable = span.tappable;
if (tappable) {
initializeClickableSpan();
ssb.setSpan(new ClickableSpan(span), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}

// TODO: Implement letterSpacing for Span here.
// const letterSpacing = formattedString.parent.style.letterSpacing;
// if (letterSpacing > 0) {
Expand Down
108 changes: 108 additions & 0 deletions nativescript-core/ui/text-base/text-base.ios.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,101 @@ export * from "./text-base-common";

const majorVersion = ios.MajorVersion;

class UILabelClickHandlerImpl extends NSObject {
private _owner: WeakRef<TextBase>;

public static initWithOwner(owner: WeakRef<TextBase>): UILabelClickHandlerImpl {
let handler = <UILabelClickHandlerImpl>UILabelClickHandlerImpl.new();
handler._owner = owner;

return handler;
}

public linkTap(tapGesture: UITapGestureRecognizer) {
let owner = this._owner.get();
if (owner) {
// https://stackoverflow.com/a/35789589
let label = <UILabel>owner.nativeTextViewProtected;
let layoutManager = NSLayoutManager.alloc().init();
let textContainer = NSTextContainer.alloc().initWithSize(CGSizeZero);
let textStorage = NSTextStorage.alloc().initWithAttributedString(owner.nativeTextViewProtected["attributedText"]);

layoutManager.addTextContainer(textContainer);
textStorage.addLayoutManager(layoutManager);

textContainer.lineFragmentPadding = 0;
textContainer.lineBreakMode = label.lineBreakMode;
textContainer.maximumNumberOfLines = label.numberOfLines;
let labelSize = label.bounds.size;
textContainer.size = labelSize;

let locationOfTouchInLabel = tapGesture.locationInView(label);
let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer);

let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);

let locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
locationOfTouchInLabel.y - textContainerOffset.y);

let indexOfCharacter = layoutManager.characterIndexForPointInTextContainerFractionOfDistanceBetweenInsertionPoints(
locationOfTouchInTextContainer, textContainer, null);

let span: Span = null;
// try to find the corresponding span using the spanRanges
for (let i = 0; i < owner._spanRanges.length; i++) {
let range = owner._spanRanges[i];
if ((range.location <= indexOfCharacter) && (range.location + range.length) > indexOfCharacter) {
if (owner.formattedText && owner.formattedText.spans.length > i) {
span = owner.formattedText.spans.getItem(i);
}
break;
}
}

if (span && span.tappable) {
// if the span is found and tappable emit the linkTap event
span._emit(Span.linkTapEvent);
}
}
}

public static ObjCExposedMethods = {
"linkTap": { returns: interop.types.void, params: [interop.types.id] }
};
}

export class TextBase extends TextBaseCommon {

public nativeViewProtected: UITextField | UITextView | UILabel | UIButton;
public nativeTextViewProtected: UITextField | UITextView | UILabel | UIButton;
private _tappable: boolean = false;
private _tapGestureRecognizer: UITapGestureRecognizer;
public _spanRanges: NSRange[];

public initNativeView(): void {
super.initNativeView();
this._setTappableState(false);
}

_setTappableState(tappable: boolean) {
if (this._tappable !== tappable) {
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;

this._tapGestureRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(tapHandler, "linkTap");
this.nativeViewProtected.userInteractionEnabled = true;
this.nativeViewProtected.addGestureRecognizer(this._tapGestureRecognizer);
}
else {
this.nativeViewProtected.userInteractionEnabled = false;
this.nativeViewProtected.removeGestureRecognizer(this._tapGestureRecognizer);
}
}
}

[textProperty.getDefault](): number | symbol {
return resetSymbol;
Expand All @@ -35,6 +126,7 @@ export class TextBase extends TextBaseCommon {

[formattedTextProperty.setNative](value: FormattedString) {
this._setNativeText();
this._setTappableState(isStringTappable(value));
textProperty.nativeValueChange(this, !value ? "" : value.toString());
this._requestLayoutOnTextChanged();
}
Expand Down Expand Up @@ -253,6 +345,7 @@ export class TextBase extends TextBaseCommon {

createNSMutableAttributedString(formattedString: FormattedString): NSMutableAttributedString {
let mas = NSMutableAttributedString.alloc().init();
this._spanRanges = [];
if (formattedString && formattedString.parent) {
for (let i = 0, spanStart = 0, length = formattedString.spans.length; i < length; i++) {
const span = formattedString.spans.getItem(i);
Expand All @@ -265,6 +358,7 @@ export class TextBase extends TextBaseCommon {

const nsAttributedString = this.createMutableStringForSpan(span, spanText);
mas.insertAttributedStringAtIndex(nsAttributedString, spanStart);
this._spanRanges.push({location: spanStart, length: spanText.length});
spanStart += spanText.length;
}
}
Expand Down Expand Up @@ -349,3 +443,17 @@ export function getTransformedText(text: string, textTransform: TextTransform):
function NSStringFromNSAttributedString(source: NSAttributedString | string): NSString {
return NSString.stringWithString(source instanceof NSAttributedString && source.string || <string>source);
}

function isStringTappable(formattedString: FormattedString) {
if (!formattedString) {
return false;
}
for (let i = 0, length = formattedString.spans.length; i < length; i++) {
const span = formattedString.spans.getItem(i);
if (span.tappable) {
return true;
}
}

return false;
}

Back | FazBrowse Home | New Git URL