From 76615e94292615425620a403b62bd0a9119f0c05 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 26 Mar 2019 18:44:10 -0300 Subject: [PATCH 01/10] feat(android): clickable span Initial support for clickable span on Android --- nativescript-core/ui/text-base/span.d.ts | 121 ++++++++++-------- nativescript-core/ui/text-base/span.ts | 26 +++- .../ui/text-base/text-base.android.ts | 73 +++++++++++ 3 files changed, 163 insertions(+), 57 deletions(-) diff --git a/nativescript-core/ui/text-base/span.d.ts b/nativescript-core/ui/text-base/span.d.ts index 9c93d8ddca..52a52f19c3 100644 --- a/nativescript-core/ui/text-base/span.d.ts +++ b/nativescript-core/ui/text-base/span.d.ts @@ -2,59 +2,68 @@ * @module "ui/text-base/span" */ /** */ -import { Color } from "../../color"; -import { ViewBase } from "../core/view-base"; -import { FontStyle, FontWeight } from "../styling/font"; -import { TextDecoration } from "../text-base"; - -/** - * A class used to create a single part of formatted string with a common text properties. - */ -export class Span extends ViewBase { - /** - * Gets or sets the font family of the span. - */ - public fontFamily: string; - - /** - * Gets or sets the font size of the span. - */ - public fontSize: number; - - /** - * Gets or sets the font style of the span. - */ - public fontStyle: FontStyle; - - /** - * Gets or sets the font weight of the span. - */ - public fontWeight: FontWeight; - - /** - * Gets or sets text decorations for the span. - */ - public textDecoration: TextDecoration; - - /** - * Gets or sets the font foreground color of the span. - */ - public color: Color; - - /** - * Gets or sets the font background color of the span. - */ - public backgroundColor: Color; - - /** - * Gets or sets the text for the span. - */ - public text: string; - - //@private - /** - * @private - */ - _setTextInternal(value: string): void; - //@endprivate -} + import { Color } from "../../color"; + import { ViewBase } from "../core/view-base"; + import { FontStyle, FontWeight } from "../styling/font"; + import { TextDecoration } from "../text-base"; + + /** + * A class used to create a single part of formatted string with a common text properties. + */ + export class Span extends ViewBase { + /** + * Gets or sets the font family of the span. + */ + public fontFamily: string; + + /** + * Gets or sets the font size of the span. + */ + public fontSize: number; + + /** + * Gets or sets the font style of the span. + */ + public fontStyle: FontStyle; + + /** + * Gets or sets the font weight of the span. + */ + public fontWeight: FontWeight; + + /** + * Gets or sets text decorations for the span. + */ + public textDecoration: TextDecoration; + + /** + * Gets or sets the font foreground color of the span. + */ + public color: Color; + + /** + * Gets or sets the font background color of the span. + */ + public backgroundColor: Color; + + /** + * 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 + } \ No newline at end of file diff --git a/nativescript-core/ui/text-base/span.ts b/nativescript-core/ui/text-base/span.ts index ce485cd15f..e4608a091f 100644 --- a/nativescript-core/ui/text-base/span.ts +++ b/nativescript-core/ui/text-base/span.ts @@ -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 linkClickEvent = "linkClick"; private _text: string; + private _clickable: boolean = false; get fontFamily(): string { return this.style.fontFamily; @@ -68,7 +70,29 @@ export class Span extends ViewBase implements SpanDefinition { } } + get clickable(): boolean { + return this._clickable; + } + + addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any) { + console.log(arg); + super.addEventListener(arg, callback, thisArg); + this._setClickable(this.hasListeners(Span.linkClickEvent)); + } + + removeEventListener(arg: string, callback?: any, thisArg?: any) { + super.removeEventListener(arg, callback, thisArg); + this._setClickable(this.hasListeners(Span.linkClickEvent)); + } + _setTextInternal(value: string): void { this._text = value; } + + private _setClickable(value: boolean): void { + if (this._clickable !== value) { + this._clickable = value; + this.notifyPropertyChange("clickable", value); + } + } } diff --git a/nativescript-core/ui/text-base/text-base.android.ts b/nativescript-core/ui/text-base/text-base.android.ts index d7cf1bfa75..a572d541c9 100644 --- a/nativescript-core/ui/text-base/text-base.android.ts +++ b/nativescript-core/ui/text-base/text-base.android.ts @@ -51,6 +51,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.notify({ eventName: Span.linkClickEvent, object: owner }); + } + 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; @@ -60,12 +95,15 @@ export class TextBase extends TextBaseCommon { private _maxHeight: number; private _minLines: number; private _maxLines: number; + private _clickable: 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(); @@ -112,6 +150,8 @@ export class TextBase extends TextBaseCommon { return; } + this._setClickableState(false); + this._setNativeText(reset); } @@ -131,6 +171,7 @@ export class TextBase extends TextBaseCommon { const spannableStringBuilder = createSpannableStringBuilder(value); nativeView.setText(spannableStringBuilder); + this._setClickableState(isStringClickable(value)); textProperty.nativeValueChange(this, (value === null || value === undefined) ? "" : value.toString()); @@ -315,6 +356,19 @@ export class TextBase extends TextBaseCommon { this.nativeTextViewProtected.setText(transformedText); } + + _setClickableState(clickable: boolean) { + if (this._clickable !== clickable) { + this._clickable = clickable; + if (this._clickable) { + this.nativeViewProtected.setMovementMethod(android.text.method.LinkMovementMethod.getInstance()); + this.nativeViewProtected.setHighlightColor(null); + } + else { + this.nativeViewProtected.setMovementMethod(this._defaultMovementMethod); + } + } + } } function getCapitalizedString(str: string): string { @@ -346,6 +400,19 @@ export function getTransformedText(text: string, textTransform: TextTransform): } } +function isStringClickable(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.clickable) { + return true; + } + } + return false; +} + function createSpannableStringBuilder(formattedString: FormattedString): android.text.SpannableStringBuilder { if (!formattedString || !formattedString.parent) { return null; @@ -444,6 +511,12 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span, } } + const clickable = span.clickable; + if (clickable) { + 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) { From 78f4ef610af9d8e9bfe88379079963a173c33253 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 26 Mar 2019 18:58:59 -0300 Subject: [PATCH 02/10] test: clickable-span test page --- e2e/ui-tests-app/app/css/clickable-span-page.ts | 9 +++++++++ .../app/css/clickable-span-page.xml | 17 +++++++++++++++++ e2e/ui-tests-app/app/css/main-page.ts | 1 + 3 files changed, 27 insertions(+) create mode 100644 e2e/ui-tests-app/app/css/clickable-span-page.ts create mode 100644 e2e/ui-tests-app/app/css/clickable-span-page.xml diff --git a/e2e/ui-tests-app/app/css/clickable-span-page.ts b/e2e/ui-tests-app/app/css/clickable-span-page.ts new file mode 100644 index 0000000000..bb77c8b605 --- /dev/null +++ b/e2e/ui-tests-app/app/css/clickable-span-page.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/e2e/ui-tests-app/app/css/clickable-span-page.xml b/e2e/ui-tests-app/app/css/clickable-span-page.xml new file mode 100644 index 0000000000..6d90f09058 --- /dev/null +++ b/e2e/ui-tests-app/app/css/clickable-span-page.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/e2e/ui-tests-app/app/css/main-page.ts b/e2e/ui-tests-app/app/css/main-page.ts index 28a135d952..6a094b5447 100644 --- a/e2e/ui-tests-app/app/css/main-page.ts +++ b/e2e/ui-tests-app/app/css/main-page.ts @@ -46,6 +46,7 @@ export function loadExamples() { examples.set("background-image-linear-gradient", "css/background-image-linear-gradient-page"); examples.set("background-image", "css/background-image-page"); examples.set("styles", "css/styles-page"); + examples.set("clickable-span", "css/clickable-span-page"); return examples; } From 2ac54920f08ee706fc9b6d640e6594a6d7eae459 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 26 Mar 2019 19:22:21 -0300 Subject: [PATCH 03/10] remove console.log --- nativescript-core/ui/text-base/span.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/nativescript-core/ui/text-base/span.ts b/nativescript-core/ui/text-base/span.ts index e4608a091f..1c880562fb 100644 --- a/nativescript-core/ui/text-base/span.ts +++ b/nativescript-core/ui/text-base/span.ts @@ -75,7 +75,6 @@ export class Span extends ViewBase implements SpanDefinition { } addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any) { - console.log(arg); super.addEventListener(arg, callback, thisArg); this._setClickable(this.hasListeners(Span.linkClickEvent)); } From f85632637a27dd95b614b1b79986b8381d04f71f Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 29 Mar 2019 12:07:42 -0300 Subject: [PATCH 04/10] use _emit instead of notify --- nativescript-core/ui/text-base/text-base.android.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nativescript-core/ui/text-base/text-base.android.ts b/nativescript-core/ui/text-base/text-base.android.ts index a572d541c9..e7aefc6b46 100644 --- a/nativescript-core/ui/text-base/text-base.android.ts +++ b/nativescript-core/ui/text-base/text-base.android.ts @@ -73,7 +73,7 @@ function initializeClickableSpan(): void { onClick(view: android.view.View): void { const owner = this.owner.get(); if (owner) { - owner.notify({ eventName: Span.linkClickEvent, object: owner }); + owner._emit(Span.linkClickEvent); } view.clearFocus(); view.invalidate(); From 3ec6599563ece14dc1a97c0f7a9ef8a3faf129e4 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 6 May 2019 20:20:03 -0300 Subject: [PATCH 05/10] rename clickable to tappable in Span --- e2e/ui-tests-app/app/css/main-page.ts | 2 +- ...ble-span-page.ts => tappable-span-page.ts} | 0 ...e-span-page.xml => tappable-span-page.xml} | 4 ++-- nativescript-core/ui/text-base/span.ts | 20 ++++++++-------- .../ui/text-base/text-base.android.ts | 24 +++++++++---------- 5 files changed, 25 insertions(+), 25 deletions(-) rename e2e/ui-tests-app/app/css/{clickable-span-page.ts => tappable-span-page.ts} (100%) rename e2e/ui-tests-app/app/css/{clickable-span-page.xml => tappable-span-page.xml} (92%) diff --git a/e2e/ui-tests-app/app/css/main-page.ts b/e2e/ui-tests-app/app/css/main-page.ts index 6a094b5447..c64d278f38 100644 --- a/e2e/ui-tests-app/app/css/main-page.ts +++ b/e2e/ui-tests-app/app/css/main-page.ts @@ -46,7 +46,7 @@ export function loadExamples() { examples.set("background-image-linear-gradient", "css/background-image-linear-gradient-page"); examples.set("background-image", "css/background-image-page"); examples.set("styles", "css/styles-page"); - examples.set("clickable-span", "css/clickable-span-page"); + examples.set("tappable-span", "css/tappable-span-page"); return examples; } diff --git a/e2e/ui-tests-app/app/css/clickable-span-page.ts b/e2e/ui-tests-app/app/css/tappable-span-page.ts similarity index 100% rename from e2e/ui-tests-app/app/css/clickable-span-page.ts rename to e2e/ui-tests-app/app/css/tappable-span-page.ts diff --git a/e2e/ui-tests-app/app/css/clickable-span-page.xml b/e2e/ui-tests-app/app/css/tappable-span-page.xml similarity index 92% rename from e2e/ui-tests-app/app/css/clickable-span-page.xml rename to e2e/ui-tests-app/app/css/tappable-span-page.xml index 6d90f09058..0730daf9ad 100644 --- a/e2e/ui-tests-app/app/css/clickable-span-page.xml +++ b/e2e/ui-tests-app/app/css/tappable-span-page.xml @@ -5,9 +5,9 @@ - + - + diff --git a/nativescript-core/ui/text-base/span.ts b/nativescript-core/ui/text-base/span.ts index 1c880562fb..c37adf340b 100644 --- a/nativescript-core/ui/text-base/span.ts +++ b/nativescript-core/ui/text-base/span.ts @@ -5,9 +5,9 @@ import { FontStyle, FontWeight, } from "../styling/font"; import { TextDecoration, EventData } from "../text-base"; export class Span extends ViewBase implements SpanDefinition { - static linkClickEvent = "linkClick"; + static linkTapEvent = "linkTap"; private _text: string; - private _clickable: boolean = false; + private _tappable: boolean = false; get fontFamily(): string { return this.style.fontFamily; @@ -70,28 +70,28 @@ export class Span extends ViewBase implements SpanDefinition { } } - get clickable(): boolean { - return this._clickable; + get tappable(): boolean { + return this._tappable; } addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any) { super.addEventListener(arg, callback, thisArg); - this._setClickable(this.hasListeners(Span.linkClickEvent)); + this._setTappable(this.hasListeners(Span.linkTapEvent)); } removeEventListener(arg: string, callback?: any, thisArg?: any) { super.removeEventListener(arg, callback, thisArg); - this._setClickable(this.hasListeners(Span.linkClickEvent)); + this._setTappable(this.hasListeners(Span.linkTapEvent)); } _setTextInternal(value: string): void { this._text = value; } - private _setClickable(value: boolean): void { - if (this._clickable !== value) { - this._clickable = value; - this.notifyPropertyChange("clickable", value); + private _setTappable(value: boolean): void { + if (this._tappable !== value) { + this._tappable = value; + this.notifyPropertyChange("tappable", value); } } } diff --git a/nativescript-core/ui/text-base/text-base.android.ts b/nativescript-core/ui/text-base/text-base.android.ts index e7aefc6b46..b4b370acb1 100644 --- a/nativescript-core/ui/text-base/text-base.android.ts +++ b/nativescript-core/ui/text-base/text-base.android.ts @@ -73,7 +73,7 @@ function initializeClickableSpan(): void { onClick(view: android.view.View): void { const owner = this.owner.get(); if (owner) { - owner._emit(Span.linkClickEvent); + owner._emit(Span.linkTapEvent); } view.clearFocus(); view.invalidate(); @@ -95,7 +95,7 @@ export class TextBase extends TextBaseCommon { private _maxHeight: number; private _minLines: number; private _maxLines: number; - private _clickable: boolean = false; + private _tappable: boolean = false; private _defaultMovementMethod: android.text.method.MovementMethod; public initNativeView(): void { @@ -150,7 +150,7 @@ export class TextBase extends TextBaseCommon { return; } - this._setClickableState(false); + this._setTappableState(false); this._setNativeText(reset); } @@ -171,7 +171,7 @@ export class TextBase extends TextBaseCommon { const spannableStringBuilder = createSpannableStringBuilder(value); nativeView.setText(spannableStringBuilder); - this._setClickableState(isStringClickable(value)); + this._setTappableState(isStringTappable(value)); textProperty.nativeValueChange(this, (value === null || value === undefined) ? "" : value.toString()); @@ -357,10 +357,10 @@ export class TextBase extends TextBaseCommon { this.nativeTextViewProtected.setText(transformedText); } - _setClickableState(clickable: boolean) { - if (this._clickable !== clickable) { - this._clickable = clickable; - if (this._clickable) { + _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); } @@ -400,13 +400,13 @@ export function getTransformedText(text: string, textTransform: TextTransform): } } -function isStringClickable(formattedString: FormattedString) { +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.clickable) { + if (span.tappable) { return true; } } @@ -511,8 +511,8 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span, } } - const clickable = span.clickable; - if (clickable) { + const tappable = span.tappable; + if (tappable) { initializeClickableSpan(); ssb.setSpan(new ClickableSpan(span), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } From dccefd77f6102abcafccdfd4043e1e46ff9653e1 Mon Sep 17 00:00:00 2001 From: Vasil Trifonov Date: Fri, 17 Jan 2020 16:38:14 +0200 Subject: [PATCH 06/10] updated NativeScript.api.md --- api-reports/NativeScript.api.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/api-reports/NativeScript.api.md b/api-reports/NativeScript.api.md index be3a9b1754..ae82528e74 100644 --- a/api-reports/NativeScript.api.md +++ b/api-reports/NativeScript.api.md @@ -1916,26 +1916,30 @@ export class Slider extends View { // @public export class Span extends ViewBase { - public backgroundColor: Color; + public backgroundColor: Color; - public color: Color; + public color: Color; - public fontFamily: string; + public fontFamily: string; - public fontSize: number; + public fontSize: number; - public fontStyle: FontStyle; + public fontStyle: FontStyle; - public fontWeight: FontWeight; + public fontWeight: FontWeight; - // (undocumented) - _setTextInternal(value: string): void; + public static linkTapEvent: string; - public text: string; + // (undocumented) + _setTextInternal(value: string): void; - public textDecoration: TextDecoration; - //@endprivate -} + public readonly tappable: boolean; + + public text: string; + + public textDecoration: TextDecoration; + //@endprivate + } // @public export class StackLayout extends LayoutBase { From ef3e4e27d11384ec415a4cd089b88701243848ab Mon Sep 17 00:00:00 2001 From: Vasil Trifonov Date: Fri, 17 Jan 2020 16:52:54 +0200 Subject: [PATCH 07/10] chore: fixing tslint errors --- nativescript-core/ui/text-base/text-base.android.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nativescript-core/ui/text-base/text-base.android.ts b/nativescript-core/ui/text-base/text-base.android.ts index b4b370acb1..200285c8aa 100644 --- a/nativescript-core/ui/text-base/text-base.android.ts +++ b/nativescript-core/ui/text-base/text-base.android.ts @@ -68,6 +68,7 @@ function initializeClickableSpan(): void { constructor(owner: Span) { super(); this.owner = new WeakRef(owner); + return global.__native(this); } onClick(view: android.view.View): void { @@ -410,6 +411,7 @@ function isStringTappable(formattedString: FormattedString) { return true; } } + return false; } From 4086ce30e676cd034c0ca95d9cd15b0a45ed8853 Mon Sep 17 00:00:00 2001 From: Vasil Trifonov Date: Thu, 19 Mar 2020 13:13:22 +0200 Subject: [PATCH 08/10] chore: fixed witespacing --- api-reports/NativeScript.api.md | 28 +++--- nativescript-core/ui/text-base/span.d.ts | 110 +++++++++++------------ 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/api-reports/NativeScript.api.md b/api-reports/NativeScript.api.md index 67a4e4039f..a3d196f12e 100644 --- a/api-reports/NativeScript.api.md +++ b/api-reports/NativeScript.api.md @@ -1924,29 +1924,29 @@ export class Slider extends View { // @public export class Span extends ViewBase { - public backgroundColor: Color; + public backgroundColor: Color; - public color: Color; + public color: Color; - public fontFamily: string; + public fontFamily: string; - public fontSize: number; + public fontSize: number; - public fontStyle: FontStyle; + public fontStyle: FontStyle; - public fontWeight: FontWeight; + public fontWeight: FontWeight; - public static linkTapEvent: string; + public static linkTapEvent: string; - // (undocumented) - _setTextInternal(value: string): void; + // (undocumented) + _setTextInternal(value: string): void; - public readonly tappable: boolean; + public readonly tappable: boolean; - public text: string; + public text: string; - public textDecoration: TextDecoration; - //@endprivate + public textDecoration: TextDecoration; + //@endprivate } // @public @@ -2426,7 +2426,7 @@ export interface TapGestureEventData extends GestureEventData { getPointerCount(): number; getX(): number; - + getY(): number; } diff --git a/nativescript-core/ui/text-base/span.d.ts b/nativescript-core/ui/text-base/span.d.ts index 52a52f19c3..f648dd198d 100644 --- a/nativescript-core/ui/text-base/span.d.ts +++ b/nativescript-core/ui/text-base/span.d.ts @@ -2,68 +2,68 @@ * @module "ui/text-base/span" */ /** */ - import { Color } from "../../color"; - import { ViewBase } from "../core/view-base"; - import { FontStyle, FontWeight } from "../styling/font"; - import { TextDecoration } from "../text-base"; +import { Color } from "../../color"; +import { ViewBase } from "../core/view-base"; +import { FontStyle, FontWeight } from "../styling/font"; +import { TextDecoration } from "../text-base"; - /** - * A class used to create a single part of formatted string with a common text properties. - */ - export class Span extends ViewBase { - /** - * Gets or sets the font family of the span. - */ - public fontFamily: string; +/** + * A class used to create a single part of formatted string with a common text properties. + */ +export class Span extends ViewBase { + /** + * Gets or sets the font family of the span. + */ + public fontFamily: string; - /** - * Gets or sets the font size of the span. - */ - public fontSize: number; + /** + * Gets or sets the font size of the span. + */ + public fontSize: number; - /** - * Gets or sets the font style of the span. - */ - public fontStyle: FontStyle; + /** + * Gets or sets the font style of the span. + */ + public fontStyle: FontStyle; - /** - * Gets or sets the font weight of the span. - */ - public fontWeight: FontWeight; + /** + * Gets or sets the font weight of the span. + */ + public fontWeight: FontWeight; - /** - * Gets or sets text decorations for the span. - */ - public textDecoration: TextDecoration; + /** + * Gets or sets text decorations for the span. + */ + public textDecoration: TextDecoration; - /** - * Gets or sets the font foreground color of the span. - */ - public color: Color; + /** + * Gets or sets the font foreground color of the span. + */ + public color: Color; - /** - * Gets or sets the font background color of the span. - */ - public backgroundColor: Color; + /** + * Gets or sets the font background color of the span. + */ + public backgroundColor: Color; - /** - * Gets or sets the text for the span. - */ - public text: string; - /** - * String value used when hooking to linkTap event. - */ - public static linkTapEvent: string; + /** + * 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; + /** + * Gets if the span is tappable or not. + */ + public readonly tappable: boolean; - //@private - /** - * @private - */ - _setTextInternal(value: string): void; - //@endprivate - } \ No newline at end of file + //@private + /** + * @private + */ + _setTextInternal(value: string): void; + //@endprivate +} \ No newline at end of file From 40a4946d55839b7f8c7cfe324df3e0e88573bf4d Mon Sep 17 00:00:00 2001 From: Vasil Trifonov Date: Fri, 20 Mar 2020 13:47:05 +0200 Subject: [PATCH 09/10] moved and improved test page --- e2e/ui-tests-app/app/button/main-page.ts | 1 + e2e/ui-tests-app/app/css/main-page.ts | 1 - e2e/ui-tests-app/app/css/tappable-span-page.ts | 9 --------- e2e/ui-tests-app/app/css/tappable-span-page.xml | 17 ----------------- 4 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 e2e/ui-tests-app/app/css/tappable-span-page.ts delete mode 100644 e2e/ui-tests-app/app/css/tappable-span-page.xml diff --git a/e2e/ui-tests-app/app/button/main-page.ts b/e2e/ui-tests-app/app/button/main-page.ts index ecb25dc25f..5239daa131 100644 --- a/e2e/ui-tests-app/app/button/main-page.ts +++ b/e2e/ui-tests-app/app/button/main-page.ts @@ -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; } diff --git a/e2e/ui-tests-app/app/css/main-page.ts b/e2e/ui-tests-app/app/css/main-page.ts index c64d278f38..28a135d952 100644 --- a/e2e/ui-tests-app/app/css/main-page.ts +++ b/e2e/ui-tests-app/app/css/main-page.ts @@ -46,7 +46,6 @@ export function loadExamples() { examples.set("background-image-linear-gradient", "css/background-image-linear-gradient-page"); examples.set("background-image", "css/background-image-page"); examples.set("styles", "css/styles-page"); - examples.set("tappable-span", "css/tappable-span-page"); return examples; } diff --git a/e2e/ui-tests-app/app/css/tappable-span-page.ts b/e2e/ui-tests-app/app/css/tappable-span-page.ts deleted file mode 100644 index bb77c8b605..0000000000 --- a/e2e/ui-tests-app/app/css/tappable-span-page.ts +++ /dev/null @@ -1,9 +0,0 @@ -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/e2e/ui-tests-app/app/css/tappable-span-page.xml b/e2e/ui-tests-app/app/css/tappable-span-page.xml deleted file mode 100644 index 0730daf9ad..0000000000 --- a/e2e/ui-tests-app/app/css/tappable-span-page.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file From 5d09309a38d472ad65b0d441d65dba29a3f2db88 Mon Sep 17 00:00:00 2001 From: Vasil Trifonov Date: Fri, 20 Mar 2020 13:56:48 +0200 Subject: [PATCH 10/10] feat: tappable span iOS implementation --- .../ui/text-base/text-base.ios.ts | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/nativescript-core/ui/text-base/text-base.ios.ts b/nativescript-core/ui/text-base/text-base.ios.ts index 568f2ef038..0a1618afbb 100644 --- a/nativescript-core/ui/text-base/text-base.ios.ts +++ b/nativescript-core/ui/text-base/text-base.ios.ts @@ -15,10 +15,101 @@ export * from "./text-base-common"; const majorVersion = ios.MajorVersion; +class UILabelClickHandlerImpl extends NSObject { + private _owner: WeakRef; + + public static initWithOwner(owner: WeakRef): UILabelClickHandlerImpl { + let handler = 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 = 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. + (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; @@ -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(); } @@ -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); @@ -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; } } @@ -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 || 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; +} \ No newline at end of file