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

Refactor emoji helpers in preparation for async loading

See merge request !12432
This commit is contained in:
Tim Zallmann 2017-06-28 05:51:30 +00:00
commit 08ad0af49c
11 changed files with 141 additions and 175 deletions

View file

@ -2,11 +2,7 @@
/* global Flash */
import Cookies from 'js-cookie';
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';
import * as Emoji from './emoji';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
@ -17,8 +13,6 @@ const requestAnimationFrame = window.requestAnimationFrame ||
const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
let categoryMap = null;
const categoryLabelMap = {
activity: 'Activity',
people: 'People',
@ -30,26 +24,6 @@ const categoryLabelMap = {
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 = {}) {
return `
<h5 class="emoji-menu-title">
@ -59,7 +33,7 @@ function renderCategory(name, emojiList, opts = {}) {
${emojiList.map(emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${glEmojiTag(emojiName, {
${Emoji.glEmojiTag(emojiName, {
sprite: true,
})}
</button>
@ -72,7 +46,6 @@ function renderCategory(name, emojiList, opts = {}) {
export default class AwardsHandler {
constructor() {
this.eventListeners = [];
this.aliases = emojiAliases;
// If the user shows intent let's pre-build the menu
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
const $menu = $('.emoji-menu');
@ -81,8 +54,6 @@ export default class AwardsHandler {
this.createEmojiMenu();
});
}
// Prebuild the categoryMap
categoryMap = categoryMap || buildCategoryMap();
});
this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
e.stopPropagation();
@ -168,7 +139,7 @@ export default class AwardsHandler {
this.isCreatingEmojiMenu = true;
// Render the first category
categoryMap = categoryMap || buildCategoryMap();
const categoryMap = Emoji.getEmojiCategoryMap();
const categoryNameKey = Object.keys(categoryMap)[0];
const emojisInCategory = categoryMap[categoryNameKey];
const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
@ -208,7 +179,7 @@ export default class AwardsHandler {
}
this.isAddingRemainingEmojiMenuCategories = true;
categoryMap = categoryMap || buildCategoryMap();
const categoryMap = Emoji.getEmojiCategoryMap();
// Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive
@ -262,14 +233,8 @@ export default class AwardsHandler {
return $menu.css(css);
}
addAward(
votesBlock,
awardUrl,
emoji,
checkMutuality,
callback,
) {
const normalizedEmoji = this.normalizeEmojiName(emoji);
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
@ -279,16 +244,12 @@ export default class AwardsHandler {
$('.js-add-award.is-active').removeClass('is-active');
}
addAwardToEmojiBar(
votesBlock,
emoji,
checkForMutuality,
) {
addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
if (checkForMutuality || checkForMutuality === null) {
this.checkMutuality(votesBlock, emoji);
}
this.addEmojiToFrequentlyUsedList(emoji);
const normalizedEmoji = this.normalizeEmojiName(emoji);
const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
if ($emojiButton.length > 0) {
if (this.isActive($emojiButton)) {
@ -413,7 +374,7 @@ export default class AwardsHandler {
createAwardButtonForVotesBlock(votesBlock, emojiName) {
const buttonHtml = `
<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>
</button>
`;
@ -478,12 +439,8 @@ export default class AwardsHandler {
return $('body, html').animate(options, 200);
}
normalizeEmojiName(emoji) {
return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji;
}
addEmojiToFrequentlyUsedList(emoji) {
if (isEmojiNameValid(emoji)) {
if (Emoji.isEmojiNameValid(emoji)) {
this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
}
@ -493,7 +450,7 @@ export default class AwardsHandler {
return this.frequentlyUsedEmojis || (() => {
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
inputName => isEmojiNameValid(inputName),
inputName => Emoji.isEmojiNameValid(inputName),
);
return this.frequentlyUsedEmojis;
@ -535,21 +492,11 @@ export default class AwardsHandler {
}
}
findMatchingEmojiElements(term) {
const safeTerm = term.toLowerCase();
const namesMatchingAlias = [];
Object.keys(emojiAliases).forEach((alias) => {
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}"]`)),
$([]),
);
findMatchingEmojiElements(query) {
const emojiMatches = Emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements
.filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
return $matchingElements.closest('li').clone();
}

View file

@ -1,75 +1,10 @@
import installCustomElements from 'document-register-element';
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
import { emojiImageTag, emojiFallbackImageSrc } from '../emoji';
import isEmojiUnicodeSupported from '../emoji/support';
installCustomElements(window);
const generatedUnicodeSupportMap = getUnicodeSupportMap();
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() {
export default function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim();
@ -90,7 +25,7 @@ function installGlEmojiElement() {
if (
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) {
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) {
@ -100,7 +35,7 @@ function installGlEmojiElement() {
} else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else {
const src = assembleFallbackImageSrc(name);
const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src);
}
}
@ -110,9 +45,3 @@ function installGlEmojiElement() {
prototype: GlEmojiElementProto,
});
}
export {
installGlEmojiElement,
glEmojiTag,
emojiImageTag,
};

View file

@ -1,11 +0,0 @@
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;

View file

@ -1,7 +1,7 @@
import './autosize';
import './bind_in_out';
import './details_behavior';
import { installGlEmojiElement } from './gl_emoji';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
import './requires_input';
import './toggler_behavior';

View file

@ -0,0 +1,99 @@
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>
`;
}

View file

@ -0,0 +1,10 @@
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);
}

View file

@ -111,7 +111,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
}
export {
isEmojiUnicodeSupported,
isEmojiUnicodeSupported as default,
isFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,

View file

@ -140,7 +140,7 @@ function generateUnicodeSupportMap(testMap) {
return resultMap;
}
function getUnicodeSupportMap() {
export default function getUnicodeSupportMap() {
let unicodeSupportMap;
let userAgentFromCache;
@ -165,8 +165,3 @@ function getUnicodeSupportMap() {
return unicodeSupportMap;
}
export {
getUnicodeSupportMap,
generateUnicodeSupportMap,
};

View file

@ -1,8 +1,6 @@
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji';
import glRegexp from '~/lib/utils/regexp';
import AjaxCache from '~/lib/utils/ajax_cache';
import { validEmojiNames, glEmojiTag } from './emoji';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
@ -375,7 +373,7 @@ class GfmAutoComplete {
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
this.loadData($input, at, validEmojiNames);
} else {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => {

View file

@ -1,4 +1,4 @@
import { getUnicodeSupportMap } from '~/behaviors/gl_emoji/unicode_support_map';
import getUnicodeSupportMap from '~/emoji/support/unicode_support_map';
import AccessorUtilities from '~/lib/utils/accessor';
describe('Unicode Support Map', () => {

View file

@ -1,12 +1,11 @@
import { glEmojiTag } from '~/behaviors/gl_emoji';
import {
isEmojiUnicodeSupported,
import { glEmojiTag } from '~/emoji';
import isEmojiUnicodeSupported, {
isFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
} from '~/behaviors/gl_emoji/is_emoji_unicode_supported';
} from '~/emoji/support/is_emoji_unicode_supported';
const emptySupportMap = {
personZwj: false,