mirror of
				https://github.com/NativeScript/NativeScript.git
				synced 2025-11-04 12:58:38 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			360 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			360 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { TextFieldBase, secureProperty } from './text-field-common';
 | 
						|
import { textOverflowProperty, textProperty, whiteSpaceProperty } from '../text-base';
 | 
						|
import { hintProperty, placeholderColorProperty, _updateCharactersInRangeReplacementString } from '../editable-text-base';
 | 
						|
import { CoreTypes } from '../../core-types';
 | 
						|
import { Color } from '../../color';
 | 
						|
import { colorProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty, paddingLeftProperty } from '../styling/style-properties';
 | 
						|
import { layout, isEmoji } from '../../utils';
 | 
						|
import { profile } from '../../profiling';
 | 
						|
 | 
						|
export * from './text-field-common';
 | 
						|
 | 
						|
@NativeClass
 | 
						|
class UITextFieldDelegateImpl extends NSObject implements UITextFieldDelegate {
 | 
						|
	public static ObjCProtocols = [UITextFieldDelegate];
 | 
						|
 | 
						|
	private _owner: WeakRef<TextField>;
 | 
						|
	private firstEdit: boolean;
 | 
						|
 | 
						|
	public static initWithOwner(owner: WeakRef<TextField>): UITextFieldDelegateImpl {
 | 
						|
		const delegate = <UITextFieldDelegateImpl>UITextFieldDelegateImpl.new();
 | 
						|
		delegate._owner = owner;
 | 
						|
 | 
						|
		return delegate;
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldShouldBeginEditing(textField: UITextField): boolean {
 | 
						|
		const owner = this._owner?.deref();
 | 
						|
		if (owner) {
 | 
						|
			return owner.textFieldShouldBeginEditing(textField);
 | 
						|
		}
 | 
						|
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldDidBeginEditing(textField: UITextField): void {
 | 
						|
		const owner = this._owner?.deref();
 | 
						|
		if (owner) {
 | 
						|
			owner.textFieldDidBeginEditing(textField);
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldDidEndEditing(textField: UITextField) {
 | 
						|
		const owner = this._owner?.deref();
 | 
						|
		if (owner) {
 | 
						|
			owner.textFieldDidEndEditing(textField);
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldShouldClear(textField: UITextField) {
 | 
						|
		const owner = this._owner?.deref();
 | 
						|
		if (owner) {
 | 
						|
			return owner.textFieldShouldClear(textField);
 | 
						|
		}
 | 
						|
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldShouldReturn(textField: UITextField): boolean {
 | 
						|
		// Called when the user presses the return button.
 | 
						|
		const owner = this._owner?.deref();
 | 
						|
		if (owner) {
 | 
						|
			return owner.textFieldShouldReturn(textField);
 | 
						|
		}
 | 
						|
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldShouldChangeCharactersInRangeReplacementString(textField: UITextField, range: NSRange, replacementString: string): boolean {
 | 
						|
		const owner = this._owner?.deref();
 | 
						|
		if (owner) {
 | 
						|
			return owner.textFieldShouldChangeCharactersInRangeReplacementString(textField, range, replacementString);
 | 
						|
		}
 | 
						|
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
@NativeClass
 | 
						|
class UITextFieldImpl extends UITextField {
 | 
						|
	private _owner: WeakRef<TextField>;
 | 
						|
 | 
						|
	public static initWithOwner(owner: WeakRef<TextField>): UITextFieldImpl {
 | 
						|
		const handler = <UITextFieldImpl>UITextFieldImpl.new();
 | 
						|
		handler._owner = owner;
 | 
						|
 | 
						|
		return handler;
 | 
						|
	}
 | 
						|
 | 
						|
	private _getTextRectForBounds(bounds: CGRect): CGRect {
 | 
						|
		const owner = this._owner ? this._owner.deref() : null;
 | 
						|
 | 
						|
		if (!owner) {
 | 
						|
			return bounds;
 | 
						|
		}
 | 
						|
 | 
						|
		const size = bounds.size;
 | 
						|
 | 
						|
		const x = layout.toDeviceIndependentPixels(owner.effectiveBorderLeftWidth + owner.effectivePaddingLeft);
 | 
						|
		const y = layout.toDeviceIndependentPixels(owner.effectiveBorderTopWidth + owner.effectivePaddingTop);
 | 
						|
		const width = layout.toDeviceIndependentPixels(layout.toDevicePixels(size.width) - (owner.effectiveBorderLeftWidth + owner.effectivePaddingLeft + owner.effectivePaddingRight + owner.effectiveBorderRightWidth));
 | 
						|
		const height = layout.toDeviceIndependentPixels(layout.toDevicePixels(size.height) - (owner.effectiveBorderTopWidth + owner.effectivePaddingTop + owner.effectivePaddingBottom + owner.effectiveBorderBottomWidth));
 | 
						|
 | 
						|
		return CGRectMake(x, y, width, height);
 | 
						|
	}
 | 
						|
 | 
						|
	public textRectForBounds(bounds: CGRect): CGRect {
 | 
						|
		return this._getTextRectForBounds(bounds);
 | 
						|
	}
 | 
						|
 | 
						|
	public editingRectForBounds(bounds: CGRect): CGRect {
 | 
						|
		return this._getTextRectForBounds(bounds);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export class TextField extends TextFieldBase {
 | 
						|
	nativeViewProtected: UITextField;
 | 
						|
 | 
						|
	private _delegate: UITextFieldDelegateImpl;
 | 
						|
	private _firstEdit: boolean;
 | 
						|
 | 
						|
	createNativeView() {
 | 
						|
		return UITextFieldImpl.initWithOwner(new WeakRef(this));
 | 
						|
	}
 | 
						|
 | 
						|
	initNativeView() {
 | 
						|
		super.initNativeView();
 | 
						|
		this._delegate = UITextFieldDelegateImpl.initWithOwner(new WeakRef(this));
 | 
						|
		this.nativeViewProtected.delegate = this._delegate;
 | 
						|
	}
 | 
						|
 | 
						|
	disposeNativeView() {
 | 
						|
		this._delegate = null;
 | 
						|
		this._firstEdit = false;
 | 
						|
		super.disposeNativeView();
 | 
						|
	}
 | 
						|
 | 
						|
	// @ts-ignore
 | 
						|
	get ios(): UITextField {
 | 
						|
		return this.nativeViewProtected;
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldShouldBeginEditing(textField: UITextField): boolean {
 | 
						|
		this._firstEdit = true;
 | 
						|
 | 
						|
		return this.editable;
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldDidBeginEditing(textField: UITextField): void {
 | 
						|
		this.notify({ eventName: TextField.focusEvent, object: this });
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldDidEndEditing(textField: UITextField) {
 | 
						|
		if (this.updateTextTrigger === 'focusLost') {
 | 
						|
			textProperty.nativeValueChange(this, textField.text);
 | 
						|
		}
 | 
						|
 | 
						|
		this.dismissSoftInput();
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldShouldClear(textField: UITextField) {
 | 
						|
		this._firstEdit = false;
 | 
						|
		textProperty.nativeValueChange(this, '');
 | 
						|
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldShouldReturn(textField: UITextField): boolean {
 | 
						|
		// Called when the user presses the return button.
 | 
						|
		if (this.closeOnReturn) {
 | 
						|
			this.dismissSoftInput();
 | 
						|
		}
 | 
						|
		this.notify({ eventName: TextField.returnPressEvent, object: this });
 | 
						|
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
 | 
						|
	public textFieldShouldChangeCharactersInRangeReplacementString(textField: UITextField, range: NSRange, replacementString: string): boolean {
 | 
						|
		if (this.secureWithoutAutofill && !textField.secureTextEntry) {
 | 
						|
			/**
 | 
						|
			 * Helps avoid iOS 12+ autofill strong password suggestion prompt
 | 
						|
			 * Discussed in several circles but for example:
 | 
						|
			 * https://github.com/expo/expo/issues/2571#issuecomment-473347380
 | 
						|
			 */
 | 
						|
			textField.secureTextEntry = true;
 | 
						|
		}
 | 
						|
		const delta = replacementString.length - range.length;
 | 
						|
		if (delta > 0) {
 | 
						|
			if (textField.text.length + delta > this.maxLength) {
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if (this.updateTextTrigger === 'textChanged') {
 | 
						|
			if (this.valueFormatter) {
 | 
						|
				// format/replace
 | 
						|
				const currentValue = textField.text;
 | 
						|
				let nativeValueChange = `${textField.text}${replacementString}`;
 | 
						|
				if (replacementString === '') {
 | 
						|
					// clearing when empty
 | 
						|
					nativeValueChange = currentValue.slice(0, delta);
 | 
						|
				}
 | 
						|
 | 
						|
				const formattedValue = this.valueFormatter(nativeValueChange);
 | 
						|
				textField.text = formattedValue;
 | 
						|
				textProperty.nativeValueChange(this, formattedValue);
 | 
						|
				return false;
 | 
						|
			} else {
 | 
						|
				// 1. secureTextEntry with firstEdit should not replace
 | 
						|
				// 2. emoji's should not replace value
 | 
						|
				// 3. convenient keyboard shortcuts should not replace value (eg, '.com')
 | 
						|
				const shouldReplaceString = (textField.secureTextEntry && this._firstEdit) || (delta > 1 && !isEmoji(replacementString) && delta !== replacementString.length);
 | 
						|
				if (shouldReplaceString) {
 | 
						|
					textProperty.nativeValueChange(this, replacementString);
 | 
						|
				} else {
 | 
						|
					if (range.location <= textField.text.length) {
 | 
						|
						const newText = NSString.stringWithString(textField.text).stringByReplacingCharactersInRangeWithString(range, replacementString);
 | 
						|
						textProperty.nativeValueChange(this, newText);
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if (this.formattedText) {
 | 
						|
			_updateCharactersInRangeReplacementString(this.formattedText, range.location, range.length, replacementString);
 | 
						|
		}
 | 
						|
		if (this.width === 'auto') {
 | 
						|
			// if the textfield is in auto size we need to request a layout to take the new text width into account
 | 
						|
			this.requestLayout();
 | 
						|
		}
 | 
						|
		this._firstEdit = false;
 | 
						|
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
 | 
						|
	[hintProperty.getDefault](): string {
 | 
						|
		return this.nativeTextViewProtected.placeholder;
 | 
						|
	}
 | 
						|
	[hintProperty.setNative](value: string) {
 | 
						|
		this._updateAttributedPlaceholder();
 | 
						|
	}
 | 
						|
 | 
						|
	[secureProperty.getDefault](): boolean {
 | 
						|
		return this.nativeTextViewProtected.secureTextEntry;
 | 
						|
	}
 | 
						|
	[secureProperty.setNative](value: boolean) {
 | 
						|
		this.nativeTextViewProtected.secureTextEntry = value;
 | 
						|
	}
 | 
						|
 | 
						|
	[colorProperty.getDefault](): { textColor: UIColor; tintColor: UIColor } {
 | 
						|
		return {
 | 
						|
			textColor: this.nativeTextViewProtected.textColor,
 | 
						|
			tintColor: this.nativeTextViewProtected.tintColor,
 | 
						|
		};
 | 
						|
	}
 | 
						|
	[colorProperty.setNative](value: Color | { textColor: UIColor; tintColor: UIColor }) {
 | 
						|
		if (value instanceof Color) {
 | 
						|
			const color = value instanceof Color ? value.ios : value;
 | 
						|
			this.nativeTextViewProtected.textColor = color;
 | 
						|
			this.nativeTextViewProtected.tintColor = color;
 | 
						|
		} else {
 | 
						|
			this.nativeTextViewProtected.textColor = value.textColor;
 | 
						|
			this.nativeTextViewProtected.tintColor = value.tintColor;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	[placeholderColorProperty.getDefault](): UIColor {
 | 
						|
		return null;
 | 
						|
	}
 | 
						|
	[placeholderColorProperty.setNative](value: UIColor | Color) {
 | 
						|
		this._updateAttributedPlaceholder();
 | 
						|
	}
 | 
						|
 | 
						|
	_updateAttributedPlaceholder(): void {
 | 
						|
		let stringValue = this.hint;
 | 
						|
		if (stringValue === null || stringValue === void 0) {
 | 
						|
			stringValue = '';
 | 
						|
		} else {
 | 
						|
			stringValue = stringValue + '';
 | 
						|
		}
 | 
						|
		if (stringValue === '') {
 | 
						|
			// we do not use empty string since initWithStringAttributes does not return proper value and
 | 
						|
			// nativeView.attributedPlaceholder will be null
 | 
						|
			stringValue = ' ';
 | 
						|
		}
 | 
						|
		const attributes: any = {};
 | 
						|
		if (this.style.placeholderColor) {
 | 
						|
			attributes[NSForegroundColorAttributeName] = this.style.placeholderColor.ios;
 | 
						|
		}
 | 
						|
		const attributedPlaceholder = NSAttributedString.alloc().initWithStringAttributes(stringValue, attributes);
 | 
						|
		this.nativeTextViewProtected.attributedPlaceholder = attributedPlaceholder;
 | 
						|
	}
 | 
						|
 | 
						|
	[paddingTopProperty.getDefault](): CoreTypes.LengthType {
 | 
						|
		return CoreTypes.zeroLength;
 | 
						|
	}
 | 
						|
	[paddingTopProperty.setNative](value: CoreTypes.LengthType) {
 | 
						|
		// Padding is realized via UITextFieldImpl.textRectForBounds method
 | 
						|
	}
 | 
						|
 | 
						|
	[paddingRightProperty.getDefault](): CoreTypes.LengthType {
 | 
						|
		return CoreTypes.zeroLength;
 | 
						|
	}
 | 
						|
	[paddingRightProperty.setNative](value: CoreTypes.LengthType) {
 | 
						|
		// Padding is realized via UITextFieldImpl.textRectForBounds method
 | 
						|
	}
 | 
						|
 | 
						|
	[paddingBottomProperty.getDefault](): CoreTypes.LengthType {
 | 
						|
		return CoreTypes.zeroLength;
 | 
						|
	}
 | 
						|
	[paddingBottomProperty.setNative](value: CoreTypes.LengthType) {
 | 
						|
		// Padding is realized via UITextFieldImpl.textRectForBounds method
 | 
						|
	}
 | 
						|
 | 
						|
	[paddingLeftProperty.getDefault](): CoreTypes.LengthType {
 | 
						|
		return CoreTypes.zeroLength;
 | 
						|
	}
 | 
						|
	[paddingLeftProperty.setNative](value: CoreTypes.LengthType) {
 | 
						|
		// Padding is realized via UITextFieldImpl.textRectForBounds method
 | 
						|
	}
 | 
						|
 | 
						|
	[whiteSpaceProperty.setNative](value: CoreTypes.WhiteSpaceType) {
 | 
						|
		this.adjustLineBreak();
 | 
						|
	}
 | 
						|
 | 
						|
	[textOverflowProperty.setNative](value: CoreTypes.TextOverflowType) {
 | 
						|
		this.adjustLineBreak();
 | 
						|
	}
 | 
						|
 | 
						|
	private adjustLineBreak() {
 | 
						|
		let paragraphStyle: NSMutableParagraphStyle;
 | 
						|
 | 
						|
		switch (this.whiteSpace) {
 | 
						|
			case 'nowrap':
 | 
						|
				switch (this.textOverflow) {
 | 
						|
					case 'clip':
 | 
						|
						paragraphStyle = NSMutableParagraphStyle.new();
 | 
						|
						paragraphStyle.lineBreakMode = NSLineBreakMode.ByClipping;
 | 
						|
						break;
 | 
						|
					default:
 | 
						|
						// ellipsis
 | 
						|
						paragraphStyle = NSMutableParagraphStyle.new();
 | 
						|
						paragraphStyle.lineBreakMode = NSLineBreakMode.ByTruncatingTail;
 | 
						|
						break;
 | 
						|
				}
 | 
						|
				break;
 | 
						|
			case 'wrap':
 | 
						|
				paragraphStyle = NSMutableParagraphStyle.new();
 | 
						|
				paragraphStyle.lineBreakMode = NSLineBreakMode.ByWordWrapping;
 | 
						|
				break;
 | 
						|
		}
 | 
						|
 | 
						|
		if (paragraphStyle) {
 | 
						|
			const attributedString = NSMutableAttributedString.alloc().initWithString(this.nativeViewProtected.text || '');
 | 
						|
			attributedString.addAttributeValueRange(NSParagraphStyleAttributeName, paragraphStyle, NSRangeFromString(`{0,${attributedString.length}}`));
 | 
						|
 | 
						|
			this.nativeViewProtected.attributedText = attributedString;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |