diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index c591d0ef..b2e161ab 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -11,7 +11,7 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; import opusDecodeController from "../../lib/opusDecodeController"; import { RichTextProcessor } from "../../lib/richtextprocessor"; import $rootScope from '../../lib/rootScope'; -import { cancelEvent, findUpClassName, getRichValue } from "../../helpers/dom"; +import { cancelEvent, findUpClassName, getRichValue, isInputEmpty, serializeNodes } from "../../helpers/dom"; import ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu'; import emoticonsDropdown from "../emoticonsDropdown"; import PopupCreatePoll from "../popupCreatePoll"; @@ -21,7 +21,7 @@ import { ripple } from '../ripple'; import Scrollable from "../scrollable"; import { toast } from "../toast"; import { wrapReply } from "../wrappers"; -import { checkRTL } from '../../helpers/string'; +import InputField from '../inputField'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; @@ -30,7 +30,7 @@ type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply'; export class ChatInput { public pageEl = document.getElementById('page-chats') as HTMLDivElement; - public messageInput = document.getElementById('input-message') as HTMLDivElement/* HTMLInputElement */; + public messageInput: HTMLDivElement/* HTMLInputElement */; public fileInput = document.getElementById('input-file') as HTMLInputElement; public inputMessageContainer = document.getElementsByClassName('input-message-container')[0] as HTMLDivElement; public inputScroll = new Scrollable(this.inputMessageContainer); @@ -73,6 +73,15 @@ export class ChatInput { private helperFunc: () => void; constructor() { + const messageInputField = InputField({ + placeholder: 'Message', + name: 'message' + }); + + messageInputField.input.className = ''; + this.inputScroll.container.append(messageInputField.input); + this.messageInput = messageInputField.input; + this.attachMenu = document.getElementById('attach-file') as HTMLButtonElement; let willAttachType: 'document' | 'media'; @@ -183,8 +192,6 @@ export class ChatInput { this.messageInput.addEventListener('input', (e) => { //console.log('messageInput input', this.messageInput.innerText, this.serializeNodes(Array.from(this.messageInput.childNodes))); - this.setDirection(); - const value = this.messageInput.innerText; const entities = RichTextProcessor.parseEntities(value); @@ -217,7 +224,7 @@ export class ChatInput { } } - if(!value.trim() && !this.serializeNodes(Array.from(this.messageInput.childNodes)).trim()) { + if(!value.trim() && !serializeNodes(Array.from(this.messageInput.childNodes)).trim()) { this.messageInput.innerHTML = ''; appMessagesManager.setTyping($rootScope.selectedPeerID, 'sendMessageCancelAction'); @@ -232,7 +239,7 @@ export class ChatInput { this.updateSendBtn(); }); - if(!RichTextProcessor.emojiSupported) { + /* if(!RichTextProcessor.emojiSupported) { this.messageInput.addEventListener('copy', (e) => { const selection = document.getSelection(); @@ -243,7 +250,7 @@ export class ChatInput { let selectedNodes = Array.from(ancestorContainer.childNodes).slice(range.startOffset, range.endOffset); if(selectedNodes.length) { - str = this.serializeNodes(selectedNodes); + str = serializeNodes(selectedNodes); } else { str = selection.toString(); } @@ -257,30 +264,7 @@ export class ChatInput { event.clipboardData.setData('text/plain', str); event.preventDefault(); }); - } - - this.messageInput.addEventListener('paste', (e) => { - //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); - entities = entities.filter(e => e._ == 'messageEntityEmoji' || e._ == 'messageEntityLinebreak'); - //text = RichTextProcessor.wrapEmojiText(text); - text = RichTextProcessor.wrapRichText(text, {entities, noLinks: true}); - - // 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); - }); + } */ this.fileInput.addEventListener('change', (e) => { let files = (e.target as HTMLInputElement & EventTarget).files; @@ -292,7 +276,7 @@ export class ChatInput { this.fileInput.value = ''; }, false); - document.addEventListener('paste', (event) => { + document.addEventListener('paste', (e) => { const peerID = $rootScope.selectedPeerID; if(!peerID || $rootScope.overlayIsActive || (peerID < 0 && !appChatsManager.hasRights(peerID, 'send', 'send_media'))) { return; @@ -301,13 +285,15 @@ export class ChatInput { //console.log('document paste'); // @ts-ignore - var items = (event.clipboardData || event.originalEvent.clipboardData).items; + const items = (e.clipboardData || e.originalEvent.clipboardData).items; //console.log('item', event.clipboardData.getData()); + let foundFile = false; for(let i = 0; i < items.length; ++i) { if(items[i].kind == 'file') { - event.preventDefault() - event.cancelBubble = true; - event.stopPropagation(); + e.preventDefault() + e.cancelBubble = true; + e.stopPropagation(); + foundFile = true; let file = items[i].getAsFile(); //console.log(items[i], file); @@ -541,10 +527,8 @@ export class ChatInput { }); } - private isInputEmpty() { - let value = this.messageInput.innerText; - - return !value.trim() && !this.serializeNodes(Array.from(this.messageInput.childNodes)).trim(); + public isInputEmpty() { + return isInputEmpty(this.messageInput); } public updateSendBtn() { @@ -556,18 +540,6 @@ export class ChatInput { this.btnSend.classList.toggle('send', icon == 'send'); this.btnSend.classList.toggle('record', icon == 'record'); } - - public serializeNodes(nodes: Node[]): string { - return nodes.reduce((str, child: any) => { - //console.log('childNode', str, child, typeof(child), typeof(child) === 'string', child.innerText); - - if(typeof(child) === 'object' && child.textContent) return str += child.textContent; - if(child.innerText) return str += child.innerText; - if(child.tagName == 'IMG' && child.classList && child.classList.contains('emoji')) return str += child.getAttribute('alt'); - - return str; - }, ''); - }; public onMessageSent(clearInput = true, clearReply?: boolean) { let dialog = appMessagesManager.getDialogByPeerID(appImManager.peerID)[0]; @@ -587,17 +559,6 @@ export class ChatInput { } this.updateSendBtn(); - this.setDirection(); - } - - public setDirection() { - const char = this.messageInput.innerText[0]; - let direction = 'ltr'; - if(char && checkRTL(char)) { - direction = 'rtl'; - } - - this.messageInput.style.direction = direction; } public sendMessage() { @@ -708,7 +669,6 @@ export class ChatInput { this.editMsgID = 0; this.helperType = this.helperFunc = undefined; this.chatInput.parentElement.classList.remove('is-helper-active'); - this.setDirection(); } public setTopInfo(type: ChatInputHelperType, callerFunc: () => void, title = '', subtitle = '', input?: string, message?: any) { @@ -735,7 +695,6 @@ export class ChatInput { setTimeout(() => { this.updateSendBtn(); - this.setDirection(); }, 0); } diff --git a/src/components/inputField.ts b/src/components/inputField.ts index eb794e11..300a2218 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -1,33 +1,119 @@ -const InputField = (placeholder: string, label: string, name: string, maxLength?: number, showLengthOn: number = maxLength ? maxLength / 3 : 0) => { +import { getRichValue, isInputEmpty } from "../helpers/dom"; +import { checkRTL } from "../helpers/string"; +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); + entities = entities.filter(e => e._ == 'messageEntityEmoji' || e._ == 'messageEntityLinebreak'); + //text = RichTextProcessor.wrapEmojiText(text); + text = RichTextProcessor.wrapRichText(text, {entities, noLinks: true}); + + // 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; +}; + +const InputField = (options: { + placeholder?: string, + label?: string, + name: string, + maxLength?: number, + showLengthOn?: number, + plainText?: true +}) => { const div = document.createElement('div'); div.classList.add('input-field'); - div.innerHTML = ` - - - `; + if(options.maxLength) { + options.showLengthOn = Math.round(options.maxLength / 3); + } + + const {placeholder, label, maxLength, showLengthOn, name, plainText} = options; + + if(!plainText) { + if(init) { + init(); + } + div.innerHTML = ` +
+ ${label ? `` : ''} + `; + + const input = div.firstElementChild as HTMLElement; + const observer = new MutationObserver((mutationsList, observer) => { + const isEmpty = isInputEmpty(input); + console.log('input', isEmpty); + + const char = input.innerText[0]; + let direction = 'ltr'; + if(char && checkRTL(char)) { + direction = 'rtl'; + } + + input.style.direction = direction; + + if(processInput) { + processInput(); + } + }); + + // ! childList for paste first symbol + observer.observe(input, {characterData: true, childList: true, subtree: true}); + } else { + div.innerHTML = ` + + ${label ? `` : ''} + `; + } + + let processInput: () => void; if(maxLength) { const input = div.firstElementChild as HTMLInputElement; const labelEl = div.lastElementChild as HTMLLabelElement; let showingLength = false; - input.addEventListener('input', (e) => { + + processInput = () => { const wasError = input.classList.contains('error'); - const diff = maxLength - input.value.length; + const inputLength = plainText ? input.value.length : getRichValue(input).length; + const diff = maxLength - inputLength; const isError = diff < 0; input.classList.toggle('error', isError); if(isError || diff <= showLengthOn) { - labelEl.innerText = label + ` (${maxLength - input.value.length})`; + labelEl.innerText = label + ` (${maxLength - inputLength})`; if(!showingLength) showingLength = true; } else if((wasError && !isError) || showingLength) { labelEl.innerText = label; showingLength = false; } - }); + }; + + input.addEventListener('input', processInput); } - return div; + return {container: div, input: div.firstElementChild as HTMLInputElement}; }; export default InputField; \ No newline at end of file diff --git a/src/components/popupCreatePoll.ts b/src/components/popupCreatePoll.ts index 0afefbcc..92b77690 100644 --- a/src/components/popupCreatePoll.ts +++ b/src/components/popupCreatePoll.ts @@ -2,7 +2,7 @@ import appMessagesManager from "../lib/appManagers/appMessagesManager"; import appPeersManager from "../lib/appManagers/appPeersManager"; import appPollsManager, { Poll } from "../lib/appManagers/appPollsManager"; import $rootScope from "../lib/rootScope"; -import { findUpTag, whichChild } from "../helpers/dom"; +import { cancelEvent, findUpTag, getRichValue, isInputEmpty, whichChild } from "../helpers/dom"; import CheckboxField from "./checkbox"; import InputField from "./inputField"; import { PopupElement } from "./popup"; @@ -32,10 +32,15 @@ export default class PopupCreatePoll extends PopupElement { this.title.innerText = 'New Poll'; - const questionField = InputField('Ask a Question', 'Ask a Question', 'question', MAX_LENGTH_QUESTION); - this.questionInput = questionField.firstElementChild as HTMLInputElement; + const questionField = InputField({ + placeholder: 'Ask a Question', + label: 'Ask a Question', + name: 'question', + maxLength: MAX_LENGTH_QUESTION + }); + this.questionInput = questionField.input; - this.header.append(questionField); + this.header.append(questionField.container); const hr = document.createElement('hr'); const d = document.createElement('div'); @@ -93,14 +98,19 @@ export default class PopupCreatePoll extends PopupElement { const quizSolutionContainer = document.createElement('div'); quizSolutionContainer.classList.add('poll-create-questions'); - const quizSolutionField = InputField('Add a Comment (Optional)', 'Add a Comment (Optional)', 'solution', MAX_LENGTH_SOLUTION); - this.quizSolutionInput = quizSolutionField.firstElementChild as HTMLInputElement; + const quizSolutionField = InputField({ + placeholder: 'Add a Comment (Optional)', + label: 'Add a Comment (Optional)', + name: 'solution', + maxLength: MAX_LENGTH_SOLUTION + }); + this.quizSolutionInput = quizSolutionField.input; const quizSolutionSubtitle = document.createElement('div'); quizSolutionSubtitle.classList.add('subtitle'); quizSolutionSubtitle.innerText = 'Users will see this comment after choosing a wrong answer, good for educational purposes.'; - quizSolutionContainer.append(quizSolutionField, quizSolutionSubtitle); + quizSolutionContainer.append(quizSolutionField.container, quizSolutionSubtitle); quizElements.push(quizHr, quizSolutionCaption, quizSolutionContainer); quizElements.forEach(el => el.classList.add('hide')); @@ -120,15 +130,15 @@ export default class PopupCreatePoll extends PopupElement { private getFilledAnswers() { const answers = Array.from(this.questions.children).map((el, idx) => { - const input = el.querySelector('input[type="text"]') as HTMLInputElement; - return input.value; + const input = el.querySelector('.input-field-input'); + return getRichValue(input); }).filter(v => !!v.trim()); return answers; } onSubmitClick = (e: MouseEvent) => { - const question = this.questionInput.value.trim(); + const question = getRichValue(this.questionInput); if(!question) { toast('Please enter a question.'); @@ -158,7 +168,7 @@ export default class PopupCreatePoll extends PopupElement { return; } - const quizSolution = this.quizSolutionInput.value.trim() || undefined; + const quizSolution = getRichValue(this.quizSolutionInput) || undefined; if(quizSolution?.length > MAX_LENGTH_SOLUTION) { toast('Explanation is too long.'); return; @@ -210,14 +220,15 @@ export default class PopupCreatePoll extends PopupElement { const target = e.target as HTMLInputElement; const radioLabel = findUpTag(target, 'LABEL'); - if(target.value.length) { + const isEmpty = isInputEmpty(target); + if(!isEmpty) { target.parentElement.classList.add('is-filled'); radioLabel.classList.remove('hidden-widget'); radioLabel.firstElementChild.removeAttribute('disabled'); } const isLast = !radioLabel.nextElementSibling; - if(isLast && target.value.length && this.questions.childElementCount < 10) { + if(isLast && !isEmpty && this.questions.childElementCount < 10) { this.appendMoreField(); } }; @@ -235,11 +246,17 @@ export default class PopupCreatePoll extends PopupElement { private appendMoreField() { const tempID = this.tempID++; const idx = this.questions.childElementCount + 1; - const questionField = InputField('Add an Option', 'Option ' + idx, 'question-' + tempID, MAX_LENGTH_OPTION); - (questionField.firstElementChild as HTMLInputElement).addEventListener('input', this.onInput); + const questionField = InputField({ + placeholder: 'Add an Option', + label: 'Option ' + idx, + name: 'question-' + tempID, + maxLength: MAX_LENGTH_OPTION + }); + questionField.input.addEventListener('input', this.onInput); const radioField = RadioField('', 'question'); - radioField.main.append(questionField); + radioField.main.append(questionField.container); + radioField.main.addEventListener('click', cancelEvent); radioField.label.classList.add('hidden-widget'); radioField.input.disabled = true; if(!this.quizCheckboxField.input.checked) { @@ -255,7 +272,7 @@ export default class PopupCreatePoll extends PopupElement { const deleteBtn = document.createElement('span'); deleteBtn.classList.add('btn-icon', 'tgico-close'); - questionField.append(deleteBtn); + questionField.container.append(deleteBtn); deleteBtn.addEventListener('click', this.onDeleteClick, {once: true}); diff --git a/src/components/popupNewMedia.ts b/src/components/popupNewMedia.ts index a6e94c0f..564fe0a3 100644 --- a/src/components/popupNewMedia.ts +++ b/src/components/popupNewMedia.ts @@ -1,7 +1,7 @@ import { isTouchSupported } from "../helpers/touchSupport"; import appImManager from "../lib/appManagers/appImManager"; import appMessagesManager from "../lib/appManagers/appMessagesManager"; -import { calcImageInBox } from "../helpers/dom"; +import { calcImageInBox, getRichValue } from "../helpers/dom"; import { Layouter, RectPart } from "./groupedLayout"; import InputField from "./inputField"; import { PopupElement } from "./popup"; @@ -53,9 +53,15 @@ export default class PopupNewMedia extends PopupElement { const scrollable = new Scrollable(null); scrollable.container.append(this.mediaContainer); - const inputField = InputField('Add a caption...', 'Caption', 'photo-caption', MAX_LENGTH_CAPTION, 80); - this.input = inputField.firstElementChild as HTMLInputElement; - this.container.append(scrollable.container, inputField); + const inputField = InputField({ + placeholder: 'Add a caption...', + label: 'Caption', + name: 'photo-caption', + maxLength: MAX_LENGTH_CAPTION, + showLengthOn: 80 + }); + this.input = inputField.input; + this.container.append(scrollable.container, inputField.container); this.attachFiles(files); } @@ -72,7 +78,7 @@ export default class PopupNewMedia extends PopupElement { }; public send = () => { - let caption = this.input.value.trim(); + let caption = getRichValue(this.input); if(caption.length > MAX_LENGTH_CAPTION) { toast('Caption is too long.'); return; diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index d2aed243..df24d912 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -158,7 +158,7 @@ export default class Scrollable extends ScrollableBase { }); }; - public checkForTriggers() { + public checkForTriggers = () => { if(this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) return; const container = this.container; @@ -179,7 +179,7 @@ export default class Scrollable extends ScrollableBase { if(this.onScrolledBottom && (maxScrollTop - scrollTop) <= this.onScrollOffset) { this.onScrolledBottom(); } - } + }; public prepend(element: HTMLElement) { (this.splitUp || this.container).prepend(element); diff --git a/src/components/sidebarLeft/tabs/archivedTab.ts b/src/components/sidebarLeft/tabs/archivedTab.ts index e6e3d832..fcc21d6c 100644 --- a/src/components/sidebarLeft/tabs/archivedTab.ts +++ b/src/components/sidebarLeft/tabs/archivedTab.ts @@ -19,7 +19,7 @@ export default class AppArchivedTab implements SliderTab { appDialogsManager.setListClickListener(this.chatList, null, true); window.addEventListener('resize', () => { - setTimeout(appDialogsManager.onChatsScroll, 0); + setTimeout(appDialogsManager.scroll.checkForTriggers, 0); }); } diff --git a/src/components/sidebarLeft/tabs/editProfile.ts b/src/components/sidebarLeft/tabs/editProfile.ts index 500bde25..e9e19fac 100644 --- a/src/components/sidebarLeft/tabs/editProfile.ts +++ b/src/components/sidebarLeft/tabs/editProfile.ts @@ -1,9 +1,13 @@ import appSidebarLeft from ".."; +import { getRichValue } from "../../../helpers/dom"; import { InputFile } from "../../../layer"; import appProfileManager from "../../../lib/appManagers/appProfileManager"; import appUsersManager from "../../../lib/appManagers/appUsersManager"; import apiManager from "../../../lib/mtproto/mtprotoworker"; +import RichTextProcessor from "../../../lib/richtextprocessor"; import $rootScope from "../../../lib/rootScope"; +import AvatarElement from "../../avatar"; +import InputField from "../../inputField"; import PopupAvatar from "../../popupAvatar"; import Scrollable from "../../scrollable"; import { SliderTab } from "../../slider"; @@ -11,21 +15,21 @@ import { SliderTab } from "../../slider"; // TODO: аватарка не поменяется в этой вкладке после изменения почему-то (если поставить в другом клиенте, и потом тут проверить, для этого ещё вышел в чатлист) export default class AppEditProfileTab implements SliderTab { - private container = document.querySelector('.edit-profile-container') as HTMLDivElement; - private scrollWrapper = this.container.querySelector('.scroll-wrapper') as HTMLDivElement; - private nextBtn = this.container.querySelector('.btn-corner') as HTMLButtonElement; - private canvas = this.container.querySelector('.avatar-edit-canvas') as HTMLCanvasElement; + private container: HTMLElement; + private scrollWrapper: HTMLElement; + private nextBtn: HTMLButtonElement; + private canvas: HTMLCanvasElement; private uploadAvatar: () => Promise