2020-06-06 18:37:18 +02:00
|
|
|
/* eslint-disable prefer-const,no-labels,no-inner-declarations */
|
2022-12-29 19:16:39 +01:00
|
|
|
import { onMount, tick } from 'svelte'
|
2020-06-17 05:17:42 +02:00
|
|
|
import { groups as defaultGroups, customGroup } from '../../groups'
|
2020-06-12 05:29:48 +02:00
|
|
|
import { MIN_SEARCH_TEXT_LENGTH, NUM_SKIN_TONES } from '../../../shared/constants'
|
2020-05-18 04:08:00 +02:00
|
|
|
import { requestIdleCallback } from '../../utils/requestIdleCallback'
|
|
|
|
import { hasZwj } from '../../utils/hasZwj'
|
2022-12-29 19:16:39 +01:00
|
|
|
import { detectEmojiSupportLevel, supportedZwjEmojis } from '../../utils/emojiSupport'
|
2020-06-08 04:48:38 +02:00
|
|
|
import { applySkinTone } from '../../utils/applySkinTone'
|
2020-06-08 16:49:01 +02:00
|
|
|
import { halt } from '../../utils/halt'
|
|
|
|
import { incrementOrDecrement } from '../../utils/incrementOrDecrement'
|
2020-06-13 05:14:47 +02:00
|
|
|
import {
|
|
|
|
DEFAULT_NUM_COLUMNS,
|
|
|
|
MOST_COMMONLY_USED_EMOJI,
|
|
|
|
TIMEOUT_BEFORE_LOADING_MESSAGE
|
|
|
|
} from '../../constants'
|
|
|
|
import { uniqBy } from '../../../shared/uniqBy'
|
2020-06-14 23:42:05 +02:00
|
|
|
import { summarizeEmojisForUI } from '../../utils/summarizeEmojisForUI'
|
2020-12-27 18:54:37 +01:00
|
|
|
import * as widthCalculator from '../../utils/widthCalculator'
|
2020-06-15 19:38:39 +02:00
|
|
|
import { checkZwjSupport } from '../../utils/checkZwjSupport'
|
|
|
|
import { requestPostAnimationFrame } from '../../utils/requestPostAnimationFrame'
|
2020-06-25 15:59:02 +02:00
|
|
|
import { requestAnimationFrame } from '../../utils/requestAnimationFrame'
|
2020-06-28 06:15:17 +02:00
|
|
|
import { uniq } from '../../../shared/uniq'
|
2022-12-18 21:47:19 +01:00
|
|
|
import { resetScrollTopIfPossible } from '../../utils/resetScrollTopIfPossible.js'
|
2020-05-18 04:08:00 +02:00
|
|
|
|
2020-06-15 19:07:33 +02:00
|
|
|
// public
|
2021-07-11 19:01:21 +02:00
|
|
|
export let skinToneEmoji
|
|
|
|
export let i18n
|
|
|
|
export let database
|
|
|
|
export let customEmoji
|
|
|
|
export let customCategorySorting
|
2022-12-29 19:16:39 +01:00
|
|
|
export let emojiVersion
|
2020-06-15 19:07:33 +02:00
|
|
|
|
|
|
|
// private
|
|
|
|
let initialLoad = true
|
2020-05-18 04:08:00 +02:00
|
|
|
let currentEmojis = []
|
2020-06-17 05:17:42 +02:00
|
|
|
let currentEmojisWithCategories = [] // eslint-disable-line no-unused-vars
|
2020-05-18 04:08:00 +02:00
|
|
|
let rawSearchText = ''
|
|
|
|
let searchText = ''
|
|
|
|
let rootElement
|
|
|
|
let baselineEmoji
|
2020-06-25 15:59:02 +02:00
|
|
|
let tabpanelElement
|
2020-06-02 17:42:33 +02:00
|
|
|
let searchMode = false // eslint-disable-line no-unused-vars
|
|
|
|
let activeSearchItem = -1
|
2020-06-06 18:37:18 +02:00
|
|
|
let message // eslint-disable-line no-unused-vars
|
2020-06-09 05:48:41 +02:00
|
|
|
let skinTonePickerExpanded = false
|
2020-06-13 22:07:59 +02:00
|
|
|
let skinTonePickerExpandedAfterAnimation = false // eslint-disable-line no-unused-vars
|
|
|
|
let skinToneDropdown
|
2020-06-08 04:48:38 +02:00
|
|
|
let currentSkinTone = 0
|
|
|
|
let activeSkinTone = 0
|
2020-06-17 16:52:25 +02:00
|
|
|
let skinToneButtonText // eslint-disable-line no-unused-vars
|
2020-06-22 03:26:11 +02:00
|
|
|
let pickerStyle // eslint-disable-line no-unused-vars
|
2020-06-11 04:05:03 +02:00
|
|
|
let skinToneButtonLabel = '' // eslint-disable-line no-unused-vars
|
2020-06-12 05:29:48 +02:00
|
|
|
let skinTones = []
|
2020-06-13 05:14:47 +02:00
|
|
|
let currentFavorites = [] // eslint-disable-line no-unused-vars
|
2020-06-15 02:30:32 +02:00
|
|
|
let defaultFavoriteEmojis
|
2020-06-13 05:14:47 +02:00
|
|
|
let numColumns = DEFAULT_NUM_COLUMNS
|
2022-12-18 21:18:20 +01:00
|
|
|
let isRtl = false // eslint-disable-line no-unused-vars
|
2020-06-13 05:14:47 +02:00
|
|
|
let scrollbarWidth = 0 // eslint-disable-line no-unused-vars
|
2020-06-17 05:17:42 +02:00
|
|
|
let currentGroupIndex = 0
|
|
|
|
let groups = defaultGroups
|
|
|
|
let currentGroup
|
2020-12-29 02:53:23 +01:00
|
|
|
let databaseLoaded = false // eslint-disable-line no-unused-vars
|
2020-06-25 16:23:59 +02:00
|
|
|
let activeSearchItemId // eslint-disable-line no-unused-vars
|
2020-05-31 22:57:07 +02:00
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
|
|
|
// Utils/helpers
|
|
|
|
//
|
|
|
|
|
2020-12-26 22:53:26 +01:00
|
|
|
const focus = id => {
|
2020-06-15 19:38:39 +02:00
|
|
|
rootElement.getRootNode().getElementById(id).focus()
|
|
|
|
}
|
|
|
|
|
|
|
|
// fire a custom event that crosses the shadow boundary
|
2020-12-26 22:53:26 +01:00
|
|
|
const fireEvent = (name, detail) => {
|
2020-06-15 19:38:39 +02:00
|
|
|
rootElement.dispatchEvent(new CustomEvent(name, {
|
|
|
|
detail,
|
|
|
|
bubbles: true,
|
|
|
|
composed: true
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
2020-12-26 22:53:26 +01:00
|
|
|
const unicodeWithSkin = (emoji, currentSkinTone) => (
|
|
|
|
(currentSkinTone && emoji.skins && emoji.skins[currentSkinTone]) || emoji.unicode
|
|
|
|
)
|
2020-06-15 19:38:39 +02:00
|
|
|
|
2020-06-28 06:15:17 +02:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
2020-12-26 22:53:26 +01:00
|
|
|
const labelWithSkin = (emoji, currentSkinTone) => (
|
|
|
|
uniq([(emoji.name || unicodeWithSkin(emoji, currentSkinTone)), ...(emoji.shortcodes || [])]).join(', ')
|
|
|
|
)
|
|
|
|
|
|
|
|
// Detect a skintone option button
|
|
|
|
const isSkinToneOption = element => /^skintone-/.test(element.id)
|
2020-06-28 06:15:17 +02:00
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
|
|
|
// Determine the emoji support level (in requestIdleCallback)
|
|
|
|
//
|
2020-06-06 18:12:39 +02:00
|
|
|
|
2022-12-29 19:16:39 +01:00
|
|
|
onMount(() => {
|
|
|
|
if (!emojiVersion) {
|
|
|
|
detectEmojiSupportLevel().then(level => {
|
|
|
|
// Can't actually test emoji support in Jest/JSDom, emoji never render in color in Cairo
|
|
|
|
/* istanbul ignore next */
|
|
|
|
if (!level) {
|
|
|
|
message = i18n.emojiUnsupportedMessage
|
|
|
|
}
|
|
|
|
})
|
2020-06-06 18:37:18 +02:00
|
|
|
}
|
2020-06-06 18:12:39 +02:00
|
|
|
})
|
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
|
|
|
// Set or update the database object
|
|
|
|
//
|
|
|
|
|
2020-05-18 04:08:00 +02:00
|
|
|
$: {
|
2020-06-06 18:37:18 +02:00
|
|
|
// show a Loading message if it takes a long time, or show an error if there's a network/IDB error
|
|
|
|
async function handleDatabaseLoading () {
|
2020-12-29 02:53:23 +01:00
|
|
|
let showingLoadingMessage = false
|
2020-06-06 18:37:18 +02:00
|
|
|
const timeoutHandle = setTimeout(() => {
|
2020-12-29 02:53:23 +01:00
|
|
|
showingLoadingMessage = true
|
2020-06-23 04:49:42 +02:00
|
|
|
message = i18n.loadingMessage
|
2020-06-06 18:37:18 +02:00
|
|
|
}, TIMEOUT_BEFORE_LOADING_MESSAGE)
|
|
|
|
try {
|
|
|
|
await database.ready()
|
2020-12-29 02:53:23 +01:00
|
|
|
databaseLoaded = true // eslint-disable-line no-unused-vars
|
2020-06-06 18:37:18 +02:00
|
|
|
} catch (err) {
|
2021-05-31 17:45:59 +02:00
|
|
|
console.error(err)
|
2020-06-23 04:49:42 +02:00
|
|
|
message = i18n.networkErrorMessage
|
2020-06-06 18:37:18 +02:00
|
|
|
} finally {
|
|
|
|
clearTimeout(timeoutHandle)
|
2020-12-29 02:53:23 +01:00
|
|
|
if (showingLoadingMessage) { // Seems safer than checking the i18n string, which may change
|
|
|
|
showingLoadingMessage = false
|
|
|
|
message = '' // eslint-disable-line no-unused-vars
|
2020-06-06 18:37:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-06-13 21:14:19 +02:00
|
|
|
if (database) {
|
|
|
|
/* no await */ handleDatabaseLoading()
|
|
|
|
}
|
2020-06-06 18:37:18 +02:00
|
|
|
}
|
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
|
|
|
// Global styles for the entire picker
|
|
|
|
//
|
|
|
|
|
2020-12-24 22:52:41 +01:00
|
|
|
/* eslint-disable no-unused-vars */
|
2020-06-22 03:26:11 +02:00
|
|
|
$: pickerStyle = `
|
2020-06-17 05:17:42 +02:00
|
|
|
--num-groups: ${groups.length};
|
2020-06-15 19:38:39 +02:00
|
|
|
--indicator-opacity: ${searchMode ? 0 : 1};
|
|
|
|
--num-skintones: ${NUM_SKIN_TONES};`
|
2020-12-24 22:52:41 +01:00
|
|
|
/* eslint-enable no-unused-vars */
|
2020-06-15 19:38:39 +02:00
|
|
|
|
|
|
|
//
|
|
|
|
// Set or update the customEmoji
|
|
|
|
//
|
2020-06-09 06:15:00 +02:00
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
$: {
|
|
|
|
if (customEmoji && database) {
|
2021-05-31 17:45:59 +02:00
|
|
|
console.log('updating custom emoji')
|
2020-06-15 19:38:39 +02:00
|
|
|
database.customEmoji = customEmoji
|
|
|
|
}
|
|
|
|
}
|
2020-06-08 04:48:38 +02:00
|
|
|
|
2020-06-15 02:30:32 +02:00
|
|
|
$: {
|
|
|
|
if (customEmoji && customEmoji.length) {
|
2020-06-17 05:17:42 +02:00
|
|
|
groups = [customGroup, ...defaultGroups]
|
|
|
|
} else if (groups !== defaultGroups) {
|
2022-04-16 02:51:17 +02:00
|
|
|
if (currentGroupIndex) {
|
|
|
|
// If the current group is anything other than "custom" (which is first), decrement.
|
|
|
|
// This fixes the odd case where you set customEmoji, then pick a category, then unset customEmoji
|
|
|
|
currentGroupIndex--
|
|
|
|
}
|
2020-06-17 05:17:42 +02:00
|
|
|
groups = defaultGroups
|
2020-06-15 02:30:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
|
|
|
// Set or update the preferred skin tone
|
|
|
|
//
|
2020-06-06 21:38:08 +02:00
|
|
|
|
2020-06-13 05:14:47 +02:00
|
|
|
$: {
|
|
|
|
async function updatePreferredSkinTone () {
|
2020-12-29 02:53:23 +01:00
|
|
|
if (databaseLoaded) {
|
2020-06-13 05:14:47 +02:00
|
|
|
currentSkinTone = await database.getPreferredSkinTone()
|
|
|
|
}
|
|
|
|
}
|
2020-06-13 18:31:11 +02:00
|
|
|
/* no await */ updatePreferredSkinTone()
|
2020-06-13 05:14:47 +02:00
|
|
|
}
|
|
|
|
|
2020-06-17 16:52:25 +02:00
|
|
|
$: skinTones = Array(NUM_SKIN_TONES).fill().map((_, i) => applySkinTone(skinToneEmoji, i))
|
2020-12-24 22:52:41 +01:00
|
|
|
/* eslint-disable no-unused-vars */
|
2020-06-17 16:52:25 +02:00
|
|
|
$: skinToneButtonText = skinTones[currentSkinTone]
|
2020-06-15 19:38:39 +02:00
|
|
|
$: skinToneButtonLabel = i18n.skinToneLabel.replace('{skinTone}', i18n.skinTones[currentSkinTone])
|
2020-12-24 22:52:41 +01:00
|
|
|
/* eslint-enable no-unused-vars */
|
2020-06-15 19:38:39 +02:00
|
|
|
|
|
|
|
//
|
|
|
|
// Set or update the favorites emojis
|
|
|
|
//
|
|
|
|
|
2020-06-13 05:14:47 +02:00
|
|
|
$: {
|
|
|
|
async function updateDefaultFavoriteEmojis () {
|
2020-06-17 16:52:25 +02:00
|
|
|
defaultFavoriteEmojis = (await Promise.all(MOST_COMMONLY_USED_EMOJI.map(unicode => (
|
|
|
|
database.getEmojiByUnicodeOrName(unicode)
|
|
|
|
)))).filter(Boolean) // filter because in Jest tests we don't have all the emoji in the DB
|
|
|
|
}
|
2020-12-29 02:53:23 +01:00
|
|
|
if (databaseLoaded) {
|
2020-06-17 16:52:25 +02:00
|
|
|
/* no await */ updateDefaultFavoriteEmojis()
|
2020-06-13 05:14:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$: {
|
2020-06-17 16:52:25 +02:00
|
|
|
async function updateFavorites () {
|
2021-05-31 17:45:59 +02:00
|
|
|
console.log('updateFavorites')
|
2020-06-13 05:14:47 +02:00
|
|
|
const dbFavorites = await database.getTopFavoriteEmoji(numColumns)
|
2020-06-17 16:52:25 +02:00
|
|
|
const favorites = await summarizeEmojis(uniqBy([
|
2020-06-13 05:14:47 +02:00
|
|
|
...dbFavorites,
|
2020-06-15 02:30:32 +02:00
|
|
|
...defaultFavoriteEmojis
|
2020-06-17 16:52:25 +02:00
|
|
|
], _ => (_.unicode || _.name)).slice(0, numColumns))
|
|
|
|
currentFavorites = favorites
|
2020-06-13 05:14:47 +02:00
|
|
|
}
|
|
|
|
|
2020-12-29 02:53:23 +01:00
|
|
|
if (databaseLoaded && defaultFavoriteEmojis) {
|
2020-06-13 18:31:11 +02:00
|
|
|
/* no await */ updateFavorites()
|
2020-06-13 05:14:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
|
|
|
// Calculate the width of the emoji grid. This serves two purposes:
|
|
|
|
// 1) Re-calculate the --num-columns var because it may have changed
|
|
|
|
// 2) Re-calculate the scrollbar width because it may have changed
|
|
|
|
// (i.e. because the number of items changed)
|
2021-08-14 02:40:03 +02:00
|
|
|
// 3) Re-calculate whether we're in RTL mode or not.
|
|
|
|
//
|
|
|
|
// The benefit of doing this in one place is to align with rAF/ResizeObserver
|
|
|
|
// and do all the calculations in one go. RTL vs LTR is not strictly width-related,
|
|
|
|
// but since we're already reading the style here, and since it's already aligned with
|
|
|
|
// the rAF loop, this is the most appropriate place to do it perf-wise.
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
2020-06-13 05:14:47 +02:00
|
|
|
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
2021-08-14 02:40:03 +02:00
|
|
|
function calculateEmojiGridStyle (node) {
|
2020-12-27 18:54:37 +01:00
|
|
|
return widthCalculator.calculateWidth(node, width => {
|
2020-12-27 03:45:16 +01:00
|
|
|
/* istanbul ignore next */
|
2021-08-14 02:40:03 +02:00
|
|
|
if (process.env.NODE_ENV !== 'test') { // jsdom throws errors for this kind of fancy stuff
|
|
|
|
// read all the style/layout calculations we need to make
|
|
|
|
const style = getComputedStyle(rootElement)
|
|
|
|
const newNumColumns = parseInt(style.getPropertyValue('--num-columns'), 10)
|
|
|
|
const newIsRtl = style.getPropertyValue('direction') === 'rtl'
|
|
|
|
const parentWidth = node.parentElement.getBoundingClientRect().width
|
|
|
|
const newScrollbarWidth = parentWidth - width
|
|
|
|
|
|
|
|
// write to Svelte variables
|
|
|
|
numColumns = newNumColumns
|
|
|
|
scrollbarWidth = newScrollbarWidth // eslint-disable-line no-unused-vars
|
|
|
|
isRtl = newIsRtl // eslint-disable-line no-unused-vars
|
|
|
|
}
|
2020-06-13 05:14:47 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
2020-06-17 05:17:42 +02:00
|
|
|
// Update the current group based on the currentGroupIndex
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
|
|
|
|
2020-06-17 05:17:42 +02:00
|
|
|
$: currentGroup = groups[currentGroupIndex]
|
2020-06-15 19:38:39 +02:00
|
|
|
|
|
|
|
//
|
|
|
|
// Set or update the currentEmojis. Check for invalid ZWJ renderings
|
|
|
|
// (i.e. double emoji).
|
|
|
|
//
|
|
|
|
|
2020-06-06 18:37:18 +02:00
|
|
|
$: {
|
2020-06-02 17:42:33 +02:00
|
|
|
async function updateEmojis () {
|
2021-05-31 17:45:59 +02:00
|
|
|
console.log('updateEmojis')
|
2020-12-29 02:53:23 +01:00
|
|
|
if (!databaseLoaded) {
|
2020-06-07 10:10:43 +02:00
|
|
|
currentEmojis = []
|
2020-06-22 09:17:44 +02:00
|
|
|
searchMode = false
|
2020-06-07 10:10:43 +02:00
|
|
|
} else if (searchText.length >= MIN_SEARCH_TEXT_LENGTH) {
|
2020-12-19 04:03:27 +01:00
|
|
|
const currentSearchText = searchText
|
|
|
|
const newEmojis = await getEmojisBySearchQuery(currentSearchText)
|
|
|
|
if (currentSearchText === searchText) { // if the situation changes asynchronously, do not update
|
|
|
|
currentEmojis = newEmojis
|
|
|
|
searchMode = true
|
|
|
|
}
|
2020-06-17 05:17:42 +02:00
|
|
|
} else if (currentGroup) {
|
2020-12-19 04:03:27 +01:00
|
|
|
const currentGroupId = currentGroup.id
|
|
|
|
const newEmojis = await getEmojisByGroup(currentGroupId)
|
|
|
|
if (currentGroupId === currentGroup.id) { // if the situation changes asynchronously, do not update
|
|
|
|
currentEmojis = newEmojis
|
|
|
|
searchMode = false
|
|
|
|
}
|
2020-05-18 04:08:00 +02:00
|
|
|
}
|
2020-06-02 17:42:33 +02:00
|
|
|
}
|
2020-06-13 18:31:11 +02:00
|
|
|
/* no await */ updateEmojis()
|
2020-05-18 04:08:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Some emojis have their ligatures rendered as two or more consecutive emojis
|
|
|
|
// We want to treat these the same as unsupported emojis, so we compare their
|
|
|
|
// widths against the baseline widths and remove them as necessary
|
|
|
|
$: {
|
2020-06-15 02:30:32 +02:00
|
|
|
const zwjEmojisToCheck = currentEmojis
|
|
|
|
.filter(emoji => emoji.unicode) // filter custom emoji
|
|
|
|
.filter(emoji => hasZwj(emoji) && !supportedZwjEmojis.has(emoji.unicode))
|
2022-12-29 19:16:39 +01:00
|
|
|
if (!emojiVersion && zwjEmojisToCheck.length) {
|
2020-05-18 04:08:00 +02:00
|
|
|
// render now, check their length later
|
2020-06-15 19:38:39 +02:00
|
|
|
requestAnimationFrame(() => checkZwjSupportAndUpdate(zwjEmojisToCheck))
|
2020-05-18 04:08:00 +02:00
|
|
|
} else {
|
2022-12-29 19:16:39 +01:00
|
|
|
currentEmojis = emojiVersion ? currentEmojis : currentEmojis.filter(isZwjSupported)
|
2022-12-18 21:47:19 +01:00
|
|
|
// Reset scroll top to 0 when emojis change
|
|
|
|
requestAnimationFrame(() => resetScrollTopIfPossible(tabpanelElement))
|
2020-05-18 04:08:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
function checkZwjSupportAndUpdate (zwjEmojisToCheck) {
|
2020-06-15 19:52:04 +02:00
|
|
|
const rootNode = rootElement.getRootNode()
|
2020-12-28 00:38:46 +01:00
|
|
|
const emojiToDomNode = emoji => rootNode.getElementById(`emo-${emoji.id}`)
|
2020-06-15 19:52:04 +02:00
|
|
|
checkZwjSupport(zwjEmojisToCheck, baselineEmoji, emojiToDomNode)
|
2020-05-18 04:08:00 +02:00
|
|
|
// force update
|
|
|
|
currentEmojis = currentEmojis // eslint-disable-line no-self-assign
|
|
|
|
}
|
|
|
|
|
|
|
|
function isZwjSupported (emoji) {
|
2020-06-15 02:30:32 +02:00
|
|
|
return !emoji.unicode || !hasZwj(emoji) || supportedZwjEmojis.get(emoji.unicode)
|
2020-05-18 04:08:00 +02:00
|
|
|
}
|
|
|
|
|
2020-06-05 03:38:49 +02:00
|
|
|
async function filterEmojisByVersion (emojis) {
|
2022-12-29 19:16:39 +01:00
|
|
|
const emojiSupportLevel = emojiVersion || await detectEmojiSupportLevel()
|
2020-06-21 23:25:14 +02:00
|
|
|
// !version corresponds to custom emoji
|
|
|
|
return emojis.filter(({ version }) => !version || version <= emojiSupportLevel)
|
2020-05-18 04:08:00 +02:00
|
|
|
}
|
|
|
|
|
2020-06-08 17:32:04 +02:00
|
|
|
async function summarizeEmojis (emojis) {
|
2022-12-29 19:16:39 +01:00
|
|
|
return summarizeEmojisForUI(emojis, emojiVersion || await detectEmojiSupportLevel())
|
2020-06-08 17:32:04 +02:00
|
|
|
}
|
|
|
|
|
2020-05-18 04:08:00 +02:00
|
|
|
async function getEmojisByGroup (group) {
|
2021-05-31 17:45:59 +02:00
|
|
|
console.log('getEmojiByGroup', group)
|
2020-06-21 23:25:14 +02:00
|
|
|
// -1 is custom emoji
|
|
|
|
const emoji = group === -1 ? customEmoji : await database.getEmojiByGroup(group)
|
|
|
|
return summarizeEmojis(await filterEmojisByVersion(emoji))
|
2020-05-18 04:08:00 +02:00
|
|
|
}
|
|
|
|
|
2020-06-06 02:41:38 +02:00
|
|
|
async function getEmojisBySearchQuery (query) {
|
2020-06-08 17:32:04 +02:00
|
|
|
return summarizeEmojis(await filterEmojisByVersion(await database.getEmojiBySearchQuery(query)))
|
2020-05-18 04:08:00 +02:00
|
|
|
}
|
|
|
|
|
2020-06-22 02:12:14 +02:00
|
|
|
$: {
|
|
|
|
// consider initialLoad to be complete when the first tabpanel and favorites are rendered
|
2020-12-29 02:14:23 +01:00
|
|
|
/* istanbul ignore next */
|
2020-06-22 02:12:14 +02:00
|
|
|
if (process.env.NODE_ENV !== 'production' || process.env.PERF) {
|
|
|
|
if (currentEmojis.length && currentFavorites.length && initialLoad) {
|
|
|
|
initialLoad = false
|
2021-05-31 17:45:59 +02:00
|
|
|
requestPostAnimationFrame(() => performance.measure('initialLoad', 'initialLoad'))
|
2020-06-22 02:12:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-17 04:20:47 +02:00
|
|
|
//
|
|
|
|
// Derive currentEmojisWithCategories from currentEmojis. This is always done even if there
|
|
|
|
// are no categories, because it's just easier to code the HTML this way.
|
|
|
|
//
|
|
|
|
|
|
|
|
$: {
|
2020-06-17 05:17:42 +02:00
|
|
|
function calculateCurrentEmojisWithCategories () {
|
2020-06-22 09:17:44 +02:00
|
|
|
if (searchMode) {
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
category: '',
|
|
|
|
emojis: currentEmojis
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
2020-06-17 05:17:42 +02:00
|
|
|
const categoriesToEmoji = new Map()
|
|
|
|
for (const emoji of currentEmojis) {
|
|
|
|
const category = emoji.category || ''
|
|
|
|
let emojis = categoriesToEmoji.get(category)
|
|
|
|
if (!emojis) {
|
|
|
|
emojis = []
|
|
|
|
categoriesToEmoji.set(category, emojis)
|
2020-06-17 04:20:47 +02:00
|
|
|
}
|
2020-06-17 05:17:42 +02:00
|
|
|
emojis.push(emoji)
|
2020-06-17 04:20:47 +02:00
|
|
|
}
|
2020-06-17 05:17:42 +02:00
|
|
|
return [...categoriesToEmoji.entries()]
|
|
|
|
.map(([category, emojis]) => ({ category, emojis }))
|
2020-09-13 21:19:38 +02:00
|
|
|
.sort((a, b) => customCategorySorting(a.category, b.category))
|
2020-06-17 04:20:47 +02:00
|
|
|
}
|
|
|
|
|
2020-12-24 22:52:41 +01:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
2020-06-17 04:20:47 +02:00
|
|
|
currentEmojisWithCategories = calculateCurrentEmojisWithCategories()
|
|
|
|
}
|
|
|
|
|
2020-06-25 16:23:59 +02:00
|
|
|
//
|
|
|
|
// Handle active search item (i.e. pressing up or down while searching)
|
|
|
|
//
|
|
|
|
|
2020-12-24 22:52:41 +01:00
|
|
|
/* eslint-disable no-unused-vars */
|
2020-06-25 16:23:59 +02:00
|
|
|
$: activeSearchItemId = activeSearchItem !== -1 && currentEmojis[activeSearchItem].id
|
2020-12-24 22:52:41 +01:00
|
|
|
/* eslint-enable no-unused-vars */
|
2020-06-25 16:23:59 +02:00
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
|
|
|
// Handle user input on the search input
|
|
|
|
//
|
|
|
|
|
|
|
|
$: {
|
|
|
|
requestIdleCallback(() => {
|
2020-06-23 03:59:22 +02:00
|
|
|
searchText = (rawSearchText || '').trim() // defer to avoid input delays, plus we can trim here
|
2020-06-15 19:38:39 +02:00
|
|
|
activeSearchItem = -1
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-06-02 17:42:33 +02:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
|
function onSearchKeydown (event) {
|
|
|
|
if (!searchMode || !currentEmojis.length) {
|
|
|
|
return
|
|
|
|
}
|
2020-06-04 04:12:33 +02:00
|
|
|
|
|
|
|
const goToNextOrPrevious = (previous) => {
|
2020-06-08 16:49:01 +02:00
|
|
|
halt(event)
|
|
|
|
activeSearchItem = incrementOrDecrement(previous, activeSearchItem, currentEmojis)
|
2020-06-04 04:12:33 +02:00
|
|
|
}
|
|
|
|
|
2020-06-02 17:42:33 +02:00
|
|
|
switch (event.key) {
|
|
|
|
case 'ArrowDown':
|
2020-06-04 04:12:33 +02:00
|
|
|
return goToNextOrPrevious(false)
|
2020-06-02 17:42:33 +02:00
|
|
|
case 'ArrowUp':
|
2020-06-04 04:12:33 +02:00
|
|
|
return goToNextOrPrevious(true)
|
2020-06-08 16:49:01 +02:00
|
|
|
case 'Enter':
|
|
|
|
if (activeSearchItem !== -1) {
|
|
|
|
halt(event)
|
2020-06-26 04:06:03 +02:00
|
|
|
return clickEmoji(currentEmojis[activeSearchItem].id)
|
2020-06-23 04:08:42 +02:00
|
|
|
} else if (currentEmojis.length) {
|
|
|
|
activeSearchItem = 0
|
2020-06-08 16:49:01 +02:00
|
|
|
}
|
2020-06-02 17:42:33 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
|
|
|
// Handle user input on nav
|
|
|
|
//
|
|
|
|
|
2020-06-15 22:20:32 +02:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
2020-06-17 05:17:42 +02:00
|
|
|
function onNavClick (group) {
|
2020-06-15 22:20:32 +02:00
|
|
|
rawSearchText = ''
|
|
|
|
searchText = ''
|
|
|
|
activeSearchItem = -1
|
2020-06-17 05:17:42 +02:00
|
|
|
currentGroupIndex = groups.findIndex(_ => _.id === group.id)
|
2020-06-15 22:20:32 +02:00
|
|
|
}
|
|
|
|
|
2020-06-04 03:42:27 +02:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
|
function onNavKeydown (event) {
|
|
|
|
const { target, key } = event
|
|
|
|
|
2020-06-19 23:44:44 +02:00
|
|
|
const doFocus = el => {
|
|
|
|
if (el) {
|
|
|
|
halt(event)
|
|
|
|
el.focus()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-04 03:42:27 +02:00
|
|
|
switch (key) {
|
|
|
|
case 'ArrowLeft':
|
2020-06-19 23:44:44 +02:00
|
|
|
return doFocus(target.previousSibling)
|
2020-06-04 03:42:27 +02:00
|
|
|
case 'ArrowRight':
|
2020-06-19 23:44:44 +02:00
|
|
|
return doFocus(target.nextSibling)
|
2020-09-13 21:19:45 +02:00
|
|
|
case 'Home':
|
|
|
|
return doFocus(target.parentElement.firstChild)
|
|
|
|
case 'End':
|
|
|
|
return doFocus(target.parentElement.lastChild)
|
2020-06-04 03:42:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
|
|
|
// Handle user input on an emoji
|
|
|
|
//
|
2020-06-12 03:43:18 +02:00
|
|
|
|
2020-06-15 02:30:32 +02:00
|
|
|
async function clickEmoji (unicodeOrName) {
|
2020-06-15 08:38:43 +02:00
|
|
|
const emoji = await database.getEmojiByUnicodeOrName(unicodeOrName)
|
2020-06-15 02:30:32 +02:00
|
|
|
const emojiSummary = [...currentEmojis, ...currentFavorites]
|
2020-06-26 04:06:03 +02:00
|
|
|
.find(_ => (_.id === unicodeOrName))
|
2020-06-15 02:30:32 +02:00
|
|
|
const skinTonedUnicode = emojiSummary.unicode && unicodeWithSkin(emojiSummary, currentSkinTone)
|
|
|
|
await database.incrementFavoriteEmojiCount(unicodeOrName)
|
2020-06-12 03:43:18 +02:00
|
|
|
fireEvent('emoji-click', {
|
|
|
|
emoji,
|
|
|
|
skinTone: currentSkinTone,
|
2020-06-15 02:39:52 +02:00
|
|
|
...(skinTonedUnicode && { unicode: skinTonedUnicode }),
|
|
|
|
...(emojiSummary.name && { name: emojiSummary.name })
|
2020-06-12 03:43:18 +02:00
|
|
|
})
|
2020-06-08 16:49:01 +02:00
|
|
|
}
|
|
|
|
|
2020-06-06 18:54:23 +02:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
|
async function onEmojiClick (event) {
|
|
|
|
const { target } = event
|
2020-06-11 09:04:18 +02:00
|
|
|
if (!target.classList.contains('emoji')) {
|
2020-06-06 18:54:23 +02:00
|
|
|
return
|
|
|
|
}
|
2020-06-08 16:49:01 +02:00
|
|
|
halt(event)
|
2020-06-25 16:23:59 +02:00
|
|
|
const id = target.id.substring(4) // replace 'emo-' or 'fav-' prefix
|
2020-06-06 18:54:23 +02:00
|
|
|
|
2020-06-15 02:30:32 +02:00
|
|
|
/* no await */ clickEmoji(id)
|
2020-06-06 18:54:23 +02:00
|
|
|
}
|
|
|
|
|
2020-06-15 19:38:39 +02:00
|
|
|
//
|
|
|
|
// Handle user input on the skintone picker
|
|
|
|
//
|
2020-06-09 05:48:41 +02:00
|
|
|
|
2020-06-08 04:48:38 +02:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
2020-07-01 18:08:04 +02:00
|
|
|
async function onSkinToneOptionsClick (event) {
|
2020-06-09 05:48:41 +02:00
|
|
|
const { target } = event
|
2020-12-26 22:53:26 +01:00
|
|
|
if (!isSkinToneOption(target)) {
|
2020-06-09 05:48:41 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
halt(event)
|
|
|
|
const skinTone = parseInt(target.id.slice(9), 10) // remove 'skintone-' prefix
|
|
|
|
currentSkinTone = skinTone
|
|
|
|
skinTonePickerExpanded = false
|
|
|
|
focus('skintone-button')
|
2020-06-12 03:43:18 +02:00
|
|
|
fireEvent('skin-tone-change', { skinTone })
|
2020-06-12 05:04:42 +02:00
|
|
|
/* no await */ database.setPreferredSkinTone(skinTone)
|
2020-06-08 04:48:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
2020-06-09 05:48:41 +02:00
|
|
|
async function onClickSkinToneButton (event) {
|
|
|
|
skinTonePickerExpanded = !skinTonePickerExpanded
|
2020-06-08 04:48:38 +02:00
|
|
|
activeSkinTone = currentSkinTone
|
2020-06-09 05:48:41 +02:00
|
|
|
if (skinTonePickerExpanded) {
|
|
|
|
halt(event)
|
2020-06-14 03:48:55 +02:00
|
|
|
requestAnimationFrame(() => focus(`skintone-${activeSkinTone}`))
|
2020-06-09 05:48:41 +02:00
|
|
|
}
|
2020-06-08 04:48:38 +02:00
|
|
|
}
|
|
|
|
|
2020-06-13 22:07:59 +02:00
|
|
|
// To make the animation nicer, change the z-index of the skintone picker button
|
|
|
|
// *after* the animation has played. This makes it appear that the picker box
|
|
|
|
// is expanding "below" the button
|
|
|
|
$: {
|
|
|
|
if (skinTonePickerExpanded) {
|
|
|
|
skinToneDropdown.addEventListener('transitionend', () => {
|
2020-12-24 22:52:41 +01:00
|
|
|
skinTonePickerExpandedAfterAnimation = true // eslint-disable-line no-unused-vars
|
2020-06-13 22:07:59 +02:00
|
|
|
}, { once: true })
|
|
|
|
} else {
|
2020-12-24 22:52:41 +01:00
|
|
|
skinTonePickerExpandedAfterAnimation = false // eslint-disable-line no-unused-vars
|
2020-06-13 22:07:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-08 04:48:38 +02:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
2020-07-01 18:08:04 +02:00
|
|
|
function onSkinToneOptionsKeydown (event) {
|
2020-06-09 05:48:41 +02:00
|
|
|
if (!skinTonePickerExpanded) {
|
2020-06-08 05:49:21 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-09-13 21:19:45 +02:00
|
|
|
const changeActiveSkinTone = async nextSkinTone => {
|
2020-06-08 16:49:01 +02:00
|
|
|
halt(event)
|
2020-09-13 21:19:45 +02:00
|
|
|
activeSkinTone = nextSkinTone
|
2020-06-09 05:48:41 +02:00
|
|
|
await tick()
|
|
|
|
focus(`skintone-${activeSkinTone}`)
|
2020-06-08 04:48:38 +02:00
|
|
|
}
|
|
|
|
|
2020-07-01 18:08:04 +02:00
|
|
|
switch (event.key) {
|
2020-06-08 04:48:38 +02:00
|
|
|
case 'ArrowUp':
|
2020-09-13 21:19:45 +02:00
|
|
|
return changeActiveSkinTone(incrementOrDecrement(true, activeSkinTone, skinTones))
|
2020-06-08 04:48:38 +02:00
|
|
|
case 'ArrowDown':
|
2020-09-13 21:19:45 +02:00
|
|
|
return changeActiveSkinTone(incrementOrDecrement(false, activeSkinTone, skinTones))
|
|
|
|
case 'Home':
|
|
|
|
return changeActiveSkinTone(0)
|
|
|
|
case 'End':
|
|
|
|
return changeActiveSkinTone(skinTones.length - 1)
|
2020-07-01 18:08:04 +02:00
|
|
|
case 'Enter':
|
|
|
|
// enter on keydown, space on keyup. this is just how browsers work for buttons
|
|
|
|
// https://lists.w3.org/Archives/Public/w3c-wai-ig/2019JanMar/0086.html
|
|
|
|
return onSkinToneOptionsClick(event)
|
2020-09-13 21:19:45 +02:00
|
|
|
case 'Escape':
|
|
|
|
halt(event)
|
2021-07-07 06:57:22 +02:00
|
|
|
skinTonePickerExpanded = false
|
2020-09-13 21:19:45 +02:00
|
|
|
return focus('skintone-button')
|
2020-07-01 18:08:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
|
function onSkinToneOptionsKeyup (event) {
|
|
|
|
if (!skinTonePickerExpanded) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
switch (event.key) {
|
|
|
|
case ' ':
|
|
|
|
// enter on keydown, space on keyup. this is just how browsers work for buttons
|
|
|
|
// https://lists.w3.org/Archives/Public/w3c-wai-ig/2019JanMar/0086.html
|
|
|
|
return onSkinToneOptionsClick(event)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
|
async function onSkinToneOptionsFocusOut (event) {
|
|
|
|
// On blur outside of the skintone options, collapse the skintone picker.
|
|
|
|
// Except if focus is just moving to another skintone option, e.g. pressing up/down to change focus
|
|
|
|
const { relatedTarget } = event
|
2020-12-26 22:53:26 +01:00
|
|
|
if (!relatedTarget || !isSkinToneOption(relatedTarget)) {
|
2020-07-01 18:08:04 +02:00
|
|
|
skinTonePickerExpanded = false
|
2020-06-09 05:48:41 +02:00
|
|
|
}
|
|
|
|
}
|