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. 154
      src/lib/richtextprocessor.ts
  5. 5
      src/scss/partials/_chatBubble.scss

6
src/components/chat/input.ts

@ -565,7 +565,7 @@ export class ChatInput { @@ -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 { @@ -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 { @@ -690,7 +690,7 @@ export class ChatInput {
} */
if(input !== undefined) {
this.messageInput.innerHTML = input ? RichTextProcessor.wrapRichText(input, {noLinks: true}) : '';
this.messageInput.innerHTML = input || '';
}
setTimeout(() => {

2
src/components/popupCreatePoll.ts

@ -130,7 +130,7 @@ export default class PopupCreatePoll extends PopupElement { @@ -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());

108
src/helpers/dom.ts

@ -1,3 +1,5 @@ @@ -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) { @@ -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) { @@ -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[], @@ -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) {

154
src/lib/richtextprocessor.ts

@ -65,8 +65,8 @@ const usernameRegExp = '[a-zA-Z\\d_]{5,32}'; @@ -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} = { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -559,6 +584,14 @@ namespace RichTextProcessor {
);
break;
case 'messageEntityUnderline':
html.push(
'<u>',
wrapRichNestedText(entityText, entity.nested, options),
'</u>'
);
break;
case 'messageEntityCode':
if(options.noTextFormat) {
html.push(encodeEntities(entityText));
@ -599,7 +632,7 @@ namespace RichTextProcessor { @@ -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 { @@ -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;

5
src/scss/partials/_chatBubble.scss

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

Loading…
Cancel
Save