2021-05-28 16:17:46 +03:00

303 lines
9.2 KiB
TypeScript

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import emoticonsDropdown, { EmoticonsDropdown, EmoticonsTab } from "..";
import { cancelEvent } from "../../../helpers/dom/cancelEvent";
import findUpClassName from "../../../helpers/dom/findUpClassName";
import { fastRaf, pause } from "../../../helpers/schedulers";
import { isTouchSupported } from "../../../helpers/touchSupport";
import appEmojiManager from "../../../lib/appManagers/appEmojiManager";
import appImManager from "../../../lib/appManagers/appImManager";
import Config from "../../../lib/config";
import { i18n, LangPackKey } from "../../../lib/langPack";
import { RichTextProcessor } from "../../../lib/richtextprocessor";
import rootScope from "../../../lib/rootScope";
import { putPreloader } from "../../misc";
import Scrollable from "../../scrollable";
import StickyIntersector from "../../stickyIntersector";
const loadedURLs: Set<string> = new Set();
export function appendEmoji(emoji: string, container: HTMLElement, prepend = false, unify = false) {
//const emoji = details.unified;
//const emoji = (details.unified as string).split('-')
//.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
const spanEmoji = document.createElement('span');
spanEmoji.classList.add('super-emoji');
let kek: string;
if(unify) {
kek = RichTextProcessor.wrapSingleEmoji(emoji);
} else {
emoji = RichTextProcessor.fixEmoji(emoji);
kek = RichTextProcessor.wrapEmojiText(emoji);
}
/* if(!kek.includes('emoji')) {
console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji), emojiUnicode(emoji));
return;
} */
//console.log(kek);
spanEmoji.innerHTML = kek;
if(spanEmoji.children.length > 1) {
const first = spanEmoji.firstElementChild;
spanEmoji.innerHTML = '';
spanEmoji.append(first);
}
if(spanEmoji.firstElementChild && !RichTextProcessor.emojiSupported) {
const image = spanEmoji.firstElementChild as HTMLImageElement;
image.setAttribute('loading', 'lazy');
const url = image.src;
if(!loadedURLs.has(url)) {
const placeholder = document.createElement('span');
placeholder.classList.add('emoji-placeholder');
if(rootScope.settings.animationsEnabled) {
image.style.opacity = '0';
placeholder.style.opacity = '1';
}
image.addEventListener('load', () => {
fastRaf(() => {
if(rootScope.settings.animationsEnabled) {
image.style.opacity = '';
placeholder.style.opacity = '';
}
spanEmoji.classList.remove('empty');
loadedURLs.add(url);
});
}, {once: true});
spanEmoji.append(placeholder);
}
}
//spanEmoji = spanEmoji.firstElementChild as HTMLSpanElement;
//spanEmoji.setAttribute('emoji', emoji);
if(prepend) container.prepend(spanEmoji);
else container.appendChild(spanEmoji);
}
export function getEmojiFromElement(element: HTMLElement) {
if(element.nodeType === 3) return element.nodeValue;
if(element.tagName === 'SPAN' && !element.classList.contains('emoji') && element.firstElementChild) {
element = element.firstElementChild as HTMLElement;
}
return element.getAttribute('alt') || element.innerText;
}
export default class EmojiTab implements EmoticonsTab {
private content: HTMLElement;
private recentItemsDiv: HTMLElement;
private scroll: Scrollable;
private stickyIntersector: StickyIntersector;
init() {
this.content = document.getElementById('content-emoji') as HTMLDivElement;
const categories: LangPackKey[] = [
'Emoji.SmilesAndPeople',
'Emoji.AnimalsAndNature',
'Emoji.FoodAndDrink',
'Emoji.TravelAndPlaces',
'Emoji.ActivityAndSport',
'Emoji.Objects',
/* 'Emoji.Symbols', */
'Emoji.Flags',
'Skin Tones' as any
];
const divs: {
[category in LangPackKey]?: HTMLDivElement
} = {};
const sorted: Map<LangPackKey, string[]> = new Map([
[
'Emoji.Recent',
[]
]
]);
for(const emoji in Config.Emoji) {
const details = Config.Emoji[emoji];
const i = '' + details;
const category = categories[+i[0] - 1];
if(!category) continue; // maybe it's skin tones
let s = sorted.get(category);
if(!s) {
s = [];
sorted.set(category, s);
}
s[+i.slice(1) || 0] = emoji;
}
//console.log('emoticons sorted:', sorted);
//Object.keys(sorted).forEach(c => sorted[c].sort((a, b) => a - b));
sorted.delete(categories.pop());
//console.time('emojiParse');
sorted.forEach((emojis, category) => {
const div = document.createElement('div');
div.classList.add('emoji-category');
const titleDiv = document.createElement('div');
titleDiv.classList.add('category-title');
titleDiv.append(i18n(category));
const itemsDiv = document.createElement('div');
itemsDiv.classList.add('super-emojis');
div.append(titleDiv, itemsDiv);
emojis.forEach(unified => {
/* if(emojiUnicode(emoji) === '1f481-200d-2642') {
console.log('append emoji', emoji, emojiUnicode(emoji));
} */
let emoji = unified.split('-').reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), '');
//if(emoji.includes('🕵')) {
//console.log('toCodePoints', toCodePoints(emoji));
//emoji = emoji.replace(/(\u200d[\u2640\u2642\u2695])(?!\ufe0f)/, '\ufe0f$1');
// const zwjIndex = emoji.indexOf('\u200d');
// if(zwjIndex !== -1 && !emoji.includes('\ufe0f')) {
// /* if(zwjIndex !== (emoji.length - 1)) {
// emoji = emoji.replace(/(\u200d)/g, '\ufe0f$1');
// } */
// emoji += '\ufe0f';
// //emoji += '\ufe0f';
// }
//debugger;
//}
appendEmoji(emoji/* .replace(/[\ufe0f\u2640\u2642\u2695]/g, '') */, itemsDiv, false/* , false */);
/* if(category === 'Smileys & Emotion') {
console.log('appended emoji', emoji, itemsDiv.children[itemsDiv.childElementCount - 1].innerHTML, emojiUnicode(emoji));
} */
});
divs[category] = div;
});
//console.timeEnd('emojiParse');
const menu = this.content.previousElementSibling as HTMLElement;
const emojiScroll = this.scroll = new Scrollable(this.content, 'EMOJI');
//emojiScroll.setVirtualContainer(emojiScroll.container);
const preloader = putPreloader(this.content, true);
Promise.all([
pause(200),
appEmojiManager.getRecentEmojis()
]).then(([_, recent]) => {
preloader.remove();
this.recentItemsDiv = divs['Emoji.Recent'].querySelector('.super-emojis');
for(const emoji of recent) {
appendEmoji(emoji, this.recentItemsDiv);
}
this.recentItemsDiv.parentElement.classList.toggle('hide', !this.recentItemsDiv.childElementCount);
categories.unshift('Emoji.Recent');
categories.map(category => {
const div = divs[category];
if(!div) {
console.error('no div by category:', category);
}
emojiScroll.container.append(div);
this.stickyIntersector.observeStickyHeaderChanges(div);
return div;
});
});
this.content.addEventListener('click', this.onContentClick);
this.stickyIntersector = EmoticonsDropdown.menuOnClick(menu, emojiScroll);
this.init = null;
}
onContentClick = (e: MouseEvent) => {
cancelEvent(e);
let target = e.target as HTMLElement;
//if(target.tagName !== 'SPAN') return;
if(target.tagName === 'SPAN' && !target.classList.contains('emoji')) {
target = findUpClassName(target, 'super-emoji');
if(!target) {
return;
}
target = target.firstChild as HTMLElement;
} else if(target.tagName === 'DIV') return;
// set selection range
const savedRange = isTouchSupported ? undefined : emoticonsDropdown.getSavedRange();
let sel: Selection;
if(savedRange) {
sel = document.getSelection();
sel.removeAllRanges();
sel.addRange(savedRange);
}
const html = RichTextProcessor.emojiSupported ?
(target.nodeType === 3 ? target.nodeValue : target.innerHTML) :
target.outerHTML;
if((document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.hasAttribute('contenteditable'))) ||
savedRange) {
document.execCommand('insertHTML', true, html);
} else {
appImManager.chat.input.messageInput.innerHTML += html;
}
/* if(sel && isTouchSupported) {
sel.removeRange(savedRange);
blurActiveElement();
} */
// Recent
const emoji = getEmojiFromElement(target);
(Array.from(this.recentItemsDiv.children) as HTMLElement[]).forEach((el, idx) => {
const _emoji = getEmojiFromElement(el);
if(emoji === _emoji) {
el.remove();
}
});
appendEmoji(emoji, this.recentItemsDiv, true);
appEmojiManager.pushRecentEmoji(emoji);
this.recentItemsDiv.parentElement.classList.remove('hide');
// Append to input
const event = new Event('input', {bubbles: true, cancelable: true});
appImManager.chat.input.messageInput.dispatchEvent(event);
};
onClose() {
}
}