Commit 08ad0af4 authored by Tim Zallmann's avatar Tim Zallmann

Merge branch 'refactor-emoji-utils' into 'master'

Refactor emoji helpers in preparation for async loading

See merge request !12432
parents a7f114b1 88e12ac9
...@@ -2,11 +2,7 @@ ...@@ -2,11 +2,7 @@
/* global Flash */ /* global Flash */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import * as Emoji from './emoji';
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from './behaviors/gl_emoji';
import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
...@@ -17,8 +13,6 @@ const requestAnimationFrame = window.requestAnimationFrame || ...@@ -17,8 +13,6 @@ const requestAnimationFrame = window.requestAnimationFrame ||
const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
let categoryMap = null;
const categoryLabelMap = { const categoryLabelMap = {
activity: 'Activity', activity: 'Activity',
people: 'People', people: 'People',
...@@ -30,26 +24,6 @@ const categoryLabelMap = { ...@@ -30,26 +24,6 @@ const categoryLabelMap = {
flags: 'Flags', flags: 'Flags',
}; };
function buildCategoryMap() {
return Object.keys(emojiMap).reduce((currentCategoryMap, emojiNameKey) => {
const emojiInfo = emojiMap[emojiNameKey];
if (currentCategoryMap[emojiInfo.category]) {
currentCategoryMap[emojiInfo.category].push(emojiNameKey);
}
return currentCategoryMap;
}, {
activity: [],
people: [],
nature: [],
food: [],
travel: [],
objects: [],
symbols: [],
flags: [],
});
}
function renderCategory(name, emojiList, opts = {}) { function renderCategory(name, emojiList, opts = {}) {
return ` return `
<h5 class="emoji-menu-title"> <h5 class="emoji-menu-title">
...@@ -59,7 +33,7 @@ function renderCategory(name, emojiList, opts = {}) { ...@@ -59,7 +33,7 @@ function renderCategory(name, emojiList, opts = {}) {
${emojiList.map(emojiName => ` ${emojiList.map(emojiName => `
<li class="emoji-menu-list-item"> <li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button"> <button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${glEmojiTag(emojiName, { ${Emoji.glEmojiTag(emojiName, {
sprite: true, sprite: true,
})} })}
</button> </button>
...@@ -72,7 +46,6 @@ function renderCategory(name, emojiList, opts = {}) { ...@@ -72,7 +46,6 @@ function renderCategory(name, emojiList, opts = {}) {
export default class AwardsHandler { export default class AwardsHandler {
constructor() { constructor() {
this.eventListeners = []; this.eventListeners = [];
this.aliases = emojiAliases;
// If the user shows intent let's pre-build the menu // If the user shows intent let's pre-build the menu
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
const $menu = $('.emoji-menu'); const $menu = $('.emoji-menu');
...@@ -81,8 +54,6 @@ export default class AwardsHandler { ...@@ -81,8 +54,6 @@ export default class AwardsHandler {
this.createEmojiMenu(); this.createEmojiMenu();
}); });
} }
// Prebuild the categoryMap
categoryMap = categoryMap || buildCategoryMap();
}); });
this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => { this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
e.stopPropagation(); e.stopPropagation();
...@@ -168,7 +139,7 @@ export default class AwardsHandler { ...@@ -168,7 +139,7 @@ export default class AwardsHandler {
this.isCreatingEmojiMenu = true; this.isCreatingEmojiMenu = true;
// Render the first category // Render the first category
categoryMap = categoryMap || buildCategoryMap(); const categoryMap = Emoji.getEmojiCategoryMap();
const categoryNameKey = Object.keys(categoryMap)[0]; const categoryNameKey = Object.keys(categoryMap)[0];
const emojisInCategory = categoryMap[categoryNameKey]; const emojisInCategory = categoryMap[categoryNameKey];
const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
...@@ -208,7 +179,7 @@ export default class AwardsHandler { ...@@ -208,7 +179,7 @@ export default class AwardsHandler {
} }
this.isAddingRemainingEmojiMenuCategories = true; this.isAddingRemainingEmojiMenuCategories = true;
categoryMap = categoryMap || buildCategoryMap(); const categoryMap = Emoji.getEmojiCategoryMap();
// Avoid the jank and render the remaining categories separately // Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive // This will take more time, but makes UI more responsive
...@@ -262,14 +233,8 @@ export default class AwardsHandler { ...@@ -262,14 +233,8 @@ export default class AwardsHandler {
return $menu.css(css); return $menu.css(css);
} }
addAward( addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
votesBlock, const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
awardUrl,
emoji,
checkMutuality,
callback,
) {
const normalizedEmoji = this.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
...@@ -279,16 +244,12 @@ export default class AwardsHandler { ...@@ -279,16 +244,12 @@ export default class AwardsHandler {
$('.js-add-award.is-active').removeClass('is-active'); $('.js-add-award.is-active').removeClass('is-active');
} }
addAwardToEmojiBar( addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
votesBlock,
emoji,
checkForMutuality,
) {
if (checkForMutuality || checkForMutuality === null) { if (checkForMutuality || checkForMutuality === null) {
this.checkMutuality(votesBlock, emoji); this.checkMutuality(votesBlock, emoji);
} }
this.addEmojiToFrequentlyUsedList(emoji); this.addEmojiToFrequentlyUsedList(emoji);
const normalizedEmoji = this.normalizeEmojiName(emoji); const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
if ($emojiButton.length > 0) { if ($emojiButton.length > 0) {
if (this.isActive($emojiButton)) { if (this.isActive($emojiButton)) {
...@@ -413,7 +374,7 @@ export default class AwardsHandler { ...@@ -413,7 +374,7 @@ export default class AwardsHandler {
createAwardButtonForVotesBlock(votesBlock, emojiName) { createAwardButtonForVotesBlock(votesBlock, emojiName) {
const buttonHtml = ` const buttonHtml = `
<button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom"> <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
${glEmojiTag(emojiName)} ${Emoji.glEmojiTag(emojiName)}
<span class="award-control-text js-counter">1</span> <span class="award-control-text js-counter">1</span>
</button> </button>
`; `;
...@@ -478,12 +439,8 @@ export default class AwardsHandler { ...@@ -478,12 +439,8 @@ export default class AwardsHandler {
return $('body, html').animate(options, 200); return $('body, html').animate(options, 200);
} }
normalizeEmojiName(emoji) {
return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji;
}
addEmojiToFrequentlyUsedList(emoji) { addEmojiToFrequentlyUsedList(emoji) {
if (isEmojiNameValid(emoji)) { if (Emoji.isEmojiNameValid(emoji)) {
this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji)); this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 }); Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
} }
...@@ -493,7 +450,7 @@ export default class AwardsHandler { ...@@ -493,7 +450,7 @@ export default class AwardsHandler {
return this.frequentlyUsedEmojis || (() => { return this.frequentlyUsedEmojis || (() => {
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
inputName => isEmojiNameValid(inputName), inputName => Emoji.isEmojiNameValid(inputName),
); );
return this.frequentlyUsedEmojis; return this.frequentlyUsedEmojis;
...@@ -535,21 +492,11 @@ export default class AwardsHandler { ...@@ -535,21 +492,11 @@ export default class AwardsHandler {
} }
} }
findMatchingEmojiElements(term) { findMatchingEmojiElements(query) {
const safeTerm = term.toLowerCase(); const emojiMatches = Emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const namesMatchingAlias = []; const $matchingElements = $emojiElements
Object.keys(emojiAliases).forEach((alias) => { .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
if (alias.indexOf(safeTerm) >= 0) {
namesMatchingAlias.push(emojiAliases[alias]);
}
});
const $matchingElements = namesMatchingAlias.concat(safeTerm)
.reduce(
($result, searchTerm) =>
$result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)),
$([]),
);
return $matchingElements.closest('li').clone(); return $matchingElements.closest('li').clone();
} }
......
import installCustomElements from 'document-register-element'; import installCustomElements from 'document-register-element';
import emojiMap from 'emojis/digests.json'; import { emojiImageTag, emojiFallbackImageSrc } from '../emoji';
import emojiAliases from 'emojis/aliases.json'; import isEmojiUnicodeSupported from '../emoji/support';
import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
installCustomElements(window); installCustomElements(window);
const generatedUnicodeSupportMap = getUnicodeSupportMap(); export default function installGlEmojiElement() {
function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
function assembleFallbackImageSrc(inputName) {
let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`;
return fallbackImageSrc;
}
const glEmojiTagDefaults = {
sprite: false,
forceFallback: false,
};
function glEmojiTag(inputName, options) {
const opts = Object.assign({}, glEmojiTagDefaults, options);
let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = assembleFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return `
<gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
>
${contents}
</gl-emoji>
`;
}
function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype); const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() { GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim(); const emojiUnicode = this.textContent.trim();
...@@ -90,7 +25,7 @@ function installGlEmojiElement() { ...@@ -90,7 +25,7 @@ function installGlEmojiElement() {
if ( if (
emojiUnicode && emojiUnicode &&
isEmojiUnicode && isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) { ) {
// CSS sprite fallback takes precedence over image fallback // CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) { if (hasCssSpriteFalback) {
...@@ -100,7 +35,7 @@ function installGlEmojiElement() { ...@@ -100,7 +35,7 @@ function installGlEmojiElement() {
} else if (hasImageFallback) { } else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc); this.innerHTML = emojiImageTag(name, fallbackSrc);
} else { } else {
const src = assembleFallbackImageSrc(name); const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src); this.innerHTML = emojiImageTag(name, src);
} }
} }
...@@ -110,9 +45,3 @@ function installGlEmojiElement() { ...@@ -110,9 +45,3 @@ function installGlEmojiElement() {
prototype: GlEmojiElementProto, prototype: GlEmojiElementProto,
}); });
} }
export {
installGlEmojiElement,
glEmojiTag,
emojiImageTag,
};
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
function isEmojiNameValid(inputName) {
const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
return name && emojiMap[name];
}
export default isEmojiNameValid;
import './autosize'; import './autosize';
import './bind_in_out'; import './bind_in_out';
import './details_behavior'; import './details_behavior';
import { installGlEmojiElement } from './gl_emoji'; import installGlEmojiElement from './gl_emoji';
import './quick_submit'; import './quick_submit';
import './requires_input'; import './requires_input';
import './toggler_behavior'; import './toggler_behavior';
......
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
export const validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
export function normalizeEmojiName(name) {
return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name;
}
export function isEmojiNameValid(name) {
return validEmojiNames.indexOf(name) >= 0;
}
export function filterEmojiNames(filter) {
const match = filter.toLowerCase();
return validEmojiNames.filter(name => name.indexOf(match) >= 0);
}
export function filterEmojiNamesByAlias(filter) {
return _.uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name)));
}
let emojiCategoryMap;
export function getEmojiCategoryMap() {
if (!emojiCategoryMap) {
emojiCategoryMap = {
activity: [],
people: [],
nature: [],
food: [],
travel: [],
objects: [],
symbols: [],
flags: [],
};
Object.keys(emojiMap).forEach((name) => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name);
}
});
}
return emojiCategoryMap;
}
export function getEmojiInfo(query) {
let name = normalizeEmojiName(query);
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
return { ...emojiInfo, name };
}
export function emojiFallbackImageSrc(inputName) {
const { name, digest } = getEmojiInfo(inputName);
return `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${digest}.png`;
}
export function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
export function glEmojiTag(inputName, options) {
const opts = { sprite: false, forceFallback: false, ...options };
const { name, ...emojiInfo } = getEmojiInfo(inputName);
const fallbackImageSrc = emojiFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return `
<gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
>
${contents}
</gl-emoji>
`;
}
import isEmojiUnicodeSupported from './is_emoji_unicode_supported';
import getUnicodeSupportMap from './unicode_support_map';
// cache browser support map between calls
let browserUnicodeSupportMap;
export default function isEmojiUnicodeSupportedByBrowser(emojiUnicode, unicodeVersion) {
browserUnicodeSupportMap = browserUnicodeSupportMap || getUnicodeSupportMap();
return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion);
}
...@@ -111,7 +111,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe ...@@ -111,7 +111,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
} }
export { export {
isEmojiUnicodeSupported, isEmojiUnicodeSupported as default,
isFlagEmoji, isFlagEmoji,
isKeycapEmoji, isKeycapEmoji,
isSkinToneComboEmoji, isSkinToneComboEmoji,
......
...@@ -140,7 +140,7 @@ function generateUnicodeSupportMap(testMap) { ...@@ -140,7 +140,7 @@ function generateUnicodeSupportMap(testMap) {
return resultMap; return resultMap;
} }
function getUnicodeSupportMap() { export default function getUnicodeSupportMap() {
let unicodeSupportMap; let unicodeSupportMap;
let userAgentFromCache; let userAgentFromCache;
...@@ -165,8 +165,3 @@ function getUnicodeSupportMap() { ...@@ -165,8 +165,3 @@ function getUnicodeSupportMap() {
return unicodeSupportMap; return unicodeSupportMap;
} }
export {
getUnicodeSupportMap,
generateUnicodeSupportMap,
};
import emojiMap from 'emojis/digests.json'; import { validEmojiNames, glEmojiTag } from './emoji';
import emojiAliases from 'emojis/aliases.json'; import glRegexp from './lib/utils/regexp';
import { glEmojiTag } from '~/behaviors/gl_emoji'; import AjaxCache from './lib/utils/ajax_cache';
import glRegexp from '~/lib/utils/regexp';
import AjaxCache from '~/lib/utils/ajax_cache';
function sanitize(str) { function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, ''); return str.replace(/<(?:.|\n)*?>/gm, '');
...@@ -375,7 +373,7 @@ class GfmAutoComplete { ...@@ -375,7 +373,7 @@ class GfmAutoComplete {
if (this.cachedData[at]) { if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); this.loadData($input, at, validEmojiNames);
} else { } else {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => { .then((data) => {
......
import { getUnicodeSupportMap } from '~/behaviors/gl_emoji/unicode_support_map'; import getUnicodeSupportMap from '~/emoji/support/unicode_support_map';
import AccessorUtilities from '~/lib/utils/accessor'; import AccessorUtilities from '~/lib/utils/accessor';
describe('Unicode Support Map', () => { describe('Unicode Support Map', () => {
......
import { glEmojiTag } from '~/behaviors/gl_emoji'; import { glEmojiTag } from '~/emoji';
import { import isEmojiUnicodeSupported, {
isEmojiUnicodeSupported,
isFlagEmoji, isFlagEmoji,
isKeycapEmoji, isKeycapEmoji,
isSkinToneComboEmoji, isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji, isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji, isPersonZwjEmoji,
} from '~/behaviors/gl_emoji/is_emoji_unicode_supported'; } from '~/emoji/support/is_emoji_unicode_supported';
const emptySupportMap = { const emptySupportMap = {
personZwj: false, personZwj: false,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment