Merge branch 'refactor-emoji-utils' into 'master'
Refactor emoji helpers in preparation for async loading See merge request !12432
This commit is contained in:
commit
08ad0af49c
11 changed files with 141 additions and 175 deletions
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
99
app/assets/javascripts/emoji/index.js
Normal file
99
app/assets/javascripts/emoji/index.js
Normal 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>
|
||||
`;
|
||||
}
|
10
app/assets/javascripts/emoji/support/index.js
Normal file
10
app/assets/javascripts/emoji/support/index.js
Normal 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);
|
||||
}
|
|
@ -111,7 +111,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
|
|||
}
|
||||
|
||||
export {
|
||||
isEmojiUnicodeSupported,
|
||||
isEmojiUnicodeSupported as default,
|
||||
isFlagEmoji,
|
||||
isKeycapEmoji,
|
||||
isSkinToneComboEmoji,
|
|
@ -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,
|
||||
};
|
|
@ -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) => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
Loading…
Reference in a new issue