UNPKG

emoji-mart-vue-fast

Version:

Customizable Slack-like emoji picker for VueJS, fast version

676 lines (616 loc) 18.1 kB
import { intersect, unifiedToNative } from './index' import { uncompress, buildSearch } from './data' import frequently from './frequently' const SHEET_COLUMNS = 61 const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/ // Skin tones const SKINS = ['1F3FA', '1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF'] /** * Emoji data structure: * { * "compressed": false, * "aliases": { * collision: "boom" * cooking: "fried_egg" * envelope: "email" * face_with_finger_covering_closed_lips: "shushing_face" * ... * }, * "categories": [ { * id: "people", * name: "Smileys & Emotion", * emojis: [ "grinning", "grin", "joy", ... ] * }, { * id: "nature", * name: "Animals & Nature", * emojis: [ "monkey_face", "money", "gorilla", ... ] * }, * ... * ], * "emojis": [ * smiley: { * added_in: "6.0", * emoticons: ["=)", "=-)"], * has_img_apple: true, * has_img_facebook: true, * has_img_google: true, * has_img_twitter: true, * keywords: ["face", "happy", "joy", "haha", ":D", ":)", "smile", "funny"], * name: "Smiling Face with Open Mouth", * non_qualified: undefined, * search: "smiley,smiling,face,with,open,mouth,happy,joy,haha,:d,:),smile,funny,=),=-)", * sheet_x: 30, * sheet_y: 27, * short_names: ["smiley"], * text: ":)", * unified: "1F603", * }, { * +1: { // emoji with skin_variations * ..., // all the regular fields are present * name: "Thumbs Up Sign", * short_names: (2) ["+1", "thumbsup"], * skin_variations: { * 1F3FB: // each variation has additional set of fields: * added_in: "8.0", * has_img_apple: true, * has_img_facebook: true, * has_img_google: true, * has_img_twitter: true, * image: "1f44d-1f3fb.png", * non_qualified: null, * sheet_x: 14, * sheet_y: 50, * unified: "1F44D-1F3FB", * 1F3FB: {…}, * 1F3FC: {…}, * 1F3FD: {…}, * 1F3FE: {…}, * 1F3FF: {…} * }, * ... * }, * a: { // emoji with non_qualified field set * added_in: "6.0", * emoticons: undefined, * has_img_apple: true, * ... * non_qualified: "1F170", * unified: "1F170-FE0F", * }, * ... * ] * } */ /** * Wraps raw jason emoji data, serves as data source for * emoji picker components. * * Usage: * * import data from '../data/all.json' * let index = new EmojiIndex(data) * */ export class EmojiIndex { /** * Constructor. * * @param {object} data - Raw json data, see the structure above. * @param {object} options - additional options, as an object: * @param {Function} emojisToShowFilter - optional, function to filter out * some emojis, function(emoji) { return true|false } * where `emoji` is an raw emoji object, see data.emojis above. * @param {Array} include - optional, a list of category ids to include. * @param {Array} exclude - optional, a list of category ids to exclude. * @param {Array} custom - optional, a list custom emojis, each emoji is * an object, see data.emojis above for examples. */ constructor( data, { emojisToShowFilter, include, exclude, custom, recent, recentLength = 20, } = {}, ) { this._data = uncompress(data) // Callback to exclude specific emojis this._emojisFilter = emojisToShowFilter || null // Categories to include / exclude this._include = include || null this._exclude = exclude || null // Custom emojis this._custom = custom || [] // Recent emojis // TODO: make parameter configurable this._recent = recent || frequently.get(recentLength) this._emojis = {} this._nativeEmojis = {} this._emoticons = {} this._categories = [] this._recentCategory = { id: 'recent', name: 'Recent', emojis: [] } this._customCategory = { id: 'custom', name: 'Custom', emojis: [] } this._searchIndex = {} this.buildIndex() Object.freeze(this) } buildIndex() { let allCategories = this._data.categories if (this._include) { // Remove categories that are not in the include list. allCategories = allCategories.filter((item) => { return this._include.includes(item.id) }) // Sort categories according to the include list. allCategories = allCategories.sort((a, b) => { const indexA = this._include.indexOf(a.id) const indexB = this._include.indexOf(b.id) if (indexA < indexB) { return -1 } if (indexA > indexB) { return 1 } return 0 }) } allCategories.forEach((categoryData) => { if (!this.isCategoryNeeded(categoryData.id)) { return } let category = { id: categoryData.id, name: categoryData.name, emojis: [], } categoryData.emojis.forEach((emojiId) => { let emoji = this.addEmoji(emojiId) if (emoji) { category.emojis.push(emoji) } }) if (category.emojis.length) { this._categories.push(category) } }) if (this.isCategoryNeeded('custom')) { if (this._custom.length > 0) { for (let customEmoji of this._custom) { this.addCustomEmoji(customEmoji) } } if (this._customCategory.emojis.length) { this._categories.push(this._customCategory) } } if (this.isCategoryNeeded('recent')) { if (this._recent.length) { this._recent.map((id) => { for (let customEmoji of this._customCategory.emojis) { if (customEmoji.id === id) { this._recentCategory.emojis.push(customEmoji) return } } if (this.hasEmoji(id)) { this._recentCategory.emojis.push(this.emoji(id)) } return }) } // Add recent category to the top if (this._recentCategory.emojis.length) { this._categories.unshift(this._recentCategory) } } } /** * Find the emoji from the string */ findEmoji(emoji, skin) { // 1. Parse as :emoji_name:skin-tone-xx: let matches = emoji.match(COLONS_REGEX) if (matches) { emoji = matches[1] if (matches[2]) { skin = parseInt(matches[2], 10) } } // 2. Check if the specified emoji is an alias if (this._data.aliases.hasOwnProperty(emoji)) { emoji = this._data.aliases[emoji] } // 3. Check if we have the specified emoji if (this._emojis.hasOwnProperty(emoji)) { let emojiObject = this._emojis[emoji] if (skin) { return emojiObject.getSkin(skin) } return emojiObject } // 4. Check if we have the specified native emoji if (this._nativeEmojis.hasOwnProperty(emoji)) { return this._nativeEmojis[emoji] } return null } categories() { return this._categories } emoji(emojiId) { if (this._data.aliases.hasOwnProperty(emojiId)) { emojiId = this._data.aliases[emojiId] } let emoji = this._emojis[emojiId] if (!emoji) { throw new Error('Can not find emoji by id: ' + emojiId) } return emoji } firstEmoji() { let emoji = this._emojis[Object.keys(this._emojis)[0]] if (!emoji) { throw new Error('Can not get first emoji') } return emoji } hasEmoji(emojiId) { if (this._data.aliases.hasOwnProperty(emojiId)) { emojiId = this._data.aliases[emojiId] } if (this._emojis[emojiId]) { return true } return false } nativeEmoji(unicodeEmoji) { if (this._nativeEmojis.hasOwnProperty(unicodeEmoji)) { return this._nativeEmojis[unicodeEmoji] } return null } search(value, maxResults) { maxResults || (maxResults = 75) if (!value.length) { return null } if (value == '-' || value == '-1') { return [this.emoji('-1')] } let values = value.toLowerCase().split(/[\s|,|\-|_]+/) let allResults = [] if (values.length > 2) { values = [values[0], values[1]] } allResults = values .map((value) => { // Start searchin in the global list of emojis let emojis = this._emojis let currentIndex = this._searchIndex let length = 0 for (let charIndex = 0; charIndex < value.length; charIndex++) { const char = value[charIndex] length++ currentIndex[char] || (currentIndex[char] = {}) currentIndex = currentIndex[char] if (!currentIndex.results) { let scores = {} currentIndex.results = [] currentIndex.emojis = {} for (let emojiId in emojis) { let emoji = emojis[emojiId] // search is a comma-separated string with words, related // to the emoji, for example: // search: "smiley,smiling,face,joy,haha,:d,:),smile,funny,=),=-)", let search = emoji._data.search let sub = value.substr(0, length) let subIndex = search.indexOf(sub) if (subIndex != -1) { let score = subIndex + 1 if (sub == emojiId) score = 0 currentIndex.results.push(emoji) currentIndex.emojis[emojiId] = emoji scores[emojiId] = score } } currentIndex.results.sort((a, b) => { var aScore = scores[a.id], bScore = scores[b.id] return aScore - bScore }) } // continue search in the reduced set of emojis emojis = currentIndex.emojis } return currentIndex.results // The "filter" call removes undefined values from allResults // array, for example, if we have "test " (with trailing space), // we will get "[Array, undefined]" for allResults and after // the "filter" call it will turn into "[Array]" }) .filter((a) => a) var results = null if (allResults.length > 1) { results = intersect.apply(null, allResults) } else if (allResults.length) { results = allResults[0] } else { results = [] } if (results && results.length > maxResults) { results = results.slice(0, maxResults) } return results } addCustomEmoji(customEmoji) { let emojiData = Object.assign({}, customEmoji, { id: customEmoji.short_names[0], custom: true, }) if (!emojiData.search) { emojiData.search = buildSearch(emojiData) } let emoji = new EmojiData(emojiData) this._emojis[emoji.id] = emoji this._customCategory.emojis.push(emoji) return emoji } addEmoji(emojiId) { // We expect the correct emoji id that is present in the emojis data. let data = this._data.emojis[emojiId] if (!this.isEmojiNeeded(data)) { return false } let emoji = new EmojiData(data) this._emojis[emojiId] = emoji if (emoji.native) { this._nativeEmojis[emoji.native] = emoji } if (emoji._skins) { for (let idx in emoji._skins) { let skin = emoji._skins[idx] if (skin.native) { this._nativeEmojis[skin.native] = skin } } } if (emoji.emoticons) { emoji.emoticons.forEach((emoticon) => { if (this._emoticons[emoticon]) { return } this._emoticons[emoticon] = emojiId }) } return emoji } /** * Check if we need to include given category. * * @param {string} category_id - The category id. * @return {boolean} - Whether to include the emoji. */ isCategoryNeeded(category_id) { let isIncluded = this._include && this._include.length ? this._include.indexOf(category_id) > -1 : true let isExcluded = this._exclude && this._exclude.length ? this._exclude.indexOf(category_id) > -1 : false if (!isIncluded || isExcluded) { return false } return true } /** * Check if we need to include given emoji. * * @param {object} emoji - The raw emoji object. * @return {boolean} - Whether to include the emoji. */ isEmojiNeeded(emoji) { if (this._emojisFilter) { return this._emojisFilter(emoji) } return true } } export class EmojiData { constructor(data) { this._data = Object.assign({}, data) this._skins = null if (this._data.skin_variations) { this._skins = [] for (var skinIdx in SKINS) { let skinKey = SKINS[skinIdx] let variationData = this._data.skin_variations[skinKey] let skinData = Object.assign({}, data) for (let k in variationData) { skinData[k] = variationData[k] } delete skinData.skin_variations skinData['skin_tone'] = parseInt(skinIdx) + 1 this._skins.push(new EmojiData(skinData)) } } this._sanitized = sanitize(this._data) for (let key in this._sanitized) { this[key] = this._sanitized[key] } this.short_names = this._data.short_names this.short_name = this._data.short_names[0] Object.freeze(this) } getSkin(skinIdx) { if (skinIdx && skinIdx != 'native' && this._skins) { return this._skins[skinIdx - 1] } return this } getPosition() { let adjustedColumns = SHEET_COLUMNS - 1, x = +((100 / adjustedColumns) * this._data.sheet_x).toFixed(2), y = +((100 / adjustedColumns) * this._data.sheet_y).toFixed(2) return `${x}% ${y}%` } ariaLabel() { return [this.native].concat(this.short_names).filter(Boolean).join(', ') } } export class EmojiView { /** * emoji - Emoji to display * set - string, emoji set name * native - boolean, whether to render native emoji * fallback - fallback function to render missing emoji, optional * emojiTooltip - wether we need to show the emoji tooltip, optional * emojiSize - emoji size in pixels, optional */ constructor(emoji, skin, set, native, fallback, emojiTooltip, emojiSize) { this._emoji = emoji this._native = native this._skin = skin this._set = set this._fallback = fallback this.canRender = this._canRender() this.cssClass = this._cssClass() this.cssStyle = this._cssStyle(emojiSize) this.content = this._content() this.title = emojiTooltip === true ? emoji.short_name : null this.ariaLabel = emoji.ariaLabel() Object.freeze(this) } getEmoji() { return this._emoji.getSkin(this._skin) } _canRender() { return ( this._isCustom() || this._isNative() || this._hasEmoji() || this._fallback ) } _cssClass() { return ['emoji-set-' + this._set, 'emoji-type-' + this._emojiType()] } _cssStyle(emojiSize) { let cssStyle = {} if (this._isCustom()) { cssStyle = { backgroundImage: 'url(' + this.getEmoji()._data.imageUrl + ')', backgroundSize: '100%', width: emojiSize + 'px', height: emojiSize + 'px', } } else if (this._hasEmoji() && !this._isNative()) { cssStyle = { backgroundPosition: this.getEmoji().getPosition(), } } if (emojiSize) { if (this._isNative()) { // Set font-size for native emoji. cssStyle = Object.assign(cssStyle, { // font-size is used for native emoji which we need // to scale with 0.95 factor to have them look approximately // the same size as image-based emoji. fontSize: Math.round(emojiSize * 0.95 * 10) / 10 + 'px', }) } else { // Set width/height for image emoji. cssStyle = Object.assign(cssStyle, { width: emojiSize + 'px', height: emojiSize + 'px', }) } } return cssStyle } _content() { if (this._isCustom()) { return '' } if (this._isNative()) { return this.getEmoji().native } if (this._hasEmoji()) { return '' } return this._fallback ? this._fallback(this.getEmoji()) : null } _isNative() { return this._native } _isCustom() { return this.getEmoji().custom } _hasEmoji() { if (!this.getEmoji()._data) { // Return false if we have no data. return false } const hasImage = this.getEmoji()._data['has_img_' + this._set] if (hasImage === undefined) { // If there is no has_img_xxx in the data, we are working with // specific data file, like facebook.json, so we assume all // emojis are available (the :set setting for picker should // match the data file). return true } // Otherwise, we are using all.json and can switch between different // sets - in this case the `has_img_{set_name}` is a boolean that // indicates if there is such image or not for a given set. return hasImage } _emojiType() { if (this._isCustom()) { return 'custom' } if (this._isNative()) { return 'native' } if (this._hasEmoji()) { return 'image' } return 'fallback' } } export function sanitize(emoji) { var { name, short_names, skin_tone, skin_variations, emoticons, unified, custom, imageUrl, } = emoji, id = emoji.id || short_names[0], colons = `:${id}:` if (custom) { return { id, name, colons, emoticons, custom, imageUrl, } } if (skin_tone) { colons += `:skin-tone-${skin_tone}:` } return { id, name, colons, emoticons, unified: unified.toLowerCase(), skin: skin_tone || (skin_variations ? 1 : null), native: unifiedToNative(unified), } }