From 3cc7d874bf651e29b72a29f1141e2e52d664f0da Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Sat, 14 Nov 2020 04:27:13 +0200 Subject: [PATCH] Save entities for message edit Support underline entity Support markdown in message edit Support markdown for messageEntityTextUrl --- src/components/chat/input.ts | 6 +- src/components/popupCreatePoll.ts | 2 +- src/helpers/dom.ts | 108 +++++++++++++------- src/lib/richtextprocessor.ts | 154 +++++++++++++++++++++-------- src/scss/partials/_chatBubble.scss | 5 + 5 files changed, 195 insertions(+), 80 deletions(-) diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index b2e161ab..d3beb474 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -565,7 +565,7 @@ export class ChatInput { //let str = this.serializeNodes(Array.from(this.messageInput.childNodes)); let str = getRichValue(this.messageInput); - //console.log('childnode str after:', str/* , getRichValue(this.messageInput) */); + console.log('childnode str after:', str/* , getRichValue(this.messageInput) */); //return; @@ -611,7 +611,7 @@ export class ChatInput { public initMessageEditing(mid: number) { const message = appMessagesManager.getMessage(mid); - let input = message.message; + let input = RichTextProcessor.wrapDraftText(message.message, {entities: message.totalEntities}); const f = () => { this.setTopInfo('edit', f, 'Editing', message.message, input, message); this.editMsgID = mid; @@ -690,7 +690,7 @@ export class ChatInput { } */ if(input !== undefined) { - this.messageInput.innerHTML = input ? RichTextProcessor.wrapRichText(input, {noLinks: true}) : ''; + this.messageInput.innerHTML = input || ''; } setTimeout(() => { diff --git a/src/components/popupCreatePoll.ts b/src/components/popupCreatePoll.ts index 92b77690..70f4ba90 100644 --- a/src/components/popupCreatePoll.ts +++ b/src/components/popupCreatePoll.ts @@ -130,7 +130,7 @@ export default class PopupCreatePoll extends PopupElement { private getFilledAnswers() { const answers = Array.from(this.questions.children).map((el, idx) => { - const input = el.querySelector('.input-field-input'); + const input = el.querySelector('.input-field-input') as HTMLElement; return getRichValue(input); }).filter(v => !!v.trim()); diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index 3ce752b8..8f1988df 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -1,3 +1,5 @@ +import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config"; + /* export function isInDOM(element: Element, parentNode?: HTMLElement): boolean { if(!element) { return false; @@ -45,24 +47,6 @@ export function cancelEvent(event: Event) { return false; } -export function getRichValue(field: any) { - if(!field) { - return ''; - } - var lines: string[] = []; - var line: string[] = []; - - getRichElementValue(field, lines, line); - if (line.length) { - lines.push(line.join('')); - } - - var value = lines.join('\n'); - value = value.replace(/\u00A0/g, ' '); - - return value; -} - export function placeCaretAtEnd(el: HTMLElement) { el.focus(); if(typeof window.getSelection != "undefined" && typeof document.createRange != "undefined") { @@ -82,28 +66,84 @@ export function placeCaretAtEnd(el: HTMLElement) { } } -export function getRichElementValue(node: any, lines: string[], line: string[], selNode?: Node, selOffset?: number) { +export function getRichValue(field: HTMLElement) { + if(!field) { + return ''; + } + + const lines: string[] = []; + const line: string[] = []; + + getRichElementValue(field, lines, line); + if(line.length) { + lines.push(line.join('')); + } + + let value = lines.join('\n'); + value = value.replace(/\u00A0/g, ' '); + + return value; +} + +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.getRichValue = getRichValue); + +const markdownTags = [{ + tagName: 'STRONG', + markdown: '**' +}, { + tagName: 'EM', + markdown: '__' +}, { + tagName: 'CODE', + markdown: '`' +}, { + tagName: 'PRE', + markdown: '``' +}, { + tagName: 'DEL', + markdown: '~~' +}, { + tagName: 'A', + markdown: (node: HTMLElement) => `[${(node.parentElement as HTMLAnchorElement).href}](${node.nodeValue})` +}]; +export function getRichElementValue(node: HTMLElement, lines: string[], line: string[], selNode?: Node, selOffset?: number) { if(node.nodeType == 3) { // TEXT if(selNode === node) { - var value = node.nodeValue - line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset)) + const value = node.nodeValue; + line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset)); } else { - line.push(node.nodeValue) + let markdown: string; + if(node.parentNode) { + const tagName = node.parentElement.tagName; + const markdownTag = markdownTags.find(m => m.tagName == tagName); + if(markdownTag) { + if(typeof(markdownTag.markdown) === 'function') { + line.push(markdownTag.markdown(node)); + return; + } + + markdown = markdownTag.markdown; + } + } + + line.push(markdown && node.nodeValue.trim() ? markdown + node.nodeValue + markdown : node.nodeValue); } - return + + return; } - if (node.nodeType != 1) { // NON-ELEMENT - return + + if(node.nodeType != 1) { // NON-ELEMENT + return; } - var isSelected = (selNode === node) - var isBlock = node.tagName == 'DIV' || node.tagName == 'P' - var curChild + + const isSelected = (selNode === node); + const isBlock = node.tagName == 'DIV' || node.tagName == 'P'; if(isBlock && line.length || node.tagName == 'BR') { - lines.push(line.join('')) - line.splice(0, line.length) + lines.push(line.join('')); + line.splice(0, line.length); } else if(node.tagName == 'IMG') { - if(node.alt) { - line.push(node.alt); + if((node as HTMLImageElement).alt) { + line.push((node as HTMLImageElement).alt); } } @@ -111,10 +151,10 @@ export function getRichElementValue(node: any, lines: string[], line: string[], line.push('\x01'); } - var curChild = node.firstChild; + let curChild = node.firstChild as HTMLElement; while(curChild) { getRichElementValue(curChild, lines, line, selNode, selOffset); - curChild = curChild.nextSibling; + curChild = curChild.nextSibling as any; } if(isSelected && selOffset) { diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index 8dda9dd3..1841aa01 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -65,8 +65,8 @@ const usernameRegExp = '[a-zA-Z\\d_]{5,32}'; const botCommandRegExp = '\\/([a-zA-Z\\d_]{1,32})(?:@(' + usernameRegExp + '))?(\\b|$)'; const fullRegExp = new RegExp('(^| )(@)(' + usernameRegExp + ')|(' + urlRegExp + ')|(\\n)|(' + emojiRegExp + ')|(^|[\\s\\(\\]])(#[' + alphaNumericRegExp + ']{2,64})|(^|\\s)' + botCommandRegExp, 'i') const emailRegExp = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -const markdownTestRegExp = /[`_*@]/; -const markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s)(`|\*\*|__)([^\n]+?)\7([\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)/m; +//const markdownTestRegExp = /[`_*@~]/; +const markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s)(`|~~|\*\*|__)([^\n]+?)\7([\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)|(\[(.+?)\]\((.+?)\))/m; const siteHashtags: {[siteName: string]: string} = { Telegram: 'tg://search_hashtag?hashtag={1}', Twitter: 'https://twitter.com/hashtag/{1}', @@ -82,8 +82,10 @@ const siteMentions: {[siteName: string]: string} = { }; const markdownEntities = { '`': 'messageEntityCode', + '``': 'messageEntityPre', '**': 'messageEntityBold', - '__': 'messageEntityItalic' + '__': 'messageEntityItalic', + '~~': 'messageEntityStrike' }; namespace RichTextProcessor { @@ -212,71 +214,89 @@ namespace RichTextProcessor { }) } */ - export function parseMarkdown(text: string, entities: MessageEntity[], noTrim?: any) { -   if(!markdownTestRegExp.test(text)) { + export function parseMarkdown(text: string, entities: MessageEntity[], noTrim?: any): string { +   /* if(!markdownTestRegExp.test(text)) { return noTrim ? text : text.trim(); - } + } */ var raw = text; var match; var newText: any = []; var rawOffset = 0; var matchIndex; - while (match = raw.match(markdownRegExp)) { - matchIndex = rawOffset + match.index - newText.push(raw.substr(0, match.index)) - var text = (match[3] || match[8] || match[11]) - rawOffset -= text.length - text = text.replace(/^\s+|\s+$/g, '') - rawOffset += text.length - if (text.match(/^`*$/)) { - newText.push(match[0]) - } - else if (match[3]) { // pre - if (match[5] == '\n') { - match[5] = '' - rawOffset -= 1 + while(match = raw.match(markdownRegExp)) { + matchIndex = rawOffset + match.index; + newText.push(raw.substr(0, match.index)); + var text = (match[3] || match[8] || match[11] || match[14]); + rawOffset -= text.length; + text = text.replace(/^\s+|\s+$/g, ''); + rawOffset += text.length; + + if(text.match(/^`*$/)) { + newText.push(match[0]); + } else if(match[3]) { // pre + if(match[5] == '\n') { + match[5] = ''; + rawOffset -= 1; } - newText.push(match[1] + text + match[5]) + + newText.push(match[1] + text + match[5]); entities.push({ _: 'messageEntityPre', language: '', offset: matchIndex + match[1].length, length: text.length - }) - rawOffset -= match[2].length + match[4].length - } else if (match[7]) { // code|italic|bold - newText.push(match[6] + text + match[9]) + }); + + rawOffset -= match[2].length + match[4].length; + } else if(match[7]) { // code|italic|bold + newText.push(match[6] + text + match[9]); entities.push({ // @ts-ignore _: markdownEntities[match[7]], offset: matchIndex + match[6].length, length: text.length - }) - rawOffset -= match[7].length * 2 - } else if (match[11]) { // custom mention + }); + + rawOffset -= match[7].length * 2; + } else if(match[11]) { // custom mention newText.push(text) entities.push({ _: 'messageEntityMentionName', user_id: +match[10], offset: matchIndex, length: text.length - }) - rawOffset -= match[0].length - text.length + }); + + rawOffset -= match[0].length - text.length; + } else if(match[12]) { // text url + newText.push(text); + entities.push({ + _: 'messageEntityTextUrl', + url: match[13], + offset: matchIndex, + length: text.length + }); + + rawOffset -= match[12].length - text.length; } - raw = raw.substr(match.index + match[0].length) - rawOffset += match.index + match[0].length + + raw = raw.substr(match.index + match[0].length); + rawOffset += match.index + match[0].length; } - newText.push(raw) - newText = newText.join('') - if (!newText.replace(/\s+/g, '').length) { - newText = text - entities.splice(0, entities.length) + + newText.push(raw); + newText = newText.join(''); + if(!newText.replace(/\s+/g, '').length) { + newText = text; + entities.splice(0, entities.length); } - if (!entities.length && !noTrim) { - newText = newText.trim() + + if(!entities.length && !noTrim) { + newText = newText.trim(); } - return newText + + return newText; } export function mergeEntities(currentEntities: MessageEntity[], newEntities: MessageEntity[], fromApi?: boolean) { @@ -355,6 +375,10 @@ namespace RichTextProcessor { noCommands: true, fromBot: boolean, noTextFormat: true, + passEntities: Partial<{ + [_ in MessageEntity['_']]: true + }>, + nested?: true, contextHashtag?: string }> = {}) { @@ -362,6 +386,7 @@ namespace RichTextProcessor { return ''; } + const passEntities: typeof options.passEntities = options.passEntities || {}; const entities = options.entities || parseEntities(text); const contextSite = options.contextSite || 'Telegram'; const contextExternal = contextSite != 'Telegram'; @@ -469,7 +494,7 @@ namespace RichTextProcessor { inner = encodeEntities(replaceUrlEncodings(entityText)); } - if(options.noLinks) { + if(options.noLinks && !passEntities[entity._]) { html.push(inner); } else { html.push( @@ -559,6 +584,14 @@ namespace RichTextProcessor { ); break; + case 'messageEntityUnderline': + html.push( + '', + wrapRichNestedText(entityText, entity.nested, options), + '' + ); + break; + case 'messageEntityCode': if(options.noTextFormat) { html.push(encodeEntities(entityText)); @@ -599,7 +632,7 @@ namespace RichTextProcessor { return text; } - export function wrapDraftText(text: string, options: any = {}) { + /* export function wrapDraftText(text: string, options: any = {}) { if(!text || !text.length) { return ''; } @@ -673,8 +706,45 @@ namespace RichTextProcessor { } code.push(text.substr(lastOffset)); return code.join(''); + } */ + + export function wrapDraftText(text: string, options: Partial<{ + entities: MessageEntity[] + }> = {}) { + return wrapRichText(text, { + ...options, + noLinks: true, + passEntities: { + messageEntityTextUrl: true + } + }); } + //const draftEntityTypes: MessageEntity['_'][] = (['messageEntityTextUrl', 'messageEntityEmoji'] as MessageEntity['_'][]).concat(Object.values(markdownEntities) as any); + /* const draftEntityTypes: Partial<{[_ in MessageEntity['_']]: true}> = { + messageEntityCode: true, + messageEntityPre: true, + messageEntityBold: true, + messageEntityItalic: true, + messageEntityStrike: true, + messageEntityEmoji: true, + messageEntityLinebreak: true, + messageEntityUnderline: true, + messageEntityTextUrl: true + }; + export function wrapDraftText(text: string, options: Partial<{ + entities: MessageEntity[] + }> = {}) { + const checkEntity = (entity: MessageEntity) => { + return draftEntityTypes[entity._]; + }; + const entities = options.entities ? options.entities.filter(entity => { + return draftEntityTypes[entity._]; + }) : []; + + return wrapRichText(text, {entities}); + } */ + export function checkBrackets(url: string) { var urlLength = url.length; var urlOpenBrackets = url.split('(').length - 1; diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index ec5128c4..dce0e65d 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -1129,6 +1129,11 @@ $bubble-margin: .25rem; } } + pre { + display: inline; + margin: 0; + } + .video-play { background-color: var(--message-time-background); color: #fff;