Commit 77c47389 authored by Tim Zallmann's avatar Tim Zallmann Committed by Clement Ho

Optimize Emoji Sprite Handling

parent c7c9f38d
...@@ -7,27 +7,24 @@ export default function installGlEmojiElement() { ...@@ -7,27 +7,24 @@ export default 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();
const { const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
name,
unicodeVersion,
fallbackSrc,
fallbackSpriteClass,
} = this.dataset;
const isEmojiUnicode = this.childNodes && Array.prototype.every.call( const isEmojiUnicode =
this.childNodes, this.childNodes &&
childNode => childNode.nodeType === 3, Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3);
);
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0; const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if ( if (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) {
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) {
// CSS sprite fallback takes precedence over image fallback // CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) { if (hasCssSpriteFalback) {
if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
const emojiSpriteLinkTag = document.createElement('link');
emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
document.head.appendChild(emojiSpriteLinkTag);
gon.emoji_sprites_css_added = true;
}
// IE 11 doesn't like adding multiple at once :( // IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon'); this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass); this.classList.add(fallbackSpriteClass);
......
...@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() { ...@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() {
symbols: [], symbols: [],
flags: [], flags: [],
}; };
Object.keys(emojiMap).forEach((name) => { Object.keys(emojiMap).forEach(name => {
const emoji = emojiMap[name]; const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) { if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name); emojiCategoryMap[emoji.category].push(name);
...@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) { ...@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) {
classList.push(fallbackSpriteClass); classList.push(fallbackSpriteClass);
} }
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : ''; const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : ''; const fallbackSpriteAttribute = opts.sprite
? `data-fallback-sprite-class="${fallbackSpriteClass}"`
: '';
let contents = emojiInfo.moji; let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) { if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc); contents = emojiImageTag(name, fallbackImageSrc);
......
...@@ -54,7 +54,8 @@ const unicodeSupportTestMap = { ...@@ -54,7 +54,8 @@ const unicodeSupportTestMap = {
function checkPixelInImageDataArray(pixelOffset, imageDataArray) { function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA // `4 *` because RGBA
const indexOffset = 4 * pixelOffset; const indexOffset = 4 * pixelOffset;
const hasColor = imageDataArray[indexOffset + 0] || const hasColor =
imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] || imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2]; imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3]; const isVisible = imageDataArray[indexOffset + 3];
...@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche ...@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
const fontSize = 16; const fontSize = 16;
function generateUnicodeSupportMap(testMap) { function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap); const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys const numTestEntries = testMapKeys.reduce((list, testKey) => list.concat(testMap[testKey]), [])
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length; .length;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas; (window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
canvas.width = (2 * fontSize); canvas.width = 2 * fontSize;
canvas.height = (numTestEntries * fontSize); canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000'; ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically // Write each emoji to the canvas vertically
let writeIndex = 0; let writeIndex = 0;
testMapKeys.forEach((testKey) => { testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey]; const testEntry = testMap[testKey];
[].concat(testEntry).forEach((emojiUnicode) => { [].concat(testEntry).forEach(emojiUnicode => {
ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2)); ctx.fillText(emojiUnicode, 0, writeIndex * fontSize + fontSize / 2);
writeIndex += 1; writeIndex += 1;
}); });
}); });
...@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) { ...@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) {
// Read from the canvas // Read from the canvas
const resultMap = {}; const resultMap = {};
let readIndex = 0; let readIndex = 0;
testMapKeys.forEach((testKey) => { testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey]; const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to // This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries // keep the `readIndex` in sync from the writes by running all entries
const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => { const isTestSatisfied = [].concat(testEntry).reduce(isSatisfied => {
// Sample along the vertical-middle for a couple of characters // Sample along the vertical-middle for a couple of characters
const imageData = ctx.getImageData( const imageData = ctx.getImageData(0, readIndex * fontSize + fontSize / 2, 2 * fontSize, 1)
0, .data;
(readIndex * fontSize) + (fontSize / 2),
2 * fontSize,
1,
).data;
let isValidEmoji = false; let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) { for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize; const isLookingAtFirstChar = currentPixel < fontSize;
const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2)); const isLookingAtSecondChar = currentPixel >= fontSize + fontSize / 2;
// Check for the emoji somewhere along the row // Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) { if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true; isValidEmoji = true;
// Check to see that nothing is rendered next to the first character // Check to see that nothing is rendered next to the first character
// to ensure that the ZWJ sequence rendered as one piece // to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) { } else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false; isValidEmoji = false;
break; break;
...@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() { ...@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() {
if (isLocalStorageAvailable) { if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION); window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); window.localStorage.setItem(
'gl-emoji-unicode-support-map',
JSON.stringify(unicodeSupportMap),
);
} }
} }
......
This diff is collapsed.
@import "framework/variables"; @import 'framework/variables';
@import "framework/mixins"; @import 'framework/mixins';
@import 'framework/tw_bootstrap_variables'; @import 'framework/tw_bootstrap_variables';
@import 'framework/tw_bootstrap'; @import 'framework/tw_bootstrap';
@import "framework/layout"; @import 'framework/layout';
@import "framework/animations"; @import 'framework/animations';
@import "framework/vue_transitions"; @import 'framework/vue_transitions';
@import "framework/avatar"; @import 'framework/avatar';
@import "framework/asciidoctor"; @import 'framework/asciidoctor';
@import "framework/banner"; @import 'framework/banner';
@import "framework/blocks"; @import 'framework/blocks';
@import "framework/buttons"; @import 'framework/buttons';
@import "framework/badges"; @import 'framework/badges';
@import "framework/calendar"; @import 'framework/calendar';
@import "framework/callout"; @import 'framework/callout';
@import "framework/common"; @import 'framework/common';
@import "framework/dropdowns"; @import 'framework/dropdowns';
@import "framework/files"; @import 'framework/files';
@import "framework/filters"; @import 'framework/filters';
@import "framework/flash"; @import 'framework/flash';
@import "framework/forms"; @import 'framework/forms';
@import "framework/gfm"; @import 'framework/gfm';
@import "framework/gitlab_theme"; @import 'framework/gitlab_theme';
@import "framework/header"; @import 'framework/header';
@import "framework/highlight"; @import 'framework/highlight';
@import "framework/issue_box"; @import 'framework/issue_box';
@import "framework/jquery"; @import 'framework/jquery';
@import "framework/lists"; @import 'framework/lists';
@import "framework/logo"; @import 'framework/logo';
@import "framework/markdown_area"; @import 'framework/markdown_area';
@import "framework/media_object"; @import 'framework/media_object';
@import "framework/mobile"; @import 'framework/mobile';
@import "framework/modal"; @import 'framework/modal';
@import "framework/pagination"; @import 'framework/pagination';
@import "framework/panels"; @import 'framework/panels';
@import "framework/popup"; @import 'framework/popup';
@import "framework/secondary_navigation_elements"; @import 'framework/secondary_navigation_elements';
@import "framework/selects"; @import 'framework/selects';
@import "framework/sidebar"; @import 'framework/sidebar';
@import "framework/contextual_sidebar"; @import 'framework/contextual_sidebar';
@import "framework/tables"; @import 'framework/tables';
@import "framework/notes"; @import 'framework/notes';
@import "framework/tabs"; @import 'framework/tabs';
@import "framework/timeline"; @import 'framework/timeline';
@import "framework/tooltips"; @import 'framework/tooltips';
@import "framework/toggle"; @import 'framework/toggle';
@import "framework/typography"; @import 'framework/typography';
@import "framework/zen"; @import 'framework/zen';
@import "framework/blank"; @import 'framework/blank';
@import "framework/wells"; @import 'framework/wells';
@import "framework/page_header"; @import 'framework/page_header';
@import "framework/awards"; @import 'framework/awards';
@import "framework/images"; @import 'framework/images';
@import "framework/broadcast_messages"; @import 'framework/broadcast_messages';
@import "framework/emojis"; @import 'framework/emojis';
@import "framework/emoji_sprites"; @import 'framework/icons';
@import "framework/icons"; @import 'framework/snippets';
@import "framework/snippets"; @import 'framework/memory_graph';
@import "framework/memory_graph"; @import 'framework/responsive_tables';
@import "framework/responsive_tables"; @import 'framework/stacked_progress_bar';
@import "framework/stacked_progress_bar"; @import 'framework/ci_variable_list';
@import "framework/ci_variable_list"; @import 'framework/feature_highlight';
@import "framework/feature_highlight";
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
...@@ -115,6 +115,7 @@ module Gitlab ...@@ -115,6 +115,7 @@ module Gitlab
config.assets.precompile << "test.css" config.assets.precompile << "test.css"
config.assets.precompile << "snippets.css" config.assets.precompile << "snippets.css"
config.assets.precompile << "locale/**/app.js" config.assets.precompile << "locale/**/app.js"
config.assets.precompile << "emoji_sprites.css"
# Import gitlab-svgs directly from vendored directory # Import gitlab-svgs directly from vendored directory
config.assets.paths << "#{config.root}/node_modules/@gitlab-org/gitlab-svgs/dist" config.assets.paths << "#{config.root}/node_modules/@gitlab-org/gitlab-svgs/dist"
......
...@@ -19,6 +19,7 @@ module Gitlab ...@@ -19,6 +19,7 @@ module Gitlab
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png') gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
gon.sprite_icons = IconsHelper.sprite_icon_path gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
gon.emoji_sprites_css_path = ActionController::Base.helpers.stylesheet_path('emoji_sprites')
gon.test_env = Rails.env.test? gon.test_env = Rails.env.test?
gon.suggested_label_colors = LabelsHelper.suggested_colors gon.suggested_label_colors = LabelsHelper.suggested_colors
......
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