Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

378 lines
12 KiB

/*
* 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
/* 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;
}; */
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;
/* if(this.wasInputFakeClientHeight && this.wasInputFakeClientHeight !== clientHeight) {
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;