diff --git a/apps/app/ui-tests-app/css/main-page.ts b/apps/app/ui-tests-app/css/main-page.ts index c892c71ea0..6c2e063580 100644 --- a/apps/app/ui-tests-app/css/main-page.ts +++ b/apps/app/ui-tests-app/css/main-page.ts @@ -45,5 +45,6 @@ export function loadExamples() { examples.set("background-shorthand", "css/background-shorthand"); examples.set("background-image-linear-gradient", "css/background-image-linear-gradient"); examples.set("background-image", "css/background-image"); + examples.set("tappable-span", "css/tappable-span"); return examples; } \ No newline at end of file diff --git a/apps/app/ui-tests-app/css/tappable-span.ts b/apps/app/ui-tests-app/css/tappable-span.ts new file mode 100644 index 0000000000..bb77c8b605 --- /dev/null +++ b/apps/app/ui-tests-app/css/tappable-span.ts @@ -0,0 +1,9 @@ +import { EventData, TextBase } from "tns-core-modules/ui/text-base"; + +export function foxTap(args: EventData) { + console.log("foxTap"); +} + +export function dogTap(args: EventData) { + console.log("dogTap"); +} diff --git a/apps/app/ui-tests-app/css/tappable-span.xml b/apps/app/ui-tests-app/css/tappable-span.xml new file mode 100644 index 0000000000..0730daf9ad --- /dev/null +++ b/apps/app/ui-tests-app/css/tappable-span.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/tns-core-modules/text/span.d.ts b/tns-core-modules/text/span.d.ts index 7cc9258ee1..9b2855e6ff 100644 --- a/tns-core-modules/text/span.d.ts +++ b/tns-core-modules/text/span.d.ts @@ -50,6 +50,16 @@ 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 /** diff --git a/tns-core-modules/text/span.ts b/tns-core-modules/text/span.ts index 55620609d6..b184683c68 100644 --- a/tns-core-modules/text/span.ts +++ b/tns-core-modules/text/span.ts @@ -2,10 +2,12 @@ import { Span as SpanDefinition } from "./span"; import { ViewBase } from "../ui/core/view"; import { FontStyle, FontWeight, } from "../ui/styling/font"; -import { TextDecoration } from "../ui/text-base"; +import { TextDecoration, EventData } from "../ui/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; @@ -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); + } + } } diff --git a/tns-core-modules/ui/text-base/text-base.android.ts b/tns-core-modules/ui/text-base/text-base.android.ts index 9ae4eb3679..28afb3a0d8 100644 --- a/tns-core-modules/ui/text-base/text-base.android.ts +++ b/tns-core-modules/ui/text-base/text-base.android.ts @@ -50,6 +50,41 @@ 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; + + 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; @@ -59,12 +94,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(); @@ -110,6 +148,7 @@ export class TextBase extends TextBaseCommon { if (!reset && this.formattedText) { return; } + this._setTappableState(false); this._setNativeText(reset); } @@ -130,6 +169,7 @@ export class TextBase extends TextBaseCommon { const spannableStringBuilder = createSpannableStringBuilder(value); nativeView.setText(spannableStringBuilder); + this._setTappableState(isStringTappable(value)); textProperty.nativeValueChange(this, (value === null || value === undefined) ? "" : value.toString()); @@ -312,6 +352,19 @@ export class TextBase extends TextBaseCommon { this.nativeTextViewProtected.setText(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 { @@ -343,6 +396,19 @@ 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; @@ -441,6 +507,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) {