| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 92b5b02 commit c24c0d5
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1936,14 +1936,18 @@ export class Span extends ViewBase { | |||
| 1936 | 1936 | ||
| 1937 | 1937 | public fontWeight: FontWeight; | |
| 1938 | 1938 | ||
| 1939 | + public static linkTapEvent: string; | ||
| 1940 | + | ||
| 1939 | 1941 | // (undocumented) | |
| 1940 | 1942 | _setTextInternal(value: string): void; | |
| 1941 | 1943 | ||
| 1944 | + public readonly tappable: boolean; | ||
| 1945 | + | ||
| 1942 | 1946 | public text: string; | |
| 1943 | 1947 | ||
| 1944 | 1948 | public textDecoration: TextDecoration; | |
| 1945 | 1949 | //@endprivate | |
| 1946 | - } | ||
| 1950 | + } | ||
| 1947 | 1951 | ||
| 1948 | 1952 | // @public | |
| 1949 | 1953 | export class StackLayout extends LayoutBase { | |
@@ -2422,7 +2426,7 @@ export interface TapGestureEventData extends GestureEventData { | |||
| 2422 | 2426 | getPointerCount(): number; | |
| 2423 | 2427 | ||
| 2424 | 2428 | getX(): number; | |
| 2425 | - | ||
| 2429 | + | ||
| 2426 | 2430 | getY(): number; | |
| 2427 | 2431 | } | |
| 2428 | 2432 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -18,6 +18,7 @@ export function loadExamples() { | |||
| 18 | 18 | examples.set("issue-4287", "button/issue-4287-page"); | |
| 19 | 19 | examples.set("issue-4385", "button/issue-4385-page"); | |
| 20 | 20 | examples.set("highlight-4740", "button/highlight-4740/highlight-4740-page"); | |
| 21 | + examples.set("tappable-span", "button/tappable-span-page"); | ||
| 21 | 22 | ||
| 22 | 23 | return examples; | |
| 23 | 24 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -50,11 +50,20 @@ export class Span extends ViewBase { | |||
| 50 | 50 | * Gets or sets the text for the span. | |
| 51 | 51 | */ | |
| 52 | 52 | public text: string; | |
| 53 | + /** | ||
| 54 | + * String value used when hooking to linkTap event. | ||
| 55 | + */ | ||
| 56 | + public static linkTapEvent: string; | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * Gets if the span is tappable or not. | ||
| 60 | + */ | ||
| 61 | + public readonly tappable: boolean; | ||
| 53 | 62 | ||
| 54 | 63 | //@private | |
| 55 | 64 | /** | |
| 56 | 65 | * @private | |
| 57 | 66 | */ | |
| 58 | 67 | _setTextInternal(value: string): void; | |
| 59 | 68 | //@endprivate | |
| 60 | - } | ||
| 69 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,10 +2,12 @@ | |||
| 2 | 2 | import { Span as SpanDefinition } from "./span"; | |
| 3 | 3 | import { ViewBase } from "../core/view"; | |
| 4 | 4 | import { FontStyle, FontWeight, } from "../styling/font"; | |
| 5 | - import { TextDecoration } from "../text-base"; | ||
| 5 | + import { TextDecoration, EventData } from "../text-base"; | ||
| 6 | 6 | ||
| 7 | 7 | export class Span extends ViewBase implements SpanDefinition { | |
| 8 | + static linkTapEvent = "linkTap"; | ||
| 8 | 9 | private _text: string; | |
| 10 | + private _tappable: boolean = false; | ||
| 9 | 11 | ||
| 10 | 12 | get fontFamily(): string { | |
| 11 | 13 | return this.style.fontFamily; | |
@@ -68,7 +70,28 @@ export class Span extends ViewBase implements SpanDefinition { | |||
| 68 | 70 | } | |
| 69 | 71 | } | |
| 70 | 72 | ||
| 73 | + get tappable(): boolean { | ||
| 74 | + return this._tappable; | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any) { | ||
| 78 | + super.addEventListener(arg, callback, thisArg); | ||
| 79 | + this._setTappable(this.hasListeners(Span.linkTapEvent)); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + removeEventListener(arg: string, callback?: any, thisArg?: any) { | ||
| 83 | + super.removeEventListener(arg, callback, thisArg); | ||
| 84 | + this._setTappable(this.hasListeners(Span.linkTapEvent)); | ||
| 85 | + } | ||
| 86 | + | ||
| 71 | 87 | _setTextInternal(value: string): void { | |
| 72 | 88 | this._text = value; | |
| 73 | 89 | } | |
| 90 | + | ||
| 91 | + private _setTappable(value: boolean): void { | ||
| 92 | + if (this._tappable !== value) { | ||
| 93 | + this._tappable = value; | ||
| 94 | + this.notifyPropertyChange("tappable", value); | ||
| 95 | + } | ||
| 96 | + } | ||
| 74 | 97 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -51,6 +51,42 @@ function initializeTextTransformation(): void { | |||
| 51 | 51 | TextTransformation = TextTransformationImpl; | |
| 52 | 52 | } | |
| 53 | 53 | ||
| 54 | + interface ClickableSpan { | ||
| 55 | + new (owner: Span): android.text.style.ClickableSpan; | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + let ClickableSpan: ClickableSpan; | ||
| 59 | + | ||
| 60 | + function initializeClickableSpan(): void { | ||
| 61 | + if (ClickableSpan) { | ||
| 62 | + return; | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + class ClickableSpanImpl extends android.text.style.ClickableSpan { | ||
| 66 | + owner: WeakRef<Span>; | ||
| 67 | + | ||
| 68 | + constructor(owner: Span) { | ||
| 69 | + super(); | ||
| 70 | + this.owner = new WeakRef(owner); | ||
| 71 | + | ||
| 72 | + return global.__native(this); | ||
| 73 | + } | ||
| 74 | + onClick(view: android.view.View): void { | ||
| 75 | + const owner = this.owner.get(); | ||
| 76 | + if (owner) { | ||
| 77 | + owner._emit(Span.linkTapEvent); | ||
| 78 | + } | ||
| 79 | + view.clearFocus(); | ||
| 80 | + view.invalidate(); | ||
| 81 | + } | ||
| 82 | + updateDrawState(tp: android.text.TextPaint): void { | ||
| 83 | + // don't style as link | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + ClickableSpan = ClickableSpanImpl; | ||
| 88 | + } | ||
| 89 | + | ||
| 54 | 90 | export class TextBase extends TextBaseCommon { | |
| 55 | 91 | nativeViewProtected: android.widget.TextView; | |
| 56 | 92 | nativeTextViewProtected: android.widget.TextView; | |
@@ -60,12 +96,15 @@ export class TextBase extends TextBaseCommon { | |||
| 60 | 96 | private _maxHeight: number; | |
| 61 | 97 | private _minLines: number; | |
| 62 | 98 | private _maxLines: number; | |
| 99 | + private _tappable: boolean = false; | ||
| 100 | + private _defaultMovementMethod: android.text.method.MovementMethod; | ||
| 63 | 101 | ||
| 64 | 102 | public initNativeView(): void { | |
| 65 | 103 | super.initNativeView(); | |
| 66 | 104 | initializeTextTransformation(); | |
| 67 | 105 | const nativeView = this.nativeTextViewProtected; | |
| 68 | 106 | this._defaultTransformationMethod = nativeView.getTransformationMethod(); | |
| 107 | + this._defaultMovementMethod = this.nativeView.getMovementMethod(); | ||
| 69 | 108 | this._minHeight = nativeView.getMinHeight(); | |
| 70 | 109 | this._maxHeight = nativeView.getMaxHeight(); | |
| 71 | 110 | this._minLines = nativeView.getMinLines(); | |
@@ -112,6 +151,8 @@ export class TextBase extends TextBaseCommon { | |||
| 112 | 151 | return; | |
| 113 | 152 | } | |
| 114 | 153 | ||
| 154 | + this._setTappableState(false); | ||
| 155 | + | ||
| 115 | 156 | this._setNativeText(reset); | |
| 116 | 157 | } | |
| 117 | 158 | ||
@@ -131,6 +172,7 @@ export class TextBase extends TextBaseCommon { | |||
| 131 | 172 | ||
| 132 | 173 | const spannableStringBuilder = createSpannableStringBuilder(value); | |
| 133 | 174 | nativeView.setText(<any>spannableStringBuilder); | |
| 175 | + this._setTappableState(isStringTappable(value)); | ||
| 134 | 176 | ||
| 135 | 177 | textProperty.nativeValueChange(this, (value === null || value === undefined) ? "" : value.toString()); | |
| 136 | 178 | ||
@@ -315,6 +357,19 @@ export class TextBase extends TextBaseCommon { | |||
| 315 | 357 | ||
| 316 | 358 | this.nativeTextViewProtected.setText(<any>transformedText); | |
| 317 | 359 | } | |
| 360 | + | ||
| 361 | + _setTappableState(tappable: boolean) { | ||
| 362 | + if (this._tappable !== tappable) { | ||
| 363 | + this._tappable = tappable; | ||
| 364 | + if (this._tappable) { | ||
| 365 | + this.nativeViewProtected.setMovementMethod(android.text.method.LinkMovementMethod.getInstance()); | ||
| 366 | + this.nativeViewProtected.setHighlightColor(null); | ||
| 367 | + } | ||
| 368 | + else { | ||
| 369 | + this.nativeViewProtected.setMovementMethod(this._defaultMovementMethod); | ||
| 370 | + } | ||
| 371 | + } | ||
| 372 | + } | ||
| 318 | 373 | } | |
| 319 | 374 | ||
| 320 | 375 | function getCapitalizedString(str: string): string { | |
@@ -346,6 +401,20 @@ export function getTransformedText(text: string, textTransform: TextTransform): | |||
| 346 | 401 | } | |
| 347 | 402 | } | |
| 348 | 403 | ||
| 404 | + function isStringTappable(formattedString: FormattedString) { | ||
| 405 | + if (!formattedString) { | ||
| 406 | + return false; | ||
| 407 | + } | ||
| 408 | + for (let i = 0, length = formattedString.spans.length; i < length; i++) { | ||
| 409 | + const span = formattedString.spans.getItem(i); | ||
| 410 | + if (span.tappable) { | ||
| 411 | + return true; | ||
| 412 | + } | ||
| 413 | + } | ||
| 414 | + | ||
| 415 | + return false; | ||
| 416 | + } | ||
| 417 | + | ||
| 349 | 418 | function createSpannableStringBuilder(formattedString: FormattedString): android.text.SpannableStringBuilder { | |
| 350 | 419 | if (!formattedString || !formattedString.parent) { | |
| 351 | 420 | return null; | |
@@ -444,6 +513,12 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span, | |||
| 444 | 513 | } | |
| 445 | 514 | } | |
| 446 | 515 | ||
| 516 | + const tappable = span.tappable; | ||
| 517 | + if (tappable) { | ||
| 518 | + initializeClickableSpan(); | ||
| 519 | + ssb.setSpan(new ClickableSpan(span), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||
| 520 | + } | ||
| 521 | + | ||
| 447 | 522 | // TODO: Implement letterSpacing for Span here. | |
| 448 | 523 | // const letterSpacing = formattedString.parent.style.letterSpacing; | |
| 449 | 524 | // if (letterSpacing > 0) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,10 +15,101 @@ export * from "./text-base-common"; | |||
| 15 | 15 | ||
| 16 | 16 | const majorVersion = ios.MajorVersion; | |
| 17 | 17 | ||
| 18 | + class UILabelClickHandlerImpl extends NSObject { | ||
| 19 | + private _owner: WeakRef<TextBase>; | ||
| 20 | + | ||
| 21 | + public static initWithOwner(owner: WeakRef<TextBase>): UILabelClickHandlerImpl { | ||
| 22 | + let handler = <UILabelClickHandlerImpl>UILabelClickHandlerImpl.new(); | ||
| 23 | + handler._owner = owner; | ||
| 24 | + | ||
| 25 | + return handler; | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + public linkTap(tapGesture: UITapGestureRecognizer) { | ||
| 29 | + let owner = this._owner.get(); | ||
| 30 | + if (owner) { | ||
| 31 | + // https://stackoverflow.com/a/35789589 | ||
| 32 | + let label = <UILabel>owner.nativeTextViewProtected; | ||
| 33 | + let layoutManager = NSLayoutManager.alloc().init(); | ||
| 34 | + let textContainer = NSTextContainer.alloc().initWithSize(CGSizeZero); | ||
| 35 | + let textStorage = NSTextStorage.alloc().initWithAttributedString(owner.nativeTextViewProtected["attributedText"]); | ||
| 36 | + | ||
| 37 | + layoutManager.addTextContainer(textContainer); | ||
| 38 | + textStorage.addLayoutManager(layoutManager); | ||
| 39 | + | ||
| 40 | + textContainer.lineFragmentPadding = 0; | ||
| 41 | + textContainer.lineBreakMode = label.lineBreakMode; | ||
| 42 | + textContainer.maximumNumberOfLines = label.numberOfLines; | ||
| 43 | + let labelSize = label.bounds.size; | ||
| 44 | + textContainer.size = labelSize; | ||
| 45 | + | ||
| 46 | + let locationOfTouchInLabel = tapGesture.locationInView(label); | ||
| 47 | + let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer); | ||
| 48 | + | ||
| 49 | + let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, | ||
| 50 | + (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y); | ||
| 51 | + | ||
| 52 | + let locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x, | ||
| 53 | + locationOfTouchInLabel.y - textContainerOffset.y); | ||
| 54 | + | ||
| 55 | + let indexOfCharacter = layoutManager.characterIndexForPointInTextContainerFractionOfDistanceBetweenInsertionPoints( | ||
| 56 | + locationOfTouchInTextContainer, textContainer, null); | ||
| 57 | + | ||
| 58 | + let span: Span = null; | ||
| 59 | + // try to find the corresponding span using the spanRanges | ||
| 60 | + for (let i = 0; i < owner._spanRanges.length; i++) { | ||
| 61 | + let range = owner._spanRanges[i]; | ||
| 62 | + if ((range.location <= indexOfCharacter) && (range.location + range.length) > indexOfCharacter) { | ||
| 63 | + if (owner.formattedText && owner.formattedText.spans.length > i) { | ||
| 64 | + span = owner.formattedText.spans.getItem(i); | ||
| 65 | + } | ||
| 66 | + break; | ||
| 67 | + } | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + if (span && span.tappable) { | ||
| 71 | + // if the span is found and tappable emit the linkTap event | ||
| 72 | + span._emit(Span.linkTapEvent); | ||
| 73 | + } | ||
| 74 | + } | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + public static ObjCExposedMethods = { | ||
| 78 | + "linkTap": { returns: interop.types.void, params: [interop.types.id] } | ||
| 79 | + }; | ||
| 80 | + } | ||
| 81 | + | ||
| 18 | 82 | export class TextBase extends TextBaseCommon { | |
| 19 | 83 | ||
| 20 | 84 | public nativeViewProtected: UITextField | UITextView | UILabel | UIButton; | |
| 21 | 85 | public nativeTextViewProtected: UITextField | UITextView | UILabel | UIButton; | |
| 86 | + private _tappable: boolean = false; | ||
| 87 | + private _tapGestureRecognizer: UITapGestureRecognizer; | ||
| 88 | + public _spanRanges: NSRange[]; | ||
| 89 | + | ||
| 90 | + public initNativeView(): void { | ||
| 91 | + super.initNativeView(); | ||
| 92 | + this._setTappableState(false); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + _setTappableState(tappable: boolean) { | ||
| 96 | + if (this._tappable !== tappable) { | ||
| 97 | + this._tappable = tappable; | ||
| 98 | + if (this._tappable) { | ||
| 99 | + const tapHandler = UILabelClickHandlerImpl.initWithOwner(new WeakRef(this)); | ||
| 100 | + // associate handler with menuItem or it will get collected by JSC. | ||
| 101 | + (<any>this).handler = tapHandler; | ||
| 102 | + | ||
| 103 | + this._tapGestureRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(tapHandler, "linkTap"); | ||
| 104 | + this.nativeViewProtected.userInteractionEnabled = true; | ||
| 105 | + this.nativeViewProtected.addGestureRecognizer(this._tapGestureRecognizer); | ||
| 106 | + } | ||
| 107 | + else { | ||
| 108 | + this.nativeViewProtected.userInteractionEnabled = false; | ||
| 109 | + this.nativeViewProtected.removeGestureRecognizer(this._tapGestureRecognizer); | ||
| 110 | + } | ||
| 111 | + } | ||
| 112 | + } | ||
| 22 | 113 | ||
| 23 | 114 | [textProperty.getDefault](): number | symbol { | |
| 24 | 115 | return resetSymbol; | |
@@ -35,6 +126,7 @@ export class TextBase extends TextBaseCommon { | |||
| 35 | 126 | ||
| 36 | 127 | [formattedTextProperty.setNative](value: FormattedString) { | |
| 37 | 128 | this._setNativeText(); | |
| 129 | + this._setTappableState(isStringTappable(value)); | ||
| 38 | 130 | textProperty.nativeValueChange(this, !value ? "" : value.toString()); | |
| 39 | 131 | this._requestLayoutOnTextChanged(); | |
| 40 | 132 | } | |
@@ -253,6 +345,7 @@ export class TextBase extends TextBaseCommon { | |||
| 253 | 345 | ||
| 254 | 346 | createNSMutableAttributedString(formattedString: FormattedString): NSMutableAttributedString { | |
| 255 | 347 | let mas = NSMutableAttributedString.alloc().init(); | |
| 348 | + this._spanRanges = []; | ||
| 256 | 349 | if (formattedString && formattedString.parent) { | |
| 257 | 350 | for (let i = 0, spanStart = 0, length = formattedString.spans.length; i < length; i++) { | |
| 258 | 351 | const span = formattedString.spans.getItem(i); | |
@@ -265,6 +358,7 @@ export class TextBase extends TextBaseCommon { | |||
| 265 | 358 | ||
| 266 | 359 | const nsAttributedString = this.createMutableStringForSpan(span, spanText); | |
| 267 | 360 | mas.insertAttributedStringAtIndex(nsAttributedString, spanStart); | |
| 361 | + this._spanRanges.push({location: spanStart, length: spanText.length}); | ||
| 268 | 362 | spanStart += spanText.length; | |
| 269 | 363 | } | |
| 270 | 364 | } | |
@@ -349,3 +443,17 @@ export function getTransformedText(text: string, textTransform: TextTransform): | |||
| 349 | 443 | function NSStringFromNSAttributedString(source: NSAttributedString | string): NSString { | |
| 350 | 444 | return NSString.stringWithString(source instanceof NSAttributedString && source.string || <string>source); | |
| 351 | 445 | } | |
| 446 | + | ||
| 447 | + function isStringTappable(formattedString: FormattedString) { | ||
| 448 | + if (!formattedString) { | ||
| 449 | + return false; | ||
| 450 | + } | ||
| 451 | + for (let i = 0, length = formattedString.spans.length; i < length; i++) { | ||
| 452 | + const span = formattedString.spans.getItem(i); | ||
| 453 | + if (span.tappable) { | ||
| 454 | + return true; | ||
| 455 | + } | ||
| 456 | + } | ||
| 457 | + | ||
| 458 | + return false; | ||
| 459 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments