mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 17:55:07 +08:00
226be994da
The emoji-picker is a specific piece of code as it has very strong performance requirements which are almost not found anywhere else in the app, as a result it was using various hacks to make it work decently even on old browsers. Following our drop of Internet Explorer, and various new features in Ember and recent browsers we can now take advantage of this to reduce the amount of code needed, this rewrite most importantly does the following: - use loading="lazy" preventing the full list of emojis to be loaded on opening - uses InterserctionObserver to find the active section - limits the use of native event listentes only for hover/click emojis (for performance reason we track click on the whole emoji area and delegate events), everything else is using ember events - uses popper to position the emoji picker - no jquery code
258 lines
6.1 KiB
JavaScript
258 lines
6.1 KiB
JavaScript
import {
|
|
emojis,
|
|
aliases,
|
|
searchAliases,
|
|
translations,
|
|
tonableEmojis,
|
|
replacements
|
|
} from "pretty-text/emoji/data";
|
|
import { IMAGE_VERSION } from "pretty-text/emoji/version";
|
|
|
|
const extendedEmoji = {};
|
|
|
|
export function registerEmoji(code, url, group) {
|
|
code = code.toLowerCase();
|
|
extendedEmoji[code] = { url, group };
|
|
}
|
|
|
|
export function extendedEmojiList() {
|
|
return extendedEmoji;
|
|
}
|
|
|
|
const emojiHash = {};
|
|
|
|
export function buildReplacementsList(emojiReplacements) {
|
|
return Object.keys(emojiReplacements)
|
|
.sort()
|
|
.reverse()
|
|
.map(emoji => {
|
|
return emoji
|
|
.split("")
|
|
.map(chr => {
|
|
return (
|
|
"\\u" +
|
|
chr
|
|
.charCodeAt(0)
|
|
.toString(16)
|
|
.padStart(4, "0")
|
|
);
|
|
})
|
|
.join("");
|
|
})
|
|
.join("|");
|
|
}
|
|
|
|
let replacementListCache;
|
|
const unicodeRegexpCache = {};
|
|
|
|
function replacementList() {
|
|
if (replacementListCache === undefined) {
|
|
replacementListCache = buildReplacementsList(replacements);
|
|
}
|
|
|
|
return replacementListCache;
|
|
}
|
|
|
|
function unicodeRegexp(inlineEmoji) {
|
|
if (unicodeRegexpCache[inlineEmoji] === undefined) {
|
|
const emojiExpression = inlineEmoji
|
|
? "|:[^\\s:]+(?::t\\d)?:?"
|
|
: "|\\B:[^\\s:]+(?::t\\d)?:?\\B";
|
|
|
|
unicodeRegexpCache[inlineEmoji] = new RegExp(
|
|
replacementList() + emojiExpression,
|
|
"g"
|
|
);
|
|
}
|
|
|
|
return unicodeRegexpCache[inlineEmoji];
|
|
}
|
|
|
|
// add all default emojis
|
|
emojis.forEach(code => (emojiHash[code] = true));
|
|
|
|
// and their aliases
|
|
const aliasHash = {};
|
|
Object.keys(aliases).forEach(name => {
|
|
aliases[name].forEach(alias => (aliasHash[alias] = name));
|
|
});
|
|
|
|
function isReplacableInlineEmoji(string, index, inlineEmoji) {
|
|
if (inlineEmoji) return true;
|
|
|
|
// index depends on regex; when `inlineEmoji` is false, the regex starts
|
|
// with a `\B` character, so there's no need to subtract from the index
|
|
const beforeEmoji = string.slice(0, index);
|
|
|
|
return (
|
|
beforeEmoji.length === 0 ||
|
|
/(?:\s|[>.,\/#!$%^&*;:{}=\-_`~()])$/.test(beforeEmoji) ||
|
|
new RegExp(`(?:${replacementList()})$`).test(beforeEmoji)
|
|
);
|
|
}
|
|
|
|
export function performEmojiUnescape(string, opts) {
|
|
if (!string) {
|
|
return;
|
|
}
|
|
|
|
const inlineEmoji = opts.inlineEmoji;
|
|
const regexp = unicodeRegexp(inlineEmoji);
|
|
const allTranslations = Object.assign(
|
|
{},
|
|
translations,
|
|
opts.customEmojiTranslation || {}
|
|
);
|
|
|
|
return string.replace(regexp, (m, index) => {
|
|
const isEmoticon = opts.enableEmojiShortcuts && !!allTranslations[m];
|
|
const isUnicodeEmoticon = !!replacements[m];
|
|
let emojiVal;
|
|
if (isEmoticon) {
|
|
emojiVal = allTranslations[m];
|
|
} else if (isUnicodeEmoticon) {
|
|
emojiVal = replacements[m];
|
|
} else {
|
|
emojiVal = m.slice(1, m.length - 1);
|
|
}
|
|
const hasEndingColon = m.lastIndexOf(":") === m.length - 1;
|
|
const url = buildEmojiUrl(emojiVal, opts);
|
|
let classes = isCustomEmoji(emojiVal, opts)
|
|
? "emoji emoji-custom"
|
|
: "emoji";
|
|
|
|
if (opts.class) {
|
|
classes = `${classes} ${opts.class}`;
|
|
}
|
|
|
|
const isReplacable =
|
|
(isEmoticon || hasEndingColon || isUnicodeEmoticon) &&
|
|
isReplacableInlineEmoji(string, index, inlineEmoji);
|
|
|
|
return url && isReplacable
|
|
? `<img src='${url}' ${opts.skipTitle ? "" : `title='${emojiVal}'`} ${
|
|
opts.lazy ? "loading='lazy' " : ""
|
|
}alt='${emojiVal}' class='${classes}'>`
|
|
: m;
|
|
});
|
|
}
|
|
|
|
export function performEmojiEscape(string, opts) {
|
|
const inlineEmoji = opts.inlineEmoji;
|
|
const regexp = unicodeRegexp(inlineEmoji);
|
|
const allTranslations = Object.assign(
|
|
{},
|
|
translations,
|
|
opts.customEmojiTranslation || {}
|
|
);
|
|
|
|
return string.replace(regexp, (m, index) => {
|
|
if (isReplacableInlineEmoji(string, index, inlineEmoji)) {
|
|
if (!!allTranslations[m]) {
|
|
return opts.emojiShortcuts ? `:${allTranslations[m]}:` : m;
|
|
} else if (!!replacements[m]) {
|
|
return `:${replacements[m]}:`;
|
|
}
|
|
}
|
|
|
|
return m;
|
|
});
|
|
}
|
|
|
|
export function isCustomEmoji(code, opts) {
|
|
code = code.toLowerCase();
|
|
if (extendedEmoji.hasOwnProperty(code)) return true;
|
|
if (opts && opts.customEmoji && opts.customEmoji.hasOwnProperty(code))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
export function buildEmojiUrl(code, opts) {
|
|
let url;
|
|
code = String(code).toLowerCase();
|
|
if (extendedEmoji.hasOwnProperty(code)) {
|
|
url = extendedEmoji[code].url;
|
|
}
|
|
|
|
if (opts && opts.customEmoji && opts.customEmoji[code]) {
|
|
url = opts.customEmoji[code].url || opts.customEmoji[code];
|
|
}
|
|
|
|
const noToneMatch = code.match(/([^:]+):?/);
|
|
if (
|
|
noToneMatch &&
|
|
!url &&
|
|
(emojiHash.hasOwnProperty(noToneMatch[1]) ||
|
|
aliasHash.hasOwnProperty(noToneMatch[1]))
|
|
) {
|
|
url = opts.getURL(
|
|
`/images/emoji/${opts.emojiSet}/${code.replace(/:t/, "/")}.png`
|
|
);
|
|
}
|
|
|
|
if (url) {
|
|
url = url + "?v=" + IMAGE_VERSION;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
export function emojiExists(code) {
|
|
code = code.toLowerCase();
|
|
return !!(
|
|
extendedEmoji.hasOwnProperty(code) ||
|
|
emojiHash.hasOwnProperty(code) ||
|
|
aliasHash.hasOwnProperty(code)
|
|
);
|
|
}
|
|
|
|
let toSearch;
|
|
export function emojiSearch(term, options) {
|
|
const maxResults = (options && options["maxResults"]) || -1;
|
|
if (maxResults === 0) {
|
|
return [];
|
|
}
|
|
|
|
toSearch =
|
|
toSearch ||
|
|
_.union(_.keys(emojiHash), _.keys(extendedEmoji), _.keys(aliasHash)).sort();
|
|
|
|
const results = [];
|
|
|
|
function addResult(t) {
|
|
const val = aliasHash[t] || t;
|
|
if (results.indexOf(val) === -1) {
|
|
results.push(val);
|
|
}
|
|
}
|
|
|
|
// if term matches from beginning
|
|
for (let i = 0; i < toSearch.length; i++) {
|
|
const item = toSearch[i];
|
|
if (item.indexOf(term) === 0) addResult(item);
|
|
}
|
|
|
|
if (searchAliases[term]) {
|
|
results.push.apply(results, searchAliases[term]);
|
|
}
|
|
|
|
for (let i = 0; i < toSearch.length; i++) {
|
|
const item = toSearch[i];
|
|
if (item.indexOf(term) > 0) addResult(item);
|
|
}
|
|
|
|
if (maxResults === -1) {
|
|
return results;
|
|
} else {
|
|
return results.slice(0, maxResults);
|
|
}
|
|
}
|
|
|
|
export function isSkinTonableEmoji(term) {
|
|
const match = _.compact(term.split(":"))[0];
|
|
if (match) {
|
|
return tonableEmojis.indexOf(match) !== -1;
|
|
}
|
|
return false;
|
|
}
|