UNPKG

@nativescript/core

Version:

A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.

457 lines • 21.4 kB
// Types import { getClosestPropertyValue } from './text-base-common'; // Requires import { Font } from '../styling/font'; import { iosAccessibilityAdjustsFontSizeProperty, iosAccessibilityMaxFontScaleProperty, iosAccessibilityMinFontScaleProperty } from '../../accessibility/accessibility-properties'; import { TextBaseCommon, textProperty, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, textStrokeProperty, letterSpacingProperty, lineHeightProperty, maxLinesProperty, resetSymbol } from './text-base-common'; import { Color } from '../../color'; import { Span } from './span'; import { colorProperty, fontInternalProperty, fontScaleInternalProperty, Length } from '../styling/style-properties'; import { isString, isNullOrUndefined } from '../../utils/types'; import { iOSNativeHelper, layout } from '../../utils'; import { CoreTypes } from '../../core-types'; export * from './text-base-common'; const majorVersion = iOSNativeHelper.MajorVersion; var UILabelClickHandlerImpl = /** @class */ (function (_super) { __extends(UILabelClickHandlerImpl, _super); function UILabelClickHandlerImpl() { return _super !== null && _super.apply(this, arguments) || this; } UILabelClickHandlerImpl.initWithOwner = function (owner) { var handler = UILabelClickHandlerImpl.new(); handler._owner = owner; return handler; }; UILabelClickHandlerImpl.prototype.linkTap = function (tapGesture) { var _a; var owner = (_a = this._owner) === null || _a === void 0 ? void 0 : _a.deref(); if (owner) { var 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 var offsetXMultiplier = void 0; switch (owner.textAlignment) { case 'center': offsetXMultiplier = 0.5; break; case 'right': offsetXMultiplier = 1.0; break; default: offsetXMultiplier = 0.0; break; } var offsetYMultiplier = 0.5; // Text is vertically aligned to center var layoutManager = NSLayoutManager.alloc().init(); var textContainer = NSTextContainer.alloc().initWithSize(CGSizeZero); var textStorage = NSTextStorage.alloc().initWithAttributedString(nativeView.attributedText); layoutManager.addTextContainer(textContainer); textStorage.addLayoutManager(layoutManager); textContainer.lineFragmentPadding = 0; 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; } } var labelSize = nativeView.bounds.size; textContainer.size = labelSize; var locationOfTouchInLabel = tapGesture.locationInView(nativeView); var textBoundingBox = layoutManager.usedRectForTextContainer(textContainer); var textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * offsetXMultiplier - textBoundingBox.origin.x, (labelSize.height - textBoundingBox.size.height) * offsetYMultiplier - textBoundingBox.origin.y); var locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x, locationOfTouchInLabel.y - textContainerOffset.y); // Check if tap was inside text bounding rect if (CGRectContainsPoint(textBoundingBox, locationOfTouchInTextContainer)) { // According to Apple docs, if no glyph is under point, the nearest glyph is returned var glyphIndex = layoutManager.glyphIndexForPointInTextContainerFractionOfDistanceThroughGlyph(locationOfTouchInTextContainer, textContainer, null); // In order to determine whether the tap point actually lies within the bounds // of the glyph returned, we call the method below and test // whether the point falls in the rectangle returned by that method var glyphRect = layoutManager.boundingRectForGlyphRangeInTextContainer({ location: glyphIndex, length: 1, }, textContainer); // Ensure that an actual glyph was tapped if (CGRectContainsPoint(glyphRect, locationOfTouchInTextContainer)) { var indexOfCharacter = layoutManager.characterIndexForGlyphAtIndex(glyphIndex); var span = null; // Try to find the corresponding span using the spanRanges for (var i = 0; i < owner._spanRanges.length; i++) { var 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); } } } } }; UILabelClickHandlerImpl.ObjCExposedMethods = { linkTap: { returns: interop.types.void, params: [interop.types.id] }, }; return UILabelClickHandlerImpl; }(NSObject)); export class TextBase extends TextBaseCommon { constructor() { super(...arguments); this._tappable = false; } get nativeTextViewProtected() { return super.nativeTextViewProtected; } initNativeView() { super.initNativeView(); this._setTappableState(false); } disposeNativeView() { super.disposeNativeView(); this._tappable = false; this._linkTapHandler = null; this._tapGestureRecognizer = null; } _setTappableState(tappable) { 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 this._linkTapHandler = tapHandler; this._tapGestureRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(this._linkTapHandler, 'linkTap'); nativeTextView.addGestureRecognizer(this._tapGestureRecognizer); } else { nativeTextView.removeGestureRecognizer(this._tapGestureRecognizer); this._linkTapHandler = null; this._tapGestureRecognizer = null; } } } [textProperty.getDefault]() { return resetSymbol; } [textProperty.setNative](value) { const reset = value === resetSymbol; if (!reset && this.formattedText) { return; } this._setNativeText(reset); this._requestLayoutOnTextChanged(); } [formattedTextProperty.setNative](value) { this._setNativeText(); this._setTappableState(isStringTappable(value)); textProperty.nativeValueChange(this, !value ? '' : value.toString()); this._requestLayoutOnTextChanged(); } [colorProperty.getDefault]() { const nativeView = this.nativeTextViewProtected; if (nativeView instanceof UIButton) { return nativeView.titleColorForState(0 /* UIControlState.Normal */); } else { return nativeView.textColor; } } [colorProperty.setNative](value) { const color = value instanceof Color ? value.ios : value; this._setColor(color); } [fontInternalProperty.getDefault]() { let nativeView = this.nativeTextViewProtected; nativeView = nativeView instanceof UIButton ? nativeView.titleLabel : nativeView; return nativeView.font; } [fontInternalProperty.setNative](value) { if (!(value instanceof Font) || !this.formattedText) { let nativeView = this.nativeTextViewProtected; nativeView = nativeView instanceof UIButton ? nativeView.titleLabel : nativeView; nativeView.font = value instanceof Font ? value.getUIFont(nativeView.font) : value; } } [fontScaleInternalProperty.setNative](value) { const nativeView = this.nativeTextViewProtected instanceof UIButton ? this.nativeTextViewProtected.titleLabel : this.nativeTextViewProtected; const font = this.style.fontInternal || Font.default.withFontSize(nativeView.font.pointSize); const finalValue = adjustMinMaxFontScale(value, this); // Request layout on font scale as it's not done automatically if (font.fontScale !== finalValue) { this.style.fontInternal = font.withFontScale(finalValue); this.requestLayout(); } else { if (!this.style.fontInternal) { this.style.fontInternal = font; } } } [iosAccessibilityAdjustsFontSizeProperty.setNative](value) { this[fontScaleInternalProperty.setNative](this.style.fontScaleInternal); } [iosAccessibilityMinFontScaleProperty.setNative](value) { this[fontScaleInternalProperty.setNative](this.style.fontScaleInternal); } [iosAccessibilityMaxFontScaleProperty.setNative](value) { this[fontScaleInternalProperty.setNative](this.style.fontScaleInternal); } [textAlignmentProperty.setNative](value) { const nativeView = this.nativeTextViewProtected; switch (value) { case 'initial': case 'left': nativeView.textAlignment = 0 /* NSTextAlignment.Left */; break; case 'center': nativeView.textAlignment = 1 /* NSTextAlignment.Center */; break; case 'right': nativeView.textAlignment = 2 /* NSTextAlignment.Right */; break; case 'justify': nativeView.textAlignment = 3 /* NSTextAlignment.Justified */; break; } } [textDecorationProperty.setNative](value) { this._setNativeText(); } [textTransformProperty.setNative](value) { this._setNativeText(); } [textStrokeProperty.setNative](value) { this._setNativeText(); } [letterSpacingProperty.setNative](value) { this._setNativeText(); } [lineHeightProperty.setNative](value) { this._setNativeText(); } [textShadowProperty.setNative](value) { this._setShadow(value); } [maxLinesProperty.setNative](value) { const nativeTextViewProtected = this.nativeTextViewProtected; const numberOfLines = this.whiteSpace !== CoreTypes.WhiteSpace.nowrap ? value : 1; if (nativeTextViewProtected instanceof UITextView) { nativeTextViewProtected.textContainer.maximumNumberOfLines = numberOfLines; if (value !== 0) { nativeTextViewProtected.textContainer.lineBreakMode = 4 /* NSLineBreakMode.ByTruncatingTail */; } else { nativeTextViewProtected.textContainer.lineBreakMode = 0 /* NSLineBreakMode.ByWordWrapping */; } } else if (nativeTextViewProtected instanceof UILabel) { nativeTextViewProtected.numberOfLines = numberOfLines; nativeTextViewProtected.lineBreakMode = 4 /* NSLineBreakMode.ByTruncatingTail */; } else if (nativeTextViewProtected instanceof UIButton) { nativeTextViewProtected.titleLabel.numberOfLines = numberOfLines; } } _setColor(color) { if (this.nativeTextViewProtected instanceof UIButton) { this.nativeTextViewProtected.setTitleColorForState(color, 0 /* UIControlState.Normal */); this.nativeTextViewProtected.titleLabel.textColor = color; } else { this.nativeTextViewProtected.textColor = color; } } _animationWrap(fn) { const shouldAnimate = this.iosTextAnimation === 'inherit' ? TextBase.iosTextAnimationFallback : this.iosTextAnimation; if (shouldAnimate) { fn(); } else { UIView.performWithoutAnimation(fn); } } _setNativeText(reset = false) { this._animationWrap(() => { if (reset) { const nativeView = this.nativeTextViewProtected; if (nativeView instanceof UIButton) { // Clear attributedText or title won't be affected. nativeView.setAttributedTitleForState(null, 0 /* UIControlState.Normal */); nativeView.setTitleForState(null, 0 /* UIControlState.Normal */); } else { // Clear attributedText or text won't be affected. nativeView.attributedText = null; nativeView.text = null; } return; } const letterSpacing = this.style.letterSpacing ? this.style.letterSpacing : 0; const lineHeight = this.style.lineHeight ? this.style.lineHeight : 0; if (this.formattedText) { this.nativeTextViewProtected.nativeScriptSetFormattedTextDecorationAndTransformLetterSpacingLineHeight(this.getFormattedStringDetails(this.formattedText), letterSpacing, lineHeight); } else { // console.log('setTextDecorationAndTransform...') const text = getTransformedText(isNullOrUndefined(this.text) ? '' : `${this.text}`, this.textTransform); this.nativeTextViewProtected.nativeScriptSetTextDecorationAndTransformTextDecorationLetterSpacingLineHeight(text, this.style.textDecoration || '', letterSpacing, lineHeight); if (!this.style?.color && majorVersion >= 13 && UIColor.labelColor) { this._setColor(UIColor.labelColor); } } if (this.style?.textStroke) { this.nativeTextViewProtected.nativeScriptSetFormattedTextStrokeColor(Length.toDevicePixels(this.style.textStroke.width, 0), this.style.textStroke.color.ios); } }); } createFormattedTextNative(value) { return NativeScriptUtils.createMutableStringWithDetails(this.getFormattedStringDetails(value)); } getFormattedStringDetails(formattedString) { const details = { spans: [], }; 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); const text = span.text; const textTransform = formattedString.parent.textTransform; let spanText = isNullOrUndefined(text) ? '' : `${text}`; if (textTransform !== 'none' && textTransform !== 'initial') { spanText = getTransformedText(spanText, textTransform); } details.spans.push(this.createMutableStringDetails(span, spanText, spanStart)); this._spanRanges.push({ location: spanStart, length: spanText.length, }); spanStart += spanText.length; } } return details; } createMutableStringDetails(span, text, index) { const fontScale = adjustMinMaxFontScale(span.style.fontScaleInternal, span); const font = new Font(span.style.fontFamily, span.style.fontSize, span.style.fontStyle, span.style.fontWeight, fontScale, span.style.fontVariationSettings); const iosFont = font.getUIFont(this.nativeTextViewProtected.font); // Use span or formatted string color const backgroundColor = span.style.backgroundColor || span.parent.backgroundColor; return { text, iosFont, color: span.color ? span.color.ios : null, backgroundColor: backgroundColor ? backgroundColor.ios : null, textDecoration: getClosestPropertyValue(textDecorationProperty, span), baselineOffset: this.getBaselineOffset(iosFont, span.style.verticalAlignment), index, }; } createMutableStringForSpan(span, text) { const details = this.createMutableStringDetails(span, text); return NativeScriptUtils.createMutableStringForSpanFontColorBackgroundColorTextDecorationBaselineOffset(details.text, details.iosFont, details.color, details.backgroundColor, details.textDecoration, details.baselineOffset); } getBaselineOffset(font, align) { if (!align || ['stretch', 'baseline'].includes(align)) { return 0; } if (align === 'top') { return -this.fontSize - font.descender - font.ascender - font.leading / 2; } if (align === 'bottom') { return font.descender + font.leading / 2; } if (align === 'text-top') { return -this.fontSize - font.descender - font.ascender; } if (align === 'text-bottom') { return font.descender; } if (align === 'middle') { return (font.descender - font.ascender) / 2 - font.descender; } if (align === 'sup') { return -this.fontSize * 0.4; } if (align === 'sub') { return (font.descender - font.ascender) * 0.4; } } _setShadow(value) { const layer = this.nativeTextViewProtected.layer; if (isNullOrUndefined(value)) { // clear the text shadow layer.shadowOpacity = 0; layer.shadowRadius = 0; layer.shadowColor = UIColor.clearColor; layer.shadowOffset = CGSizeMake(0, 0); return; } // shadow opacity is handled on the shadow's color instance layer.shadowOpacity = value.color?.a ? value.color.a / 255 : 1; layer.shadowColor = value.color.ios.CGColor; layer.shadowRadius = layout.toDeviceIndependentPixels(Length.toDevicePixels(value.blurRadius, 0)); // prettier-ignore layer.shadowOffset = CGSizeMake(layout.toDeviceIndependentPixels(Length.toDevicePixels(value.offsetX, 0)), layout.toDeviceIndependentPixels(Length.toDevicePixels(value.offsetY, 0))); layer.masksToBounds = false; // NOTE: generally should not need shouldRasterize // however for various detailed animation work which involves text-shadow applicable layers, we may want to give users the control of enabling this with text-shadow // if (!(this.nativeTextViewProtected instanceof UITextView)) { // layer.shouldRasterize = true; // } } } export function getTransformedText(text, textTransform) { if (!text || !isString(text)) { return ''; } switch (textTransform) { case 'uppercase': return NSStringFromNSAttributedString(text).uppercaseString; case 'lowercase': return NSStringFromNSAttributedString(text).lowercaseString; case 'capitalize': return NSStringFromNSAttributedString(text).capitalizedString; default: return text; } } function NSStringFromNSAttributedString(source) { return NSString.stringWithString((source instanceof NSAttributedString && source.string) || source); } function isStringTappable(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 adjustMinMaxFontScale(value, view) { let finalValue; if (view.iosAccessibilityAdjustsFontSize) { finalValue = value; if (view.iosAccessibilityMinFontScale && view.iosAccessibilityMinFontScale > value) { finalValue = view.iosAccessibilityMinFontScale; } if (view.iosAccessibilityMaxFontScale && view.iosAccessibilityMaxFontScale < value) { finalValue = view.iosAccessibilityMaxFontScale; } } else { finalValue = 1.0; } return finalValue; } //# sourceMappingURL=index.ios.js.map