You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			164 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			164 lines
		
	
	
		
			4.1 KiB
		
	
	
	
		
			TypeScript
		
	
| /* eslint-disable no-restricted-syntax */
 | |
| /* eslint-disable no-continue */
 | |
| /* eslint-disable no-param-reassign */
 | |
| /* eslint-disable import/no-mutable-exports  */
 | |
| import { init, I18n } from 'emoji-mart';
 | |
| import { FixedBaseEmoji, NativeEmojiData } from '../types/Reaction';
 | |
| import { loadEmojiPanelI18n } from './i18n';
 | |
| 
 | |
| export type SizeClassType = 'default' | 'small' | 'medium' | 'large' | 'jumbo';
 | |
| 
 | |
| function getRegexUnicodeEmojis() {
 | |
|   return /\p{Emoji_Presentation}/gu;
 | |
| }
 | |
| 
 | |
| function getCountOfAllMatches(str: string) {
 | |
|   const regex = getRegexUnicodeEmojis();
 | |
| 
 | |
|   const matches = str.match(regex);
 | |
| 
 | |
|   return matches?.length || 0;
 | |
| }
 | |
| 
 | |
| function hasNormalCharacters(str: string) {
 | |
|   const noEmoji = str.replace(getRegexUnicodeEmojis(), '').trim();
 | |
|   return noEmoji.length > 0;
 | |
| }
 | |
| 
 | |
| export function getEmojiSizeClass(str: string): SizeClassType {
 | |
|   if (!str || !str.length) {
 | |
|     return 'small';
 | |
|   }
 | |
|   if (hasNormalCharacters(str)) {
 | |
|     return 'small';
 | |
|   }
 | |
| 
 | |
|   const emojiCount = getCountOfAllMatches(str);
 | |
|   if (emojiCount > 6) {
 | |
|     return 'small';
 | |
|   }
 | |
|   if (emojiCount > 4) {
 | |
|     return 'medium';
 | |
|   }
 | |
|   if (emojiCount > 2) {
 | |
|     return 'large';
 | |
|   }
 | |
|   return 'jumbo';
 | |
| }
 | |
| 
 | |
| export let nativeEmojiData: NativeEmojiData | null = null;
 | |
| export let i18nEmojiData: typeof I18n | null = null;
 | |
| 
 | |
| export async function initialiseEmojiData(data: any): Promise<void> {
 | |
|   const ariaLabels: Record<string, string> = {};
 | |
|   Object.entries(data.emojis).forEach(([key, value]: [string, any]) => {
 | |
|     value.search = `,${[
 | |
|       [value.id, false],
 | |
|       [value.name, true],
 | |
|       [value.keywords, false],
 | |
|       [value.emoticons, false],
 | |
|     ]
 | |
|       .map(([strings, split]) => {
 | |
|         if (!strings) {
 | |
|           return null;
 | |
|         }
 | |
|         return (Array.isArray(strings) ? strings : [strings])
 | |
|           .map(string =>
 | |
|             (split ? string.split(/[-|_|\s]+/) : [string]).map((s: string) => s.toLowerCase())
 | |
|           )
 | |
|           .flat();
 | |
|       })
 | |
|       .flat()
 | |
|       .filter(a => a && a.trim())
 | |
|       .join(',')})}`;
 | |
| 
 | |
|     (value as FixedBaseEmoji).skins.forEach(skin => {
 | |
|       ariaLabels[skin.native] = value.name;
 | |
|     });
 | |
| 
 | |
|     data.emojis[key] = value;
 | |
|   });
 | |
| 
 | |
|   data.ariaLabels = ariaLabels;
 | |
|   nativeEmojiData = data;
 | |
| 
 | |
|   i18nEmojiData = await loadEmojiPanelI18n();
 | |
|   // Data needs to be initialised once per page load for the emoji components
 | |
|   // See https://github.com/missive/emoji-mart#%EF%B8%8F%EF%B8%8F-headless-search
 | |
|   await init({ data, i18n: i18nEmojiData });
 | |
| }
 | |
| 
 | |
| // Synchronous version of Emoji Mart's SearchIndex.search()
 | |
| // If we upgrade the package things will probably break
 | |
| export function searchSync(query: string, args?: any): Array<any> {
 | |
|   if (!nativeEmojiData) {
 | |
|     window.log.error('No native emoji data found');
 | |
|     return [];
 | |
|   }
 | |
| 
 | |
|   if (!query || !query.trim().length) {
 | |
|     return [];
 | |
|   }
 | |
| 
 | |
|   const maxResults = args && args.maxResults ? args.maxResults : 90;
 | |
|   const values = query
 | |
|     .toLowerCase()
 | |
|     .replace(/(\w)-/, '$1 ')
 | |
|     .split(/[\s|,]+/)
 | |
|     .filter((word: string, i: number, words: Array<string>) => {
 | |
|       return word.trim() && words.indexOf(word) === i;
 | |
|     });
 | |
| 
 | |
|   if (!values.length) {
 | |
|     return [];
 | |
|   }
 | |
| 
 | |
|   let pool: any = Object.values(nativeEmojiData.emojis);
 | |
|   let results: Array<FixedBaseEmoji> = [];
 | |
|   let scores: Record<string, number> = {};
 | |
| 
 | |
|   for (const value of values) {
 | |
|     if (!pool.length) {
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     results = [];
 | |
|     scores = {};
 | |
| 
 | |
|     for (const emoji of pool) {
 | |
|       if (!emoji.search) {
 | |
|         continue;
 | |
|       }
 | |
|       const score: number = emoji.search.indexOf(`,${value}`);
 | |
|       if (score === -1) {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       results.push(emoji);
 | |
|       scores[emoji.id] = scores[emoji.id] ? scores[emoji.id] : 0;
 | |
|       scores[emoji.id] += emoji.id === value ? 0 : score + 1;
 | |
|     }
 | |
|     pool = results;
 | |
|   }
 | |
| 
 | |
|   if (results.length < 2) {
 | |
|     return results;
 | |
|   }
 | |
| 
 | |
|   results.sort((a: FixedBaseEmoji, b: FixedBaseEmoji) => {
 | |
|     const aScore = scores[a.id];
 | |
|     const bScore = scores[b.id];
 | |
| 
 | |
|     if (aScore === bScore) {
 | |
|       return a.id.localeCompare(b.id);
 | |
|     }
 | |
| 
 | |
|     return aScore - bScore;
 | |
|   });
 | |
| 
 | |
|   if (results.length > maxResults) {
 | |
|     results = results.slice(0, maxResults);
 | |
|   }
 | |
|   return results;
 | |
| }
 |