/ *
* https : //github.com/morethanwords/tweb
* Copyright ( C ) 2019 - 2021 Eduard Kuzmenko
* https : //github.com/morethanwords/tweb/blob/master/LICENSE
* /
import simulateEvent from "../helpers/dom/dispatchEvent" ;
import findUpAttribute from "../helpers/dom/findUpAttribute" ;
import getRichValue from "../helpers/dom/getRichValue" ;
import isInputEmpty from "../helpers/dom/isInputEmpty" ;
import selectElementContents from "../helpers/dom/selectElementContents" ;
import { MessageEntity } from "../layer" ;
import { i18n , LangPackKey , _i18n } from "../lib/langPack" ;
import RichTextProcessor from "../lib/richtextprocessor" ;
import SetTransition from "./singleTransition" ;
let init = ( ) = > {
document . addEventListener ( 'paste' , ( e ) = > {
if ( ! findUpAttribute ( e . target , 'contenteditable="true"' ) ) {
return ;
}
e . preventDefault ( ) ;
let text : string , entities : MessageEntity [ ] ;
// @ts-ignore
let plainText : string = ( e . originalEvent || e ) . clipboardData . getData ( 'text/plain' ) ;
let usePlainText = true ;
// @ts-ignore
let html : string = ( e . originalEvent || e ) . clipboardData . getData ( 'text/html' ) ;
if ( html . trim ( ) ) {
html = html . replace ( /<style([\s\S]*)<\/style>/ , '' ) ;
html = html . replace ( /<!--([\s\S]*)-->/ , '' ) ;
const match = html . match ( /<body>([\s\S]*)<\/body>/ ) ;
if ( match ) {
html = match [ 1 ] . trim ( ) ;
}
let span : HTMLElement = document . createElement ( 'span' ) ;
span . innerHTML = html ;
let curChild = span . firstChild ;
while ( curChild ) { // * fix whitespace between elements like <p>asd</p>\n<p>zxc</p>
let nextSibling = curChild . nextSibling ;
if ( curChild . nodeType === 3 ) {
if ( ! curChild . nodeValue . trim ( ) ) {
curChild . remove ( ) ;
}
}
curChild = nextSibling ;
}
const richValue = getRichValue ( span , true ) ;
if ( richValue . value . replace ( /\s/g , '' ) . length === plainText . replace ( /\s/g , '' ) . length ) {
text = richValue . value ;
entities = richValue . entities ;
usePlainText = false ;
let entities2 = RichTextProcessor . parseEntities ( text ) ;
entities2 = entities2 . filter ( e = > e . _ === 'messageEntityEmoji' || e . _ === 'messageEntityLinebreak' ) ;
RichTextProcessor . mergeEntities ( entities , entities2 ) ;
}
}
if ( usePlainText ) {
text = plainText ;
entities = RichTextProcessor . parseEntities ( text ) ;
entities = entities . filter ( e = > e . _ === 'messageEntityEmoji' || e . _ === 'messageEntityLinebreak' ) ;
}
text = RichTextProcessor . wrapDraftText ( text , { entities } ) ;
window . document . execCommand ( 'insertHTML' , false , text ) ;
} ) ;
init = null ;
} ;
// ! it doesn't respect symbols other than strongs
/ * c o n s t c h e c k A n d S e t R T L = ( i n p u t : H T M L E l e m e n t ) = > {
//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 ;
} ; * /
export enum InputState {
Neutral = 0 ,
Valid = 1 ,
Error = 2
} ;
export type InputFieldOptions = {
placeholder? : LangPackKey ,
label? : LangPackKey ,
labelOptions? : any [ ] ,
labelText? : string ,
name? : string ,
maxLength? : number ,
showLengthOn? : number ,
plainText? : true ,
animate? : boolean ,
required? : boolean ,
canBeEdited? : boolean ,
validate ? : ( ) = > boolean
} ;
class InputField {
public container : HTMLElement ;
public input : HTMLElement ;
public inputFake : HTMLElement ;
public label : HTMLLabelElement ;
public originalValue : string ;
public required : boolean ;
public validate : ( ) = > boolean ;
//public onLengthChange: (length: number, isOverflow: boolean) => void;
// protected wasInputFakeClientHeight: number;
// protected showScrollDebounced: () => void;
constructor ( public options : InputFieldOptions = { } ) {
this . container = document . createElement ( 'div' ) ;
this . container . classList . add ( 'input-field' ) ;
this . required = options . required ;
this . validate = options . validate ;
if ( options . maxLength !== undefined && options . showLengthOn === undefined ) {
options . showLengthOn = Math . min ( 40 , Math . round ( options . maxLength / 3 ) ) ;
}
const { placeholder , maxLength , showLengthOn , name , plainText , canBeEdited = true } = options ;
let label = options . label || options . labelText ;
let input : HTMLElement ;
if ( ! plainText ) {
if ( init ) {
init ( ) ;
}
this . container . innerHTML = `
< div contenteditable = "${String(!!canBeEdited)}" class = "input-field-input" > < / div >
` ;
input = this . container . firstElementChild as HTMLElement ;
const observer = new MutationObserver ( ( ) = > {
//checkAndSetRTL(input);
if ( processInput ) {
processInput ( ) ;
}
} ) ;
// * because if delete all characters there will br left
input . addEventListener ( 'input' , ( ) = > {
if ( isInputEmpty ( input ) ) {
input . innerHTML = '' ;
}
if ( this . inputFake ) {
this . inputFake . innerHTML = input . innerHTML ;
this . onFakeInput ( ) ;
}
} ) ;
// ! childList for paste first symbol
observer . observe ( input , { characterData : true , childList : true , subtree : true } ) ;
if ( options . animate ) {
input . classList . add ( 'scrollable' , 'scrollable-y' ) ;
// this.wasInputFakeClientHeight = 0;
// this.showScrollDebounced = debounce(() => this.input.classList.remove('no-scrollbar'), 150, false, true);
this . inputFake = document . createElement ( 'div' ) ;
this . inputFake . setAttribute ( 'contenteditable' , 'true' ) ;
this . inputFake . className = input . className + ' input-field-input-fake' ;
}
} else {
this . container . innerHTML = `
< input type = "text" $ { name ? ` name = "${name}" ` : ' ' } autocomplete = "off" $ { label ? ' required = "" ' : ' ' } class = "input-field-input" >
` ;
input = this . container . firstElementChild as HTMLElement ;
//input.addEventListener('input', () => checkAndSetRTL(input));
}
input . setAttribute ( 'dir' , 'auto' ) ;
if ( placeholder ) {
_i18n ( input , placeholder , undefined , 'placeholder' ) ;
if ( this . inputFake ) {
_i18n ( this . inputFake , placeholder , undefined , 'placeholder' ) ;
}
}
if ( label || placeholder ) {
const border = document . createElement ( 'div' ) ;
border . classList . add ( 'input-field-border' ) ;
this . container . append ( border ) ;
}
if ( label ) {
this . label = document . createElement ( 'label' ) ;
this . setLabel ( ) ;
this . container . append ( this . label ) ;
}
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 , false ) . value ] . length ;
const diff = maxLength - inputLength ;
const isError = diff < 0 ;
input . classList . toggle ( 'error' , isError ) ;
//this.onLengthChange && this.onLengthChange(inputLength, isError);
if ( isError || diff <= showLengthOn ) {
this . setLabel ( ) ;
labelEl . append ( ` ( ${ maxLength - inputLength } ) ` ) ;
if ( ! showingLength ) showingLength = true ;
} else if ( ( wasError && ! isError ) || showingLength ) {
this . setLabel ( ) ;
showingLength = false ;
}
} ;
input . addEventListener ( 'input' , processInput ) ;
}
this . input = input ;
}
public select() {
if ( ! this . value ) { // * avoid selecting whole empty field on iOS devices
return ;
}
if ( this . options . plainText ) {
( this . input as HTMLInputElement ) . select ( ) ; // * select text
} else {
selectElementContents ( this . input ) ;
}
}
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 ) ) ;
}
}
public onFakeInput ( setHeight = true ) {
const { scrollHeight : newHeight /* , clientHeight */ } = this . inputFake ;
/ * i f ( t h i s . w a s I n p u t F a k e C l i e n t H e i g h t & & t h i s . w a s I n p u t F a k e C l i e n t H e i g h t ! = = c l i e n t H e i g h t ) {
this . input . classList . add ( 'no-scrollbar' ) ; // ! в сафари может вообще не появиться скролл после анимации, так как ему нужен полный reflow блока с overflow.
this . showScrollDebounced ( ) ;
} * /
const currentHeight = + this . input . style . height . replace ( 'px' , '' ) ;
if ( currentHeight === newHeight ) {
return ;
}
const TRANSITION_DURATION_FACTOR = 50 ;
const transitionDuration = Math . round (
TRANSITION_DURATION_FACTOR * Math . log ( Math . abs ( newHeight - currentHeight ) ) ,
) ;
// this.wasInputFakeClientHeight = clientHeight;
this . input . style . transitionDuration = ` ${ transitionDuration } ms ` ;
if ( setHeight ) {
this . input . style . height = newHeight ? newHeight + 'px' : '' ;
}
const className = 'is-changing-height' ;
SetTransition ( this . input , className , true , transitionDuration , ( ) = > {
this . input . classList . remove ( className ) ;
} ) ;
}
get value() {
return this . options . plainText ? ( this . input as HTMLInputElement ) . value : getRichValue ( this . input , false ) . value ;
//return getRichValue(this.input);
}
set value ( value : string ) {
this . setValueSilently ( value , false ) ;
simulateEvent ( this . input , 'input' ) ;
}
public setValueSilently ( value : string , fireFakeInput = true ) {
if ( this . options . plainText ) {
( this . input as HTMLInputElement ) . value = value ;
} else {
this . input . innerHTML = value ;
if ( this . inputFake ) {
this . inputFake . innerHTML = value ;
if ( fireFakeInput ) {
this . onFakeInput ( ) ;
}
}
}
}
public isChanged() {
return this . value !== this . originalValue ;
}
public isValid() {
return ! this . input . classList . contains ( 'error' ) &&
( ! this . validate || this . validate ( ) ) &&
( ! this . required || ! isInputEmpty ( this . input ) ) ;
}
public isValidToChange() {
return this . isValid ( ) && this . isChanged ( ) ;
}
public setDraftValue ( value = '' , silent = false ) {
if ( ! this . options . plainText ) {
value = RichTextProcessor . wrapDraftText ( value ) ;
}
if ( silent ) {
this . setValueSilently ( value , false ) ;
} else {
this . value = value ;
}
}
public setOriginalValue ( value : InputField [ 'originalValue' ] = '' , silent = false ) {
this . originalValue = value ;
this . setDraftValue ( value , silent ) ;
}
public setState ( state : InputState , label? : LangPackKey ) {
if ( label ) {
this . label . textContent = '' ;
this . label . append ( i18n ( label , this . options . labelOptions ) ) ;
}
this . input . classList . toggle ( 'error' , ! ! ( state & InputState . Error ) ) ;
this . input . classList . toggle ( 'valid' , ! ! ( state & InputState . Valid ) ) ;
}
public setError ( label? : LangPackKey ) {
this . setState ( InputState . Error , label ) ;
}
}
export default InputField ;