From 9f7c5377b536dff7b1888810246dab3b596489bb Mon Sep 17 00:00:00 2001 From: morethanwords Date: Thu, 11 Nov 2021 02:58:04 +0400 Subject: [PATCH] Fix inserting emoji --- src/components/chat/input.ts | 42 +++++++++++------ src/components/emoticonsDropdown/index.ts | 15 ++++-- .../emoticonsDropdown/tabs/emoji.ts | 46 ++----------------- src/helpers/dom/getRichValueWithCaret.ts | 18 ++++++-- src/helpers/dom/setCaretAt.ts | 33 +++++++++++++ src/lib/richtextprocessor.ts | 46 +++++++++++++++---- src/scss/partials/_chat.scss | 6 ++- src/scss/partials/_emojiDropdown.scss | 1 + 8 files changed, 133 insertions(+), 74 deletions(-) create mode 100644 src/helpers/dom/setCaretAt.ts diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 27dd9eb2..c08acc9c 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -60,7 +60,6 @@ import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd'; import { MarkdownType, markdownTags } from '../../helpers/dom/getRichElementValue'; import getRichValueWithCaret from '../../helpers/dom/getRichValueWithCaret'; import EmojiHelper from './emojiHelper'; -import setRichFocus from '../../helpers/dom/setRichFocus'; import CommandsHelper from './commandsHelper'; import AutocompleteHelperController from './autocompleteHelperController'; import AutocompleteHelper from './autocompleteHelper'; @@ -82,7 +81,7 @@ import PopupPeer from '../popups/peer'; import MEDIA_MIME_TYPES_SUPPORTED from '../../environment/mediaMimeTypesSupport'; import appMediaPlaybackController from '../appMediaPlaybackController'; import { NULL_PEER_ID } from '../../lib/mtproto/mtproto_config'; -import CheckboxField from '../checkboxField'; +import setCaretAt from '../../helpers/dom/setCaretAt'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; @@ -1332,14 +1331,8 @@ export default class ChatInput { insertEntity.offset = matchIndex; } - addEntities.push({ - _: 'messageEntityCaret', - length: 0, - offset: matchIndex + insertLength - }); - // add offset to entities next to emoji - const diff = insertLength - (matches ? matches[2].length : prefix.length); + const diff = matches ? insertLength - matches[2].length : insertLength; entities.forEach(entity => { if(entity.offset >= matchIndex) { entity.offset += diff; @@ -1348,13 +1341,34 @@ export default class ChatInput { RichTextProcessor.mergeEntities(entities, addEntities); + if(/* caretPos !== -1 && caretPos !== fullValue.length */true) { + const caretEntity: MessageEntity.messageEntityCaret = { + _: 'messageEntityCaret', + offset: matchIndex + insertLength, + length: 0 + }; + + let insertCaretAtIndex = 0; + for(let length = entities.length; insertCaretAtIndex < length; ++insertCaretAtIndex) { + const entity = entities[insertCaretAtIndex]; + if(entity.offset > caretEntity.offset) { + break; + } + } + + entities.splice(insertCaretAtIndex, 0, caretEntity); + } + //const saveExecuted = this.prepareDocumentExecute(); // can't exec .value here because it will instantly check for autocomplete - this.messageInputField.setValueSilently(RichTextProcessor.wrapDraftText(newValue, {entities}), true); + const value = RichTextProcessor.wrapDraftText(newValue, {entities}); + this.messageInputField.setValueSilently(value, true); const caret = this.messageInput.querySelector('.composer-sel'); - setRichFocus(this.messageInput, caret); - caret.remove(); + if(caret) { + setCaretAt(caret); + caret.remove(); + } // but it's needed to be checked only here this.onMessageInput(); @@ -1365,9 +1379,7 @@ export default class ChatInput { } public onEmojiSelected = (emoji: string, autocomplete: boolean) => { - if(autocomplete) { - this.insertAtCaret(emoji, RichTextProcessor.getEmojiEntityFromEmoji(emoji)); - } + this.insertAtCaret(emoji, RichTextProcessor.getEmojiEntityFromEmoji(emoji), autocomplete); }; private checkAutocomplete(value?: string, caretPos?: number, entities?: MessageEntity[]) { diff --git a/src/components/emoticonsDropdown/index.ts b/src/components/emoticonsDropdown/index.ts index 44f88184..bb40f133 100644 --- a/src/components/emoticonsDropdown/index.ts +++ b/src/components/emoticonsDropdown/index.ts @@ -27,6 +27,7 @@ import { cancelEvent } from "../../helpers/dom/cancelEvent"; import DropdownHover from "../../helpers/dropdownHover"; import { pause } from "../../helpers/schedulers/pause"; import appMessagesManager from "../../lib/appManagers/appMessagesManager"; +import { IS_APPLE_MOBILE } from "../../environment/userAgent"; export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown'; @@ -163,10 +164,18 @@ export class EmoticonsDropdown extends DropdownHover { cancelEvent(e); }); + + const HIDE_EMOJI_TAB = IS_APPLE_MOBILE; - (this.tabsEl.children[1] as HTMLLIElement).click(); // set emoji tab - if(this.tabs[0].init) { - this.tabs[0].init(); // onTransitionEnd не вызовется, т.к. это первая открытая вкладка + const INIT_TAB_ID = HIDE_EMOJI_TAB ? 1 : 0; + + if(HIDE_EMOJI_TAB) { + (this.tabsEl.children[1] as HTMLElement).classList.add('hide'); + } + + (this.tabsEl.children[INIT_TAB_ID + 1] as HTMLLIElement).click(); // set emoji tab + if(this.tabs[INIT_TAB_ID].init) { + this.tabs[INIT_TAB_ID].init(); // onTransitionEnd не вызовется, т.к. это первая открытая вкладка } rootScope.addEventListener('peer_changed', this.checkRights); diff --git a/src/components/emoticonsDropdown/tabs/emoji.ts b/src/components/emoticonsDropdown/tabs/emoji.ts index 1bc51b2e..73374f0f 100644 --- a/src/components/emoticonsDropdown/tabs/emoji.ts +++ b/src/components/emoticonsDropdown/tabs/emoji.ts @@ -9,7 +9,6 @@ import { cancelEvent } from "../../../helpers/dom/cancelEvent"; import findUpClassName from "../../../helpers/dom/findUpClassName"; import { fastRaf } from "../../../helpers/schedulers"; import { pause } from "../../../helpers/schedulers/pause"; -import { IS_TOUCH_SUPPORTED } from "../../../environment/touchSupport"; import appEmojiManager from "../../../lib/appManagers/appEmojiManager"; import appImManager from "../../../lib/appManagers/appImManager"; import Config from "../../../lib/config"; @@ -21,6 +20,8 @@ import { putPreloader } from "../../misc"; import Scrollable from "../../scrollable"; import StickyIntersector from "../../stickyIntersector"; import IS_EMOJI_SUPPORTED from "../../../environment/emojiSupport"; +import { IS_TOUCH_SUPPORTED } from "../../../environment/touchSupport"; +import blurActiveElement from "../../../helpers/dom/blurActiveElement"; const loadedURLs: Set = new Set(); export function appendEmoji(emoji: string, container: HTMLElement, prepend = false, unify = false) { @@ -290,47 +291,10 @@ export default class EmojiTab implements EmoticonsTab { return; } - const messageInput = appImManager.chat.input.messageInput; - let inputHTML = messageInput.innerHTML; - - const html = RichTextProcessor.wrapEmojiText(emoji, true); - let inserted = false; - if(window.getSelection) { - const savedRange = IS_TOUCH_SUPPORTED ? undefined : emoticonsDropdown.getSavedRange(); - let sel = window.getSelection(); - if(savedRange) { - sel.removeAllRanges(); - sel.addRange(savedRange); - } - - if(sel.getRangeAt && sel.rangeCount) { - var el = document.createElement('div'); - el.innerHTML = html; - var node = el.firstChild; - var range = sel.getRangeAt(0); - range.deleteContents(); - //range.insertNode(document.createTextNode(' ')); - range.insertNode(node); - range.setStart(node, 0); - inserted = true; - - setTimeout(() => { - range = document.createRange(); - range.setStartAfter(node); - range.collapse(true); - sel.removeAllRanges(); - sel.addRange(range); - }, 0); - } + appImManager.chat.input.onEmojiSelected(emoji, false); + if(IS_TOUCH_SUPPORTED) { + blurActiveElement(); } - - if(!inserted || messageInput.innerHTML === inputHTML) { - messageInput.insertAdjacentHTML('beforeend', html); - } - - // Append to input - const event = new Event('input', {bubbles: true, cancelable: true}); - messageInput.dispatchEvent(event); }; onClose() { diff --git a/src/helpers/dom/getRichValueWithCaret.ts b/src/helpers/dom/getRichValueWithCaret.ts index ebd91eb4..f714ff3d 100644 --- a/src/helpers/dom/getRichValueWithCaret.ts +++ b/src/helpers/dom/getRichValueWithCaret.ts @@ -22,11 +22,21 @@ export default function getRichValueWithCaret(field: HTMLElement, withEntities = let selOffset: number; if(sel && sel.rangeCount) { const range = sel.getRangeAt(0); - if(range.startContainer && + const startOffset = range.startOffset; + if( + range.startContainer && range.startContainer == range.endContainer && - range.startOffset == range.endOffset) { - selNode = range.startContainer; - selOffset = range.startOffset; + startOffset == range.endOffset + ) { + // * if focused on img + const possibleChildrenFocusOffset = startOffset - 1; + if(range.startContainer === field && field.childNodes[possibleChildrenFocusOffset]) { + selNode = field.childNodes[possibleChildrenFocusOffset]; + selOffset = 1; + } else { + selNode = range.startContainer; + selOffset = startOffset; + } } } diff --git a/src/helpers/dom/setCaretAt.ts b/src/helpers/dom/setCaretAt.ts new file mode 100644 index 00000000..ee995154 --- /dev/null +++ b/src/helpers/dom/setCaretAt.ts @@ -0,0 +1,33 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +export default function setCaretAt(node: Node) { + // node.appendChild(document.createTextNode('')); + + const originalNode = node; + node = node.previousSibling; + + if(node.nodeType === 1) { + const newNode = document.createTextNode(''); + node.parentNode.insertBefore(newNode, !originalNode.nextSibling || originalNode.nextSibling.nodeType === node.nodeType ? originalNode : originalNode.nextSibling); + node = newNode; + } + + if(window.getSelection && document.createRange) { + const range = document.createRange(); + if(node) { + range.setStartAfter(node); + range.insertNode(node); + range.setStart(node, node.nodeValue.length); + } + + range.collapse(true); + + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } +} diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index 45eafc33..a68bdbfc 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -403,13 +403,14 @@ namespace RichTextProcessor { currentEntities.push(...filtered); currentEntities.sort((a, b) => a.offset - b.offset); + // currentEntities.sort((a, b) => (a.offset - b.offset) || (a._ === 'messageEntityCaret' && -1)); if(!IS_EMOJI_SUPPORTED) { // fix splitted emoji. messageEntityTextUrl can split the emoji if starts before its end (e.g. on fe0f) for(let i = 0; i < currentEntities.length; ++i) { const entity = currentEntities[i]; if(entity._ === 'messageEntityEmoji') { const nextEntity = currentEntities[i + 1]; - if(nextEntity && nextEntity.offset < (entity.offset + entity.length)) { + if(nextEntity /* && nextEntity._ !== 'messageEntityCaret' */ && nextEntity.offset < (entity.offset + entity.length)) { entity.length = nextEntity.offset - entity.offset; } } @@ -460,7 +461,8 @@ namespace RichTextProcessor { const lol: { part: string, - offset: number + offset: number, + // priority: number }[] = []; const entities = options.entities || parseEntities(text); @@ -468,14 +470,16 @@ namespace RichTextProcessor { const contextSite = options.contextSite || 'Telegram'; const contextExternal = contextSite !== 'Telegram'; - const insertPart = (entity: MessageEntity, startPart: string, endPart?: string) => { - lol.push({part: startPart, offset: entity.offset}); + const insertPart = (entity: MessageEntity, startPart: string, endPart?: string/* , priority = 0 */) => { + lol.push({part: startPart, offset: entity.offset/* , priority */}); if(endPart) { - lol.unshift({part: endPart, offset: entity.offset + entity.length}); + lol.push({part: endPart, offset: entity.offset + entity.length/* , priority */}); } }; + const pushPartsAfterSort: typeof lol = []; + for(let i = 0, length = entities.length; i < length; ++i) { const entity = entities[i]; switch(entity._) { @@ -579,9 +583,9 @@ namespace RichTextProcessor { //} else if(options.mustWrapEmoji) { } else if(!options.wrappingDraft) { insertPart(entity, '', ''); - } else if(!IS_SAFARI) { + }/* else if(!IS_SAFARI) { insertPart(entity, '', ''); - } + } */ /* if(!IS_EMOJI_SUPPORTED) { insertPart(entity, ``, ``); } */ @@ -590,7 +594,12 @@ namespace RichTextProcessor { } case 'messageEntityCaret': { - insertPart(entity, ''); + const html = ''; + // const html = ''; + // insertPart(entity, ''); + // insertPart(entity, ''); + pushPartsAfterSort.push({part: html, offset: entity.offset}); + // insertPart(entity, html/* , undefined, 1 */); break; } @@ -700,11 +709,28 @@ namespace RichTextProcessor { } } - lol.sort((a, b) => a.offset - b.offset); + // lol.sort((a, b) => (a.offset - b.offset) || (a.priority - b.priority)); + lol.sort((a, b) => a.offset - b.offset); // have to sort because of nested entities + + let partsLength = lol.length, partsAfterSortLength = pushPartsAfterSort.length; + for(let i = 0; i < partsAfterSortLength; ++i) { + const part = pushPartsAfterSort[i]; + let insertAt = 0; + while(insertAt < partsLength) { + if(lol[insertAt++].offset >= part.offset) { + break; + } + } + + lol.splice(insertAt, 0, part); + } + + partsLength += partsAfterSortLength; const arr: string[] = []; let usedLength = 0; - for(const {part, offset} of lol) { + for(let i = 0; i < partsLength; ++i) { + const {part, offset} = lol[i]; if(offset > usedLength) { arr.push(encodeEntities(text.slice(usedLength, offset))); usedLength = offset; diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 5d8237c1..e0a74293 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -159,8 +159,12 @@ $chat-helper-size: 36px; content: $tgico-smile; } + html.is-ios &:before { + content: $tgico-stickers; + } + &.flip-icon:before { - content: $tgico-keyboard; + content: $tgico-keyboard !important; } } diff --git a/src/scss/partials/_emojiDropdown.scss b/src/scss/partials/_emojiDropdown.scss index cc920903..47b307a5 100644 --- a/src/scss/partials/_emojiDropdown.scss +++ b/src/scss/partials/_emojiDropdown.scss @@ -253,6 +253,7 @@ padding: 0; height: 48px; max-width: 100%; + position: relative; } .menu-horizontal-div-item {