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.
 
 
 
 
 

661 lines
21 KiB

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import cancelEvent from '../helpers/dom/cancelEvent';
import simulateEvent from '../helpers/dom/dispatchEvent';
import documentFragmentToHTML from '../helpers/dom/documentFragmentToHTML';
import findUpAttribute from '../helpers/dom/findUpAttribute';
import findUpTag from '../helpers/dom/findUpTag';
import getCaretPosNew from '../helpers/dom/getCaretPosNew';
import getRichValueWithCaret from '../helpers/dom/getRichValueWithCaret';
import isInputEmpty from '../helpers/dom/isInputEmpty';
import replaceContent from '../helpers/dom/replaceContent';
import RichInputHandler, {USING_BOMS} from '../helpers/dom/richInputHandler';
import selectElementContents from '../helpers/dom/selectElementContents';
import setInnerHTML, {setDirection} from '../helpers/dom/setInnerHTML';
import {MessageEntity} from '../layer';
import {i18n, LangPackKey, _i18n} from '../lib/langPack';
import {NULL_PEER_ID} from '../lib/mtproto/mtproto_config';
import mergeEntities from '../lib/richTextProcessor/mergeEntities';
import parseEntities from '../lib/richTextProcessor/parseEntities';
import wrapDraftText from '../lib/richTextProcessor/wrapDraftText';
import {createCustomFiller, CustomEmojiElement, CustomEmojiRendererElement, insertCustomFillers, renderEmojis} from '../lib/richTextProcessor/wrapRichText';
export async function insertRichTextAsHTML(input: HTMLElement, text: string, entities: MessageEntity[], wrappingForPeerId: PeerId) {
const loadPromises: Promise<any>[] = [];
const wrappingCustomEmoji = entities?.some((entity) => entity._ === 'messageEntityCustomEmoji');
const renderer = wrappingCustomEmoji ? createCustomEmojiRendererForInput() : undefined;
const fragment = wrapDraftText(text, {entities, wrappingForPeerId, loadPromises, customEmojiRenderer: renderer});
const something = fragment.querySelectorAll<HTMLElement>('[contenteditable="false"]');
something.forEach((el) => {
el.contentEditable = 'inherit';
el.classList.add('pc');
});
loadPromises.length && await Promise.all(loadPromises);
fragment.querySelectorAll<HTMLElement>('.input-selectable').forEach((el) => {
el.prepend(createCustomFiller(true));
});
const richInputHandler = USING_BOMS ? RichInputHandler.getInstance() : undefined;
// const restore = richInputHandler.prepareApplyingMarkdown();
// fragment.querySelectorAll('.input-filler').forEach((el) => el.remove());
// const fillers = Array.from(input.querySelectorAll<HTMLElement>('.input-filler')).map((el) => {
// el.contentEditable = 'false';
// return el;
// });
const customEmojiElements = Array.from(fragment.querySelectorAll<HTMLImageElement>('.custom-emoji-placeholder')).map((el) => {
el.dataset.ces = '1';
return (el as any).customEmojiElement as CustomEmojiElement;
});
const html = documentFragmentToHTML(fragment);
renderer?.destroy();
// console.log(html);
const pre = getCaretPosNew(input);
// console.log('pre', pre);
let textNode: ChildNode, textNodeValue: string;
if(pre.node) {
// if(pre.node?.nodeValue === BOM && false) {
// textNode = document.createTextNode(textNodeValue = BOM);
// (pre.node.parentNode as any as ChildNode).after(textNode);
// pre.selection.modify('extend', 'forward', 'character');
// pre.selection.collapseToEnd();
// }
} else {
const range = document.createRange();
let node = input.lastChild;
if(!node) {
input.append(node /* = textNode */ = document.createTextNode(''));
}
range.setStartAfter(node);
range.collapse(true);
pre.selection.removeAllRanges();
pre.selection.addRange(range);
}
// const fragmentLastChild = fragment.lastChild;
// const s = document.createElement('span');
// (node as ChildNode).replaceWith(s);
// s.append(node);
input.addEventListener('input', cancelEvent, {capture: true, once: true, passive: false});
richInputHandler?.onBeforeInput({inputType: 'insertContent'});
window.document.execCommand('insertHTML', false, html);
Array.from(input.querySelectorAll<HTMLImageElement>('[data-ces]')).forEach((el, idx) => {
delete el.dataset.ces;
const customEmojiElement = customEmojiElements[idx];
(el as any).customEmojiElement = customEmojiElement;
customEmojiElement.placeholder = el;
});
// fillers.forEach((el) => {
// el.contentEditable = 'inherit';
// });
input.querySelectorAll<HTMLElement>('.pc').forEach((el) => {
el.contentEditable = 'false';
});
if(textNode) {
const {nodeValue} = textNode;
if(nodeValue === textNodeValue) {
textNode.remove();
} else {
(textNode as CharacterData).replaceData(nodeValue.indexOf(textNodeValue), textNodeValue.length, '');
}
}
// restore();
richInputHandler?.removeExtraBOMs(input);
simulateEvent(input, 'input');
// if(textNode) {
// const selection = document.getSelection();
// const node = fragmentLastChild.nextSibling || fragmentLastChild;
// const range = document.createRange();
// range.setStartAfter(node);
// range.collapse(true);
// selection.removeAllRanges();
// selection.addRange(range);
// }
// const after = getCaretPosNew(input);
// console.log('after', after);
// if(after.node?.nodeValue === BOM) {
// const smth = findUpClassName(after.node.parentElement, 'input-something');
// if(smth) {
// const selection = document.getSelection();
// const node = smth.nextSibling;
// const range = document.createRange();
// range.setStartAfter(node);
// range.collapse(true);
// selection.removeAllRanges();
// selection.addRange(range);
// // if(after.offset === 0) after.selection.modify('extend', 'forward', 'character');
// // after.selection.modify('extend', 'forward', 'character');
// // after.selection.collapseToEnd();
// }
// }
// setCaretAt(fragmentLastChild.nextSibling);
// console.log('ASD');
}
let init = () => {
document.addEventListener('paste', (e) => {
const input = findUpAttribute(e.target, 'contenteditable="true"');
if(!input) {
return;
}
const noLinebreaks = !!input.dataset.noLinebreaks;
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') || plainText;
const filterEntity = (e: MessageEntity) => e._ === 'messageEntityEmoji' || (e._ === 'messageEntityLinebreak' && !noLinebreaks);
if(noLinebreaks) {
const regExp = /[\r\n]/g;
plainText = plainText.replace(regExp, '');
html = html.replace(regExp, '');
}
const peerId = (input.dataset.peerId || NULL_PEER_ID).toPeerId();
if(html.trim()) {
// console.log(html.replace(/ (style|class|id)=".+?"/g, ''));
html = html.replace(/<style([\s\S]*)<\/style>/, '');
html = html.replace(/<!--([\s\S]*)-->/, '');
html = html.replace('<br class="Apple-interchange-newline">', '');
const match = html.match(/<body>([\s\S]*)<\/body>/);
if(match) {
html = match[1].trim();
}
// const s = cleanHTML(html, true) as NodeList;
// console.log(s);
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const span = doc.body || document.createElement('body');
// const span: HTMLElement = document.createElement('span');
// span.innerHTML = html;
// span.append(...Array.from(s));
let curChild = span.firstChild;
while(curChild) { // * fix whitespace between elements like <p>asd</p>\n<p>zxc</p>
const nextSibling = curChild.nextSibling;
if(curChild.nodeType === curChild.TEXT_NODE) {
if(!curChild.nodeValue.trim()) {
curChild.remove();
}
}
curChild = nextSibling;
}
const richValue = getRichValueWithCaret(span, true, false);
const canWrapCustomEmojis = !!peerId;
if(!canWrapCustomEmojis) {
richValue.entities = richValue.entities.filter((entity) => entity._ !== 'messageEntityCustomEmoji');
}
/* if(false) */ { // * fix extra new lines appearing from <p> (can have them from some sources, like macOS Terminal)
const lines = richValue.value.split('\n');
let textLength = 0;
for(let lineIndex = 0; lineIndex < lines.length; ++lineIndex) {
const line = lines[lineIndex];
textLength += line.length;
const index = textLength;
if(plainText[index] !== '\n' && lineIndex !== (lines.length - 1)) {
const nextLine = lines.splice(lineIndex + 1, 1)[0];
lines[lineIndex] = line + nextLine;
// fix entities
richValue.entities.forEach((entity) => {
if(entity.offset >= index) {
entity.offset -= 1;
}
});
textLength += nextLine.length;
}
textLength += 1;
}
const correctedText = lines.join('\n');
richValue.value = correctedText;
}
const richTextLength = richValue.value.replace(/\s/g, '').length;
const plainTextLength = plainText.replace(/\s/g, '').length;
if(richTextLength === plainTextLength ||
richValue.entities.find((entity) => entity._ === 'messageEntityCustomEmoji')) {
text = richValue.value;
entities = richValue.entities;
usePlainText = false;
let entities2 = parseEntities(text);
entities2 = entities2.filter(filterEntity);
mergeEntities(entities, entities2);
}
// console.log('usePlainText', usePlainText);
}
if(usePlainText) {
text = plainText;
entities = parseEntities(text);
entities = entities.filter(filterEntity);
}
insertRichTextAsHTML(input, text, entities, peerId);
});
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 | DocumentFragment,
name?: string,
maxLength?: number,
showLengthOn?: number,
plainText?: true,
required?: boolean,
canBeEdited?: boolean,
validate?: () => boolean,
inputMode?: 'tel' | 'numeric',
withLinebreaks?: boolean,
autocomplete?: string
};
function createCustomEmojiRendererForInput() {
const renderer = CustomEmojiRendererElement.create({
wrappingDraft: true,
isSelectable: true,
textColor: 'primary-text-color'
});
return renderer;
}
function processCustomEmojisInInput(input: HTMLElement) {
const customEmojiElements = Array.from(input.querySelectorAll<CustomEmojiElement | HTMLElement>('.custom-emoji, .custom-emoji-placeholder'));
let renderer = input.querySelector<CustomEmojiRendererElement>('.custom-emoji-renderer');
if(!renderer && customEmojiElements.length) {
renderer = createCustomEmojiRendererForInput();
input.prepend(renderer);
} else if(renderer && !customEmojiElements.length) {
renderer.remove();
return;
}
if(!renderer) {
return;
}
const customEmojis: Parameters<CustomEmojiRendererElement['add']>[0] = new Map();
customEmojiElements.forEach((element) => {
const customEmojiElement = element instanceof CustomEmojiElement ? element : (element as any).customEmojiElement as CustomEmojiElement;
const {docId} = customEmojiElement;
let set = customEmojis.get(docId);
if(!set) {
customEmojis.set(docId, set = new Set());
}
set.add(customEmojiElement);
});
for(const [docId, customEmojiElements] of customEmojis) {
let hasSet = renderer.customEmojis.get(docId);
if(hasSet) {
for(const customEmojiElement of hasSet) {
if(!customEmojiElements.has(customEmojiElement)) {
customEmojiElement.destroy();
}
}
} else {
hasSet = new Set();
}
for(const customEmojiElement of customEmojiElements) {
if(!hasSet.has(customEmojiElement)) {
customEmojiElement.connectedCallback();
}
}
}
renderer.add(customEmojis, false);
renderer.forceRender();
}
export default class InputField {
public container: HTMLElement;
public input: HTMLElement;
public label: HTMLLabelElement;
public originalValue: string;
public required: boolean;
public validate: () => boolean;
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, autocomplete} = options;
const label = options.label || options.labelText;
const onInputCallbacks: Array<() => void> = [];
let input: HTMLElement;
if(!plainText) {
if(init) {
init();
}
this.container.innerHTML = `<div class="input-field-input"></div>`;
input = this.container.firstElementChild as HTMLElement;
input.contentEditable = '' + !!canBeEdited;
// const observer = new MutationObserver(() => {
// //checkAndSetRTL(input);
// if(processInput) {
// processInput();
// }
// });
RichInputHandler.getInstance();
input.addEventListener('mousedown', (e) => {
const selection = document.getSelection();
if(!selection.isCollapsed) {
return;
}
const placeholder = findUpTag(e.target, 'IMG');
if(!placeholder) {
return;
}
const rect = placeholder.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const focusOnNext = e.clientX >= centerX;
const range = document.createRange();
range.setStartAfter(focusOnNext ? placeholder : placeholder.previousSibling ?? placeholder);
selection.removeAllRanges();
selection.addRange(range);
});
onInputCallbacks.push(() => {
// console.log('input');
// return;
// * because if delete all characters there will br left
if(isInputEmpty(input)) {
// const textNode = Array.from(input.childNodes).find((node) => node.nodeType === node.TEXT_NODE) || document.createTextNode('');
input.replaceChildren();
// input.append(document.createTextNode('')); // need first text node to support history stack
}
// const fillers = Array.from(input.querySelectorAll('.emoji-filler')) as HTMLElement[];
// fillers.forEach((filler) => {
// const textContent = filler.textContent;
// if(textContent === BOM) {
// return;
// }
// if(textContent) {
// } else {
// let curChild = filler.firstChild;
// while(curChild) {
// curChild = curChild.nextSibling;
// }
// }
// filler.classList.remove('emoji-filler');
// });
// Array.from(input.querySelectorAll('br, span:empty')).forEach((el) => {
// const parentElement = el.parentElement;
// (parentElement === input ? el : parentElement).remove();
// });
USING_BOMS && Array.from(input.querySelectorAll('br:not(.br-not-br)')).forEach((el) => {
// const parentElement = el.parentElement;
el.remove();
// if(!parentElement.children.length && !parentElement.textContent) {
// parentElement.textContent = '';
// }
});
insertCustomFillers(Array.from(input.querySelectorAll('.input-something')));
processCustomEmojisInInput(input);
// .forEach((el) => el.remove());
});
// ! childList for paste first symbol
// observer.observe(input, {characterData: true, childList: true, subtree: true});
} else {
this.container.innerHTML = `
<input type="text" ${name ? `name="${name}"` : ''} autocomplete="${autocomplete ?? 'off'}" ${label ? 'required=""' : ''} class="input-field-input">
`;
input = this.container.firstElementChild as HTMLElement;
// input.addEventListener('input', () => checkAndSetRTL(input));
}
setDirection(input);
if(options.inputMode) {
input.inputMode = options.inputMode;
}
if(placeholder) {
_i18n(input, 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);
}
if(maxLength) {
const labelEl = this.container.lastElementChild as HTMLLabelElement;
let showingLength = false;
const onInput = () => {
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 : [...getRichValueWithCaret(input, false, 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;
}
};
onInputCallbacks.push(onInput);
}
const noLinebreaks = !options.withLinebreaks;
if(noLinebreaks && !plainText) {
input.dataset.noLinebreaks = '1';
input.addEventListener('keypress', (e) => {
if(e.key === 'Enter') {
e.preventDefault();
return false;
}
});
}
if(onInputCallbacks.length) {
input.addEventListener('input', () => {
onInputCallbacks.forEach((callback) => callback());
});
}
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) {
setInnerHTML(this.label, this.options.labelText);
} else {
this.label.append(i18n(this.options.label, this.options.labelOptions));
}
}
get value(): string {
return this.options.plainText ? (this.input as HTMLInputElement).value : getRichValueWithCaret(this.input, false, false).value;
// return getRichValue(this.input);
}
set value(value: Parameters<typeof replaceContent>[1]) {
this.setValueSilently(value, true);
this.simulateInputEvent();
}
public simulateInputEvent() {
simulateEvent(this.input, 'input');
}
public setValueSilently(value: Parameters<typeof replaceContent>[1], fromSet?: boolean) {
if(this.options.plainText) {
(this.input as HTMLInputElement).value = value as string;
} else {
replaceContent(this.input, value);
processCustomEmojisInInput(this.input);
}
}
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?: boolean) {
if(!this.options.plainText) {
value = documentFragmentToHTML(wrapDraftText(value));
}
if(silent) {
this.setValueSilently(value, false);
} else {
this.value = value;
}
}
public setOriginalValue(value: InputField['originalValue'] = '', silent?: boolean) {
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));
} else {
this.setLabel();
}
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);
}
}