2021-04-08 17:52:31 +04:00
/ *
* https : //github.com/morethanwords/tweb
* Copyright ( C ) 2019 - 2021 Eduard Kuzmenko
* https : //github.com/morethanwords/tweb/blob/master/LICENSE
* /
2021-01-03 20:32:34 +04:00
import { getRichValue , isInputEmpty } from "../helpers/dom" ;
2021-02-01 04:31:34 +02:00
import { debounce } from "../helpers/schedulers" ;
2020-11-14 01:04:03 +02:00
import { checkRTL } from "../helpers/string" ;
2021-03-25 22:07:00 +04:00
import { i18n , LangPackKey , _i18n } from "../lib/langPack" ;
2020-11-14 01:04:03 +02:00
import RichTextProcessor from "../lib/richtextprocessor" ;
let init = ( ) = > {
document . addEventListener ( 'paste' , ( e ) = > {
if ( ! ( e . target as HTMLElement ) . hasAttribute ( 'contenteditable' ) && ! ( e . target as HTMLElement ) . parentElement . hasAttribute ( 'contenteditable' ) ) {
return ;
}
//console.log('document paste');
//console.log('messageInput paste');
e . preventDefault ( ) ;
// @ts-ignore
let text = ( e . originalEvent || e ) . clipboardData . getData ( 'text/plain' ) ;
let entities = RichTextProcessor . parseEntities ( text ) ;
//console.log('messageInput paste', text, entities);
2021-02-04 02:30:23 +02:00
entities = entities . filter ( e = > e . _ === 'messageEntityEmoji' || e . _ === 'messageEntityLinebreak' ) ;
2020-11-14 01:04:03 +02:00
//text = RichTextProcessor.wrapEmojiText(text);
2020-11-27 16:27:27 +02:00
text = RichTextProcessor . wrapRichText ( text , { entities , noLinks : true , wrappingDraft : true } ) ;
2020-11-14 01:04:03 +02:00
// console.log('messageInput paste after', text);
// @ts-ignore
//let html = (e.originalEvent || e).clipboardData.getData('text/html');
// @ts-ignore
//console.log('paste text', text, );
window . document . execCommand ( 'insertHTML' , false , text ) ;
} ) ;
init = null ;
} ;
2020-12-14 00:28:17 +02:00
const checkAndSetRTL = ( input : HTMLElement ) = > {
//const isEmpty = isInputEmpty(input);
//console.log('input', isEmpty);
//const char = [...getRichValue(input)][0];
const char = ( input instanceof HTMLInputElement ? input.value : input.innerText ) [ 0 ] ;
let direction = 'ltr' ;
if ( char && checkRTL ( char ) ) {
direction = 'rtl' ;
}
//console.log('RTL', direction, char);
input . style . direction = direction ;
} ;
2021-03-16 19:18:51 +04:00
export enum InputState {
Neutral = 0 ,
Valid = 1 ,
Error = 2
} ;
export type InputFieldOptions = {
2021-03-25 22:07:00 +04:00
placeholder? : LangPackKey ,
2021-03-21 13:59:59 +04:00
label? : LangPackKey ,
2021-03-26 21:49:29 +04:00
labelOptions? : any [ ] ,
2021-03-30 19:31:11 +04:00
labelText? : string ,
2021-03-16 19:18:51 +04:00
name? : string ,
maxLength? : number ,
showLengthOn? : number ,
plainText? : true ,
animate? : true
} ;
2020-12-18 05:07:32 +02:00
class InputField {
public container : HTMLElement ;
public input : HTMLElement ;
2021-01-18 22:34:41 +04:00
public inputFake : HTMLElement ;
2021-02-19 19:27:56 +04:00
public label : HTMLLabelElement ;
2020-12-18 05:07:32 +02:00
2021-03-12 22:08:39 +04:00
public originalValue : string ;
2020-12-18 18:43:17 +02:00
//public onLengthChange: (length: number, isOverflow: boolean) => void;
2021-02-19 19:27:56 +04:00
protected wasInputFakeClientHeight : number ;
protected showScrollDebounced : ( ) = > void ;
2020-12-18 18:43:17 +02:00
2021-03-16 19:18:51 +04:00
constructor ( public options : InputFieldOptions = { } ) {
2020-12-18 05:07:32 +02:00
this . container = document . createElement ( 'div' ) ;
this . container . classList . add ( 'input-field' ) ;
if ( options . maxLength ) {
options . showLengthOn = Math . round ( options . maxLength / 3 ) ;
}
2021-03-30 19:31:11 +04:00
const { placeholder , maxLength , showLengthOn , name , plainText } = options ;
let label = options . label || options . labelText ;
2020-11-14 01:04:03 +02:00
2020-12-18 05:07:32 +02:00
let input : HTMLElement ;
if ( ! plainText ) {
if ( init ) {
init ( ) ;
}
2020-11-14 01:04:03 +02:00
2020-12-18 05:07:32 +02:00
this . container . innerHTML = `
2021-03-26 19:29:10 +04:00
< div contenteditable = "true" class = "input-field-input" > < / div >
2020-12-18 05:07:32 +02:00
` ;
input = this . container . firstElementChild as HTMLElement ;
const observer = new MutationObserver ( ( ) = > {
checkAndSetRTL ( input ) ;
if ( processInput ) {
processInput ( ) ;
}
} ) ;
2021-01-03 19:59:13 +04:00
// * because if delete all characters there will br left
input . addEventListener ( 'input' , ( ) = > {
2021-01-03 20:32:34 +04:00
if ( isInputEmpty ( input ) ) {
2021-01-03 19:59:13 +04:00
input . innerHTML = '' ;
}
2021-01-18 22:34:41 +04:00
if ( this . inputFake ) {
this . inputFake . innerHTML = input . innerHTML ;
this . onFakeInput ( ) ;
}
2021-01-03 19:59:13 +04:00
} ) ;
2020-12-18 05:07:32 +02:00
// ! childList for paste first symbol
observer . observe ( input , { characterData : true , childList : true , subtree : true } ) ;
2021-01-18 22:34:41 +04:00
if ( options . animate ) {
2021-02-01 04:31:34 +02:00
input . classList . add ( 'scrollable' , 'scrollable-y' ) ;
2021-02-01 04:37:35 +02:00
this . wasInputFakeClientHeight = 0 ;
2021-02-01 04:31:34 +02:00
this . showScrollDebounced = debounce ( ( ) = > this . input . classList . remove ( 'no-scrollbar' ) , 150 , false , true ) ;
2021-01-18 22:34:41 +04:00
this . inputFake = document . createElement ( 'div' ) ;
this . inputFake . setAttribute ( 'contenteditable' , 'true' ) ;
this . inputFake . className = input . className + ' input-field-input-fake' ;
}
2020-12-18 05:07:32 +02:00
} else {
this . container . innerHTML = `
2021-03-25 22:07:00 +04:00
< input type = "text" $ { name ? ` name=" ${ name } " ` : '' } autocomplete = "off" $ { label ? 'required=""' : '' } class = "input-field-input" >
2020-12-18 05:07:32 +02:00
` ;
input = this . container . firstElementChild as HTMLElement ;
input . addEventListener ( 'input' , ( ) = > checkAndSetRTL ( input ) ) ;
2021-03-26 19:29:10 +04:00
}
2021-03-25 22:07:00 +04:00
2021-03-26 19:29:10 +04:00
if ( placeholder ) {
_i18n ( input , placeholder , undefined , 'placeholder' ) ;
2021-04-16 07:51:47 +04:00
if ( this . inputFake ) {
_i18n ( this . inputFake , placeholder , undefined , 'placeholder' ) ;
}
2020-11-14 01:04:03 +02:00
}
2020-10-13 00:06:02 +03:00
2021-04-18 15:55:56 +04:00
if ( label || placeholder ) {
const border = document . createElement ( 'div' ) ;
border . classList . add ( 'input-field-border' ) ;
this . container . append ( border ) ;
}
2021-02-19 19:27:56 +04:00
if ( label ) {
2021-03-21 13:59:59 +04:00
this . label = document . createElement ( 'label' ) ;
2021-03-30 19:31:11 +04:00
this . setLabel ( ) ;
2021-03-21 13:59:59 +04:00
this . container . append ( this . label ) ;
2021-02-19 19:27:56 +04:00
}
2020-12-18 05:07:32 +02:00
let processInput : ( ) = > void ;
if ( maxLength ) {
const labelEl = this . container . lastElementChild as HTMLLabelElement ;
let showingLength = false ;
processInput = ( ) = > {
const wasError = input . classList . contains ( 'error' ) ;
// * https://stackoverflow.com/a/54369605 #2 to count emoji as 1 symbol
const inputLength = plainText ? ( input as HTMLInputElement ) . value . length : [ . . . getRichValue ( input ) ] . length ;
const diff = maxLength - inputLength ;
const isError = diff < 0 ;
input . classList . toggle ( 'error' , isError ) ;
2020-12-18 18:43:17 +02:00
//this.onLengthChange && this.onLengthChange(inputLength, isError);
2020-12-18 05:07:32 +02:00
if ( isError || diff <= showLengthOn ) {
2021-03-30 19:31:11 +04:00
this . setLabel ( ) ;
labelEl . append ( ` ( ${ maxLength - inputLength } ) ` ) ;
2020-12-18 05:07:32 +02:00
if ( ! showingLength ) showingLength = true ;
} else if ( ( wasError && ! isError ) || showingLength ) {
2021-03-30 19:31:11 +04:00
this . setLabel ( ) ;
2020-12-18 05:07:32 +02:00
showingLength = false ;
}
} ;
input . addEventListener ( 'input' , processInput ) ;
}
2020-11-14 01:04:03 +02:00
2020-12-18 05:07:32 +02:00
this . input = input ;
}
2020-11-14 01:04:03 +02:00
2021-04-16 08:11:52 +04:00
public select() {
if ( ( this . input as HTMLInputElement ) . value ) { // * avoid selecting whole empty field on iOS devices
( this . input as HTMLInputElement ) . select ( ) ; // * select text
}
}
2021-03-30 19:31:11 +04:00
public setLabel() {
this . label . textContent = '' ;
if ( this . options . labelText ) {
this . label . innerHTML = this . options . labelText ;
} else {
this . label . append ( i18n ( this . options . label , this . options . labelOptions ) ) ;
}
}
2021-01-18 22:34:41 +04:00
public onFakeInput() {
2021-02-01 04:37:35 +02:00
const { scrollHeight , clientHeight } = this . inputFake ;
if ( this . wasInputFakeClientHeight && this . wasInputFakeClientHeight !== clientHeight ) {
2021-02-01 04:31:34 +02:00
this . input . classList . add ( 'no-scrollbar' ) ; // ! в сафари может вообще не появиться скролл после анимации, так как ему нужен полный reflow блока с overflow.
this . showScrollDebounced ( ) ;
}
2021-02-01 04:37:35 +02:00
this . wasInputFakeClientHeight = clientHeight ;
2021-01-18 22:34:41 +04:00
this . input . style . height = scrollHeight ? scrollHeight + 'px' : '' ;
}
2020-12-18 05:07:32 +02:00
get value() {
return this . options . plainText ? ( this . input as HTMLInputElement ) . value : getRichValue ( this . input ) ;
//return getRichValue(this.input);
2020-11-14 01:04:03 +02:00
}
2020-12-18 05:07:32 +02:00
set value ( value : string ) {
2021-01-18 22:34:41 +04:00
this . setValueSilently ( value , false ) ;
2020-11-14 01:04:03 +02:00
2020-12-18 05:07:32 +02:00
const event = new Event ( 'input' , { bubbles : true , cancelable : true } ) ;
this . input . dispatchEvent ( event ) ;
2020-10-13 01:38:54 +03:00
}
2021-01-18 22:34:41 +04:00
public setValueSilently ( value : string , fireFakeInput = true ) {
2020-12-18 05:07:32 +02:00
if ( this . options . plainText ) {
( this . input as HTMLInputElement ) . value = value ;
} else {
this . input . innerHTML = value ;
2021-01-18 22:34:41 +04:00
if ( this . inputFake ) {
this . inputFake . innerHTML = value ;
if ( fireFakeInput ) {
this . onFakeInput ( ) ;
}
}
2020-12-18 05:07:32 +02:00
}
}
2021-03-12 22:08:39 +04:00
public isValid() {
return ! this . input . classList . contains ( 'error' ) && this . value !== this . originalValue ;
}
2021-03-16 19:18:51 +04:00
public setOriginalValue ( value : InputField [ 'originalValue' ] = '' , silent = false ) {
2021-03-12 22:08:39 +04:00
this . originalValue = value ;
2021-03-16 19:18:51 +04:00
if ( ! this . options . plainText ) {
value = RichTextProcessor . wrapDraftText ( value ) ;
}
if ( silent ) {
this . setValueSilently ( value , false ) ;
2021-03-12 22:08:39 +04:00
} else {
2021-03-16 19:18:51 +04:00
this . value = value ;
}
}
2021-03-21 18:36:14 +04:00
public setState ( state : InputState , label? : LangPackKey ) {
2021-03-16 19:18:51 +04:00
if ( label ) {
2021-03-26 21:49:29 +04:00
this . label . textContent = '' ;
this . label . append ( i18n ( label , this . options . labelOptions ) ) ;
2021-03-12 22:08:39 +04:00
}
2021-03-16 19:18:51 +04:00
this . input . classList . toggle ( 'error' , ! ! ( state & InputState . Error ) ) ;
this . input . classList . toggle ( 'valid' , ! ! ( state & InputState . Valid ) ) ;
}
2021-03-23 20:13:35 +04:00
public setError ( label? : LangPackKey ) {
2021-03-16 19:18:51 +04:00
this . setState ( InputState . Error , label ) ;
2021-03-12 22:08:39 +04:00
}
2020-12-18 05:07:32 +02:00
}
2020-10-13 00:06:02 +03:00
2021-03-09 02:15:44 +04:00
export default InputField ;