Files
2022-11-12 10:09:45 -08:00

535 lines
16 KiB
TypeScript

import { EditableTextBase as EditableTextBaseCommon, autofillTypeProperty, keyboardTypeProperty, returnKeyTypeProperty, editableProperty, autocapitalizationTypeProperty, autocorrectProperty, hintProperty, placeholderColorProperty, maxLengthProperty } from './editable-text-base-common';
import { textTransformProperty, textProperty, resetSymbol } from '../text-base';
import { Color } from '../../color';
import { SDK_VERSION, ad } from '../../utils';
import { CoreTypes } from '../../core-types';
import { Device } from '../../platform';
import lazy from '../../utils/lazy';
export * from './editable-text-base-common';
//https://github.com/NativeScript/NativeScript/issues/2942
export let dismissKeyboardTimeoutId: NodeJS.Timer;
interface EditTextListeners extends android.text.TextWatcher, android.view.View.OnFocusChangeListener, android.widget.TextView.OnEditorActionListener {}
interface EditTextListenersClass {
prototype: EditTextListeners;
new (owner: WeakRef<EditableTextBase>): EditTextListeners;
}
let EditTextListeners: EditTextListenersClass;
function clearDismissTimer(): void {
if (dismissKeyboardTimeoutId) {
clearTimeout(dismissKeyboardTimeoutId);
dismissKeyboardTimeoutId = null;
}
}
function dismissSoftInput(_owner: WeakRef<EditableTextBase>): void {
clearDismissTimer();
if (!dismissKeyboardTimeoutId) {
dismissKeyboardTimeoutId = setTimeout(() => {
const owner = _owner && _owner.get();
const activity = owner?._context as androidx.appcompat.app.AppCompatActivity;
dismissKeyboardTimeoutId = null;
const focused = activity && activity.getCurrentFocus();
if (focused && !(focused instanceof android.widget.EditText)) {
// warning `ad.dismissSoftInput` will actually focus the next view
// if we pass a null parameter!!!
// => focus and show keyboard again
// the fix was for where there were multiple TextField for which it would work!
// with this it will still work without breaking for single TextField
ad.dismissSoftInput(focused);
}
}, 10);
}
}
function initializeEditTextListeners(): void {
if (EditTextListeners) {
return;
}
@NativeClass
@Interfaces([android.text.TextWatcher, android.view.View.OnFocusChangeListener, android.widget.TextView.OnEditorActionListener])
class EditTextListenersImpl extends java.lang.Object implements android.text.TextWatcher, android.view.View.OnFocusChangeListener, android.widget.TextView.OnEditorActionListener {
constructor(private owner: WeakRef<EditableTextBase>) {
super();
return global.__native(this);
}
public beforeTextChanged(text: string, start: number, count: number, after: number): void {
//
}
public onTextChanged(text: string, start: number, before: number, count: number): void {
// const owner = this.owner;
// let selectionStart = owner.android.getSelectionStart();
// owner.android.removeTextChangedListener(owner._editTextListeners);
// owner.android.addTextChangedListener(owner._editTextListeners);
// owner.android.setSelection(selectionStart);
}
public afterTextChanged(editable: android.text.Editable): void {
const owner = this.owner && this.owner.get();
if (!owner || owner._changeFromCode) {
return;
}
switch (owner.updateTextTrigger) {
case 'focusLost':
owner._dirtyTextAccumulator = editable.toString();
break;
case 'textChanged':
textProperty.nativeValueChange(owner, editable.toString());
break;
default:
throw new Error('Invalid updateTextTrigger: ' + owner.updateTextTrigger);
}
}
public onFocusChange(view: android.view.View, hasFocus: boolean): void {
const owner = this.owner && this.owner.get();
if (!owner) {
return;
}
if (hasFocus) {
clearDismissTimer();
owner.notify({
eventName: EditableTextBase.focusEvent,
object: owner,
});
} else {
if (owner._dirtyTextAccumulator || owner._dirtyTextAccumulator === '') {
textProperty.nativeValueChange(owner, owner._dirtyTextAccumulator);
owner._dirtyTextAccumulator = undefined;
}
owner.notify({
eventName: EditableTextBase.blurEvent,
object: owner,
});
dismissSoftInput(this.owner);
}
}
public onEditorAction(textView: android.widget.TextView, actionId: number, event: android.view.KeyEvent): boolean {
const owner = this.owner && this.owner.get();
if (!owner) {
return false;
}
if (actionId === android.view.inputmethod.EditorInfo.IME_ACTION_DONE || actionId === android.view.inputmethod.EditorInfo.IME_ACTION_UNSPECIFIED || (event && event.getKeyCode() === android.view.KeyEvent.KEYCODE_ENTER)) {
// If it is TextField, close the keyboard. If it is TextView, do not close it since the TextView is multiline
// https://github.com/NativeScript/NativeScript/issues/3111
if (textView.getMaxLines() === 1) {
owner.dismissSoftInput();
}
owner._onReturnPress();
} else if (actionId === android.view.inputmethod.EditorInfo.IME_ACTION_NEXT || actionId === android.view.inputmethod.EditorInfo.IME_ACTION_PREVIOUS) {
// do not close keyboard for ACTION_NEXT or ACTION_PREVIOUS
owner._onReturnPress();
}
return false;
}
}
EditTextListeners = EditTextListenersImpl;
}
export abstract class EditableTextBase extends EditableTextBaseCommon {
/* tslint:disable */
_dirtyTextAccumulator: string;
/* tslint:enable */
nativeViewProtected: android.widget.EditText;
nativeTextViewProtected: android.widget.EditText;
private _keyListenerCache: android.text.method.KeyListener;
private _inputType: number;
public _changeFromCode: boolean;
public abstract _configureEditText(editText: android.widget.EditText): void;
public _onReturnPress(): void {
//
}
public createNativeView() {
return new android.widget.EditText(this._context);
}
public initNativeView(): void {
super.initNativeView();
const editText = this.nativeTextViewProtected;
this._configureEditText(editText);
initializeEditTextListeners();
const listeners = new EditTextListeners(new WeakRef(this));
editText.addTextChangedListener(listeners);
editText.setOnFocusChangeListener(listeners);
editText.setOnEditorActionListener(listeners);
(<any>editText).listener = listeners;
this._inputType = editText.getInputType();
}
public disposeNativeView(): void {
(<any>this.nativeTextViewProtected).listener.owner = null;
this._keyListenerCache = null;
super.disposeNativeView();
}
public resetNativeView(): void {
super.resetNativeView();
this.nativeTextViewProtected.setInputType(this._inputType);
}
public onUnloaded() {
this.dismissSoftInput();
super.onUnloaded();
}
public dismissSoftInput() {
const nativeView = this.nativeTextViewProtected;
if (!nativeView) {
return;
}
ad.dismissSoftInput(nativeView);
}
public focus(): boolean {
const nativeView = this.nativeTextViewProtected;
if (!nativeView) {
return;
}
const result = super.focus();
if (result) {
ad.showSoftInput(this.nativeTextViewProtected);
}
return result;
}
public _setInputType(inputType: number): void {
const nativeView = this.nativeTextViewProtected;
try {
this._changeFromCode = true;
nativeView.setInputType(parseInt(<any>inputType, 10));
} finally {
this._changeFromCode = false;
}
// setInputType will change the keyListener so we should cache it again
const listener = nativeView.getKeyListener();
if (listener) {
this._keyListenerCache = listener;
}
// clear these fields instead of clearing listener.
// this allows input Type to be changed even after editable is false.
if (!this.editable) {
nativeView.setFocusable(false);
nativeView.setFocusableInTouchMode(false);
nativeView.setLongClickable(false);
nativeView.setClickable(false);
}
}
[textProperty.getDefault](): number | symbol {
return resetSymbol;
}
[textProperty.setNative](value: string | number | symbol) {
try {
this._changeFromCode = true;
this._setNativeText(value === resetSymbol);
} finally {
this._changeFromCode = false;
}
}
[keyboardTypeProperty.getDefault](): number {
return this.nativeTextViewProtected.getInputType();
}
[keyboardTypeProperty.setNative](value: 'datetime' | 'phone' | 'number' | 'url' | 'email' | 'integer' | number) {
let newInputType;
switch (value) {
case 'datetime':
newInputType = android.text.InputType.TYPE_CLASS_DATETIME | android.text.InputType.TYPE_DATETIME_VARIATION_NORMAL;
break;
case 'phone':
newInputType = android.text.InputType.TYPE_CLASS_PHONE;
break;
case 'number':
newInputType = android.text.InputType.TYPE_CLASS_NUMBER | android.text.InputType.TYPE_NUMBER_VARIATION_NORMAL | android.text.InputType.TYPE_NUMBER_FLAG_SIGNED | android.text.InputType.TYPE_NUMBER_FLAG_DECIMAL;
break;
case 'url':
newInputType = android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_URI;
break;
case 'email':
newInputType = android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
break;
case 'integer':
newInputType = android.text.InputType.TYPE_CLASS_NUMBER;
break;
default:
const inputType = +value;
if (!isNaN(inputType)) {
newInputType = inputType;
} else {
newInputType = android.text.InputType.TYPE_CLASS_TEXT;
}
break;
}
this._setInputType(newInputType);
}
[autofillTypeProperty.setNative](value: CoreTypes.AutofillType) {
if (SDK_VERSION < 26) {
return;
}
let newOptions;
switch (value) {
case 'phone':
newOptions = 'phone'; // android.view.View.AUTOFILL_HINT_PHONE
break;
case 'postalCode':
newOptions = 'postalCode'; // android.view.View.AUTOFILL_HINT_POSTAL_CODE
break;
case 'creditCardNumber':
newOptions = 'creditCardNumber'; // android.view.View.AUTOFILL_HINT_CREDIT_CARD_NUMBER
break;
case 'email':
newOptions = 'emailAddress'; // android.view.View.AUTOFILL_HINT_EMAIL_ADDRESS
break;
case 'name':
newOptions = 'name'; // android.view.View.AUTOFILL_HINT_NAME
break;
case 'username':
newOptions = 'username'; // android.view.View.AUTOFILL_HINT_USERNAME
break;
case 'password':
newOptions = 'password'; // android.view.View.AUTOFILL_HINT_PASSWORD
break;
case 'none':
newOptions = null;
break;
default: {
newOptions = value;
break;
}
}
if (newOptions) {
const array = Array.create(java.lang.String, 1);
array[0] = newOptions;
this.nativeTextViewProtected.setAutofillHints(array);
} else {
this.nativeTextViewProtected.setAutofillHints(null);
}
}
[returnKeyTypeProperty.getDefault](): 'done' | 'next' | 'go' | 'search' | 'send' | string {
const ime = this.nativeTextViewProtected.getImeOptions();
switch (ime) {
case android.view.inputmethod.EditorInfo.IME_ACTION_DONE:
return 'done';
case android.view.inputmethod.EditorInfo.IME_ACTION_GO:
return 'go';
case android.view.inputmethod.EditorInfo.IME_ACTION_NEXT:
return 'next';
case android.view.inputmethod.EditorInfo.IME_ACTION_SEARCH:
return 'search';
case android.view.inputmethod.EditorInfo.IME_ACTION_SEND:
return 'send';
default:
return ime.toString();
}
}
[returnKeyTypeProperty.setNative](value: 'done' | 'next' | 'go' | 'search' | 'send' | string) {
let newImeOptions;
switch (value) {
case 'done':
newImeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
break;
case 'go':
newImeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_GO;
break;
case 'next':
newImeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_NEXT;
break;
case 'search':
newImeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_SEARCH;
break;
case 'send':
newImeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_SEND;
break;
default: {
const ime = +value;
if (!isNaN(ime)) {
newImeOptions = ime;
} else {
newImeOptions = android.view.inputmethod.EditorInfo.IME_ACTION_UNSPECIFIED;
}
break;
}
}
this.nativeTextViewProtected.setImeOptions(newImeOptions);
}
[editableProperty.setNative](value: boolean) {
const nativeView = this.nativeTextViewProtected;
if (value) {
nativeView.setKeyListener(this._keyListenerCache);
} else {
if (!this._keyListenerCache) {
this._keyListenerCache = nativeView.getKeyListener();
}
nativeView.setKeyListener(null);
}
}
[autocapitalizationTypeProperty.getDefault](): 'none' | 'words' | 'sentences' | 'allcharacters' | string {
const inputType = this.nativeTextViewProtected.getInputType();
if ((inputType & android.text.InputType.TYPE_TEXT_FLAG_CAP_WORDS) === android.text.InputType.TYPE_TEXT_FLAG_CAP_WORDS) {
return 'words';
} else if ((inputType & android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) === android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) {
return 'sentences';
} else if ((inputType & android.text.InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) === android.text.InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) {
return 'allcharacters';
} else {
return inputType.toString();
}
}
[autocapitalizationTypeProperty.setNative](value: string) {
let inputType = this.nativeTextViewProtected.getInputType();
inputType = inputType & ~28672; //28672 (0x00070000) 13,14,15bits (111 0000 0000 0000)
switch (value) {
case 'none':
//Do nothing, we have lowered the three bits above.
break;
case 'words':
inputType = inputType | android.text.InputType.TYPE_TEXT_FLAG_CAP_WORDS; //8192 (0x00020000) 14th bit
break;
case 'sentences':
inputType = inputType | android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; //16384(0x00040000) 15th bit
break;
case 'allcharacters':
inputType = inputType | android.text.InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; //4096 (0x00010000) 13th bit
break;
default: {
const number = +value;
// We set the default value.
if (!isNaN(number)) {
inputType = number;
} else {
inputType = inputType | android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
}
break;
}
}
this._setInputType(inputType);
}
[autocorrectProperty.getDefault](): boolean {
const autocorrect = this.nativeTextViewProtected.getInputType();
if ((autocorrect & android.text.InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) === android.text.InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) {
return true;
}
return false;
}
[autocorrectProperty.setNative](value: boolean) {
let inputType = this.nativeTextViewProtected.getInputType();
switch (value) {
case true:
inputType = inputType | android.text.InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
inputType = inputType | android.text.InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
inputType = inputType & ~android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
break;
case false:
inputType = inputType & ~android.text.InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
inputType = inputType & ~android.text.InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
inputType = inputType | android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
break;
default:
// We can't do anything.
break;
}
this._setInputType(inputType);
}
[hintProperty.getDefault](): string {
return this.nativeTextViewProtected.getHint();
}
[hintProperty.setNative](value: string) {
const text = value === null || value === undefined ? null : value.toString();
this.nativeTextViewProtected.setHint(text);
}
[placeholderColorProperty.getDefault](): android.content.res.ColorStateList {
return this.nativeTextViewProtected.getHintTextColors();
}
[placeholderColorProperty.setNative](value: Color | android.content.res.ColorStateList) {
const color = value instanceof Color ? value.android : value;
this.nativeTextViewProtected.setHintTextColor(<any>color);
}
[textTransformProperty.setNative](value: 'default') {
//
}
[maxLengthProperty.setNative](value: number) {
if (value === Number.POSITIVE_INFINITY) {
this.nativeTextViewProtected.setFilters([]);
} else {
const lengthFilter = new android.text.InputFilter.LengthFilter(value);
const filters = this.nativeTextViewProtected.getFilters();
const newFilters = [];
// retain existing filters
for (let i = 0; i < filters.length; i++) {
const filter = filters[i];
if (!(filter instanceof android.text.InputFilter.LengthFilter)) {
newFilters.push(filter);
}
}
newFilters.push(lengthFilter);
this.nativeTextViewProtected.setFilters(newFilters);
}
}
public setSelection(start: number, stop?: number) {
const view = this.nativeTextViewProtected;
if (view) {
if (stop !== undefined) {
view.setSelection(start, stop);
} else {
view.setSelection(start);
}
}
}
}