Browse Source

Save entities for message edit

Support underline entity
Support markdown in message edit
Support markdown for messageEntityTextUrl
master
Eduard Kuzmenko 4 years ago
parent
commit
3cc7d874bf
  1. 6
      src/components/chat/input.ts
  2. 2
      src/components/popupCreatePoll.ts
  3. 108
      src/helpers/dom.ts
  4. 156
      src/lib/richtextprocessor.ts
  5. 5
      src/scss/partials/_chatBubble.scss

6
src/components/chat/input.ts

@ -565,7 +565,7 @@ export class ChatInput {
//let str = this.serializeNodes(Array.from(this.messageInput.childNodes)); //let str = this.serializeNodes(Array.from(this.messageInput.childNodes));
let str = getRichValue(this.messageInput); let str = getRichValue(this.messageInput);
//console.log('childnode str after:', str/* , getRichValue(this.messageInput) */); console.log('childnode str after:', str/* , getRichValue(this.messageInput) */);
//return; //return;
@ -611,7 +611,7 @@ export class ChatInput {
public initMessageEditing(mid: number) { public initMessageEditing(mid: number) {
const message = appMessagesManager.getMessage(mid); const message = appMessagesManager.getMessage(mid);
let input = message.message; let input = RichTextProcessor.wrapDraftText(message.message, {entities: message.totalEntities});
const f = () => { const f = () => {
this.setTopInfo('edit', f, 'Editing', message.message, input, message); this.setTopInfo('edit', f, 'Editing', message.message, input, message);
this.editMsgID = mid; this.editMsgID = mid;
@ -690,7 +690,7 @@ export class ChatInput {
} */ } */
if(input !== undefined) { if(input !== undefined) {
this.messageInput.innerHTML = input ? RichTextProcessor.wrapRichText(input, {noLinks: true}) : ''; this.messageInput.innerHTML = input || '';
} }
setTimeout(() => { setTimeout(() => {

2
src/components/popupCreatePoll.ts

@ -130,7 +130,7 @@ export default class PopupCreatePoll extends PopupElement {
private getFilledAnswers() { private getFilledAnswers() {
const answers = Array.from(this.questions.children).map((el, idx) => { 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); return getRichValue(input);
}).filter(v => !!v.trim()); }).filter(v => !!v.trim());

108
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 { /* export function isInDOM(element: Element, parentNode?: HTMLElement): boolean {
if(!element) { if(!element) {
return false; return false;
@ -45,24 +47,6 @@ export function cancelEvent(event: Event) {
return false; 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) { export function placeCaretAtEnd(el: HTMLElement) {
el.focus(); el.focus();
if(typeof window.getSelection != "undefined" && typeof document.createRange != "undefined") { 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(node.nodeType == 3) { // TEXT
if(selNode === node) { if(selNode === node) {
var value = node.nodeValue const value = node.nodeValue;
line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset)) line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset));
} else { } 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;
} }
return
markdown = markdownTag.markdown;
} }
if (node.nodeType != 1) { // NON-ELEMENT
return
} }
var isSelected = (selNode === node)
var isBlock = node.tagName == 'DIV' || node.tagName == 'P' line.push(markdown && node.nodeValue.trim() ? markdown + node.nodeValue + markdown : node.nodeValue);
var curChild }
return;
}
if(node.nodeType != 1) { // NON-ELEMENT
return;
}
const isSelected = (selNode === node);
const isBlock = node.tagName == 'DIV' || node.tagName == 'P';
if(isBlock && line.length || node.tagName == 'BR') { if(isBlock && line.length || node.tagName == 'BR') {
lines.push(line.join('')) lines.push(line.join(''));
line.splice(0, line.length) line.splice(0, line.length);
} else if(node.tagName == 'IMG') { } else if(node.tagName == 'IMG') {
if(node.alt) { if((node as HTMLImageElement).alt) {
line.push(node.alt); line.push((node as HTMLImageElement).alt);
} }
} }
@ -111,10 +151,10 @@ export function getRichElementValue(node: any, lines: string[], line: string[],
line.push('\x01'); line.push('\x01');
} }
var curChild = node.firstChild; let curChild = node.firstChild as HTMLElement;
while(curChild) { while(curChild) {
getRichElementValue(curChild, lines, line, selNode, selOffset); getRichElementValue(curChild, lines, line, selNode, selOffset);
curChild = curChild.nextSibling; curChild = curChild.nextSibling as any;
} }
if(isSelected && selOffset) { if(isSelected && selOffset) {

156
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 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 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 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 markdownTestRegExp = /[`_*@~]/;
const markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s)(`|\*\*|__)([^\n]+?)\7([\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)/m; const markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s)(`|~~|\*\*|__)([^\n]+?)\7([\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)|(\[(.+?)\]\((.+?)\))/m;
const siteHashtags: {[siteName: string]: string} = { const siteHashtags: {[siteName: string]: string} = {
Telegram: 'tg://search_hashtag?hashtag={1}', Telegram: 'tg://search_hashtag?hashtag={1}',
Twitter: 'https://twitter.com/hashtag/{1}', Twitter: 'https://twitter.com/hashtag/{1}',
@ -82,8 +82,10 @@ const siteMentions: {[siteName: string]: string} = {
}; };
const markdownEntities = { const markdownEntities = {
'`': 'messageEntityCode', '`': 'messageEntityCode',
'``': 'messageEntityPre',
'**': 'messageEntityBold', '**': 'messageEntityBold',
'__': 'messageEntityItalic' '__': 'messageEntityItalic',
'~~': 'messageEntityStrike'
}; };
namespace RichTextProcessor { namespace RichTextProcessor {
@ -212,71 +214,89 @@ namespace RichTextProcessor {
}) })
} */ } */
export function parseMarkdown(text: string, entities: MessageEntity[], noTrim?: any) { export function parseMarkdown(text: string, entities: MessageEntity[], noTrim?: any): string {
  if(!markdownTestRegExp.test(text)) {   /* if(!markdownTestRegExp.test(text)) {
return noTrim ? text : text.trim(); return noTrim ? text : text.trim();
} } */
var raw = text; var raw = text;
var match; var match;
var newText: any = []; var newText: any = [];
var rawOffset = 0; var rawOffset = 0;
var matchIndex; var matchIndex;
while (match = raw.match(markdownRegExp)) { while(match = raw.match(markdownRegExp)) {
matchIndex = rawOffset + match.index matchIndex = rawOffset + match.index;
newText.push(raw.substr(0, match.index)) newText.push(raw.substr(0, match.index));
var text = (match[3] || match[8] || match[11]) var text = (match[3] || match[8] || match[11] || match[14]);
rawOffset -= text.length rawOffset -= text.length;
text = text.replace(/^\s+|\s+$/g, '') text = text.replace(/^\s+|\s+$/g, '');
rawOffset += text.length rawOffset += text.length;
if (text.match(/^`*$/)) {
newText.push(match[0]) if(text.match(/^`*$/)) {
} newText.push(match[0]);
else if (match[3]) { // pre } else if(match[3]) { // pre
if (match[5] == '\n') { if(match[5] == '\n') {
match[5] = '' match[5] = '';
rawOffset -= 1 rawOffset -= 1;
} }
newText.push(match[1] + text + match[5])
newText.push(match[1] + text + match[5]);
entities.push({ entities.push({
_: 'messageEntityPre', _: 'messageEntityPre',
language: '', language: '',
offset: matchIndex + match[1].length, offset: matchIndex + match[1].length,
length: text.length length: text.length
}) });
rawOffset -= match[2].length + match[4].length
} else if (match[7]) { // code|italic|bold rawOffset -= match[2].length + match[4].length;
newText.push(match[6] + text + match[9]) } else if(match[7]) { // code|italic|bold
newText.push(match[6] + text + match[9]);
entities.push({ entities.push({
// @ts-ignore // @ts-ignore
_: markdownEntities[match[7]], _: markdownEntities[match[7]],
offset: matchIndex + match[6].length, offset: matchIndex + match[6].length,
length: text.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) newText.push(text)
entities.push({ entities.push({
_: 'messageEntityMentionName', _: 'messageEntityMentionName',
user_id: +match[10], user_id: +match[10],
offset: matchIndex, offset: matchIndex,
length: text.length 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('') newText.push(raw);
if (!newText.replace(/\s+/g, '').length) { newText = newText.join('');
newText = text if(!newText.replace(/\s+/g, '').length) {
entities.splice(0, entities.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) { export function mergeEntities(currentEntities: MessageEntity[], newEntities: MessageEntity[], fromApi?: boolean) {
@ -355,6 +375,10 @@ namespace RichTextProcessor {
noCommands: true, noCommands: true,
fromBot: boolean, fromBot: boolean,
noTextFormat: true, noTextFormat: true,
passEntities: Partial<{
[_ in MessageEntity['_']]: true
}>,
nested?: true, nested?: true,
contextHashtag?: string contextHashtag?: string
}> = {}) { }> = {}) {
@ -362,6 +386,7 @@ namespace RichTextProcessor {
return ''; return '';
} }
const passEntities: typeof options.passEntities = options.passEntities || {};
const entities = options.entities || parseEntities(text); const entities = options.entities || parseEntities(text);
const contextSite = options.contextSite || 'Telegram'; const contextSite = options.contextSite || 'Telegram';
const contextExternal = contextSite != 'Telegram'; const contextExternal = contextSite != 'Telegram';
@ -469,7 +494,7 @@ namespace RichTextProcessor {
inner = encodeEntities(replaceUrlEncodings(entityText)); inner = encodeEntities(replaceUrlEncodings(entityText));
} }
if(options.noLinks) { if(options.noLinks && !passEntities[entity._]) {
html.push(inner); html.push(inner);
} else { } else {
html.push( html.push(
@ -559,6 +584,14 @@ namespace RichTextProcessor {
); );
break; break;
case 'messageEntityUnderline':
html.push(
'<u>',
wrapRichNestedText(entityText, entity.nested, options),
'</u>'
);
break;
case 'messageEntityCode': case 'messageEntityCode':
if(options.noTextFormat) { if(options.noTextFormat) {
html.push(encodeEntities(entityText)); html.push(encodeEntities(entityText));
@ -599,7 +632,7 @@ namespace RichTextProcessor {
return text; return text;
} }
export function wrapDraftText(text: string, options: any = {}) { /* export function wrapDraftText(text: string, options: any = {}) {
if(!text || !text.length) { if(!text || !text.length) {
return ''; return '';
} }
@ -673,8 +706,45 @@ namespace RichTextProcessor {
} }
code.push(text.substr(lastOffset)); code.push(text.substr(lastOffset));
return code.join(''); 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) { export function checkBrackets(url: string) {
var urlLength = url.length; var urlLength = url.length;
var urlOpenBrackets = url.split('(').length - 1; var urlOpenBrackets = url.split('(').length - 1;

5
src/scss/partials/_chatBubble.scss

@ -1129,6 +1129,11 @@ $bubble-margin: .25rem;
} }
} }
pre {
display: inline;
margin: 0;
}
.video-play { .video-play {
background-color: var(--message-time-background); background-color: var(--message-time-background);
color: #fff; color: #fff;

Loading…
Cancel
Save