`;
+ };
+ const rootDom = section();
+ const forElementWithAttribute = (attributeName, callback) => {
+ for (const element of container.querySelectorAll(`[${attributeName}]`)) {
+ callback(element, element.getAttribute(attributeName));
+ }
+ };
+ if (firstRender) {
+ container.appendChild(rootDom);
+ for (const eventName of ["click", "focusout", "input", "keydown", "keyup"]) {
+ forElementWithAttribute(`data-on-${eventName}`, (element, listenerName) => {
+ element.addEventListener(eventName, events[listenerName]);
+ });
+ }
+ forElementWithAttribute("data-ref", (element, ref) => {
+ refs[ref] = element;
+ });
+ abortSignal.addEventListener("abort", () => {
+ container.removeChild(rootDom);
+ });
+ }
+ forElementWithAttribute("data-action", (element, action) => {
+ let boundActions = actionContext.get(action);
+ if (!boundActions) {
+ actionContext.set(action, boundActions = /* @__PURE__ */ new WeakSet());
+ }
+ if (!boundActions.has(element)) {
+ boundActions.add(element);
+ actions[action](element);
+ }
+ });
+}
+var qM = typeof queueMicrotask === "function" ? queueMicrotask : (callback) => Promise.resolve().then(callback);
+function createState(abortSignal) {
+ let destroyed = false;
+ let currentObserver;
+ const propsToObservers = /* @__PURE__ */ new Map();
+ const dirtyObservers = /* @__PURE__ */ new Set();
+ let queued;
+ const flush = () => {
+ if (destroyed) {
+ return;
+ }
+ const observersToRun = [...dirtyObservers];
+ dirtyObservers.clear();
+ try {
+ for (const observer of observersToRun) {
+ observer();
+ }
+ } finally {
+ queued = false;
+ if (dirtyObservers.size) {
+ queued = true;
+ qM(flush);
+ }
+ }
+ };
+ const state = new Proxy({}, {
+ get(target, prop) {
+ if (currentObserver) {
+ let observers = propsToObservers.get(prop);
+ if (!observers) {
+ observers = /* @__PURE__ */ new Set();
+ propsToObservers.set(prop, observers);
+ }
+ observers.add(currentObserver);
+ }
+ return target[prop];
+ },
+ set(target, prop, newValue) {
+ if (target[prop] !== newValue) {
+ target[prop] = newValue;
+ const observers = propsToObservers.get(prop);
+ if (observers) {
+ for (const observer of observers) {
+ dirtyObservers.add(observer);
+ }
+ if (!queued) {
+ queued = true;
+ qM(flush);
+ }
+ }
+ }
+ return true;
+ }
+ });
+ const createEffect = (callback) => {
+ const runnable = () => {
+ const oldObserver = currentObserver;
+ currentObserver = runnable;
+ try {
+ return callback();
+ } finally {
+ currentObserver = oldObserver;
+ }
+ };
+ return runnable();
+ };
+ abortSignal.addEventListener("abort", () => {
+ destroyed = true;
+ });
+ return {
+ state,
+ createEffect
+ };
+}
+function arraysAreEqualByFunction(left, right, areEqualFunc) {
+ if (left.length !== right.length) {
+ return false;
+ }
+ for (let i = 0; i < left.length; i++) {
+ if (!areEqualFunc(left[i], right[i])) {
+ return false;
+ }
+ }
+ return true;
+}
+var intersectionObserverCache = /* @__PURE__ */ new WeakMap();
+function intersectionObserverAction(node, abortSignal, listener) {
+ {
+ const root = node.closest(".tabpanel");
+ let observer = intersectionObserverCache.get(root);
+ if (!observer) {
+ observer = new IntersectionObserver(listener, {
+ root,
+ // trigger if we are 1/2 scroll container height away so that the images load a bit quicker while scrolling
+ rootMargin: "50% 0px 50% 0px",
+ // trigger if any part of the emoji grid is intersecting
+ threshold: 0
+ });
+ intersectionObserverCache.set(root, observer);
+ abortSignal.addEventListener("abort", () => {
+ observer.disconnect();
+ });
+ }
+ observer.observe(node);
+ }
+}
+var EMPTY_ARRAY = [];
+var { assign } = Object;
+function createRoot(shadowRoot, props) {
+ const refs = {};
+ const abortController = new AbortController();
+ const abortSignal = abortController.signal;
+ const { state, createEffect } = createState(abortSignal);
+ const actionContext = /* @__PURE__ */ new Map();
+ assign(state, {
+ skinToneEmoji: void 0,
+ i18n: void 0,
+ database: void 0,
+ customEmoji: void 0,
+ customCategorySorting: void 0,
+ emojiVersion: void 0
+ });
+ assign(state, props);
+ assign(state, {
+ initialLoad: true,
+ currentEmojis: [],
+ currentEmojisWithCategories: [],
+ rawSearchText: "",
+ searchText: "",
+ searchMode: false,
+ activeSearchItem: -1,
+ message: void 0,
+ skinTonePickerExpanded: false,
+ skinTonePickerExpandedAfterAnimation: false,
+ currentSkinTone: 0,
+ activeSkinTone: 0,
+ skinToneButtonText: void 0,
+ pickerStyle: void 0,
+ skinToneButtonLabel: "",
+ skinTones: [],
+ currentFavorites: [],
+ defaultFavoriteEmojis: void 0,
+ numColumns: DEFAULT_NUM_COLUMNS,
+ isRtl: false,
+ currentGroupIndex: 0,
+ groups,
+ databaseLoaded: false,
+ activeSearchItemId: void 0
+ });
+ createEffect(() => {
+ if (state.currentGroup !== state.groups[state.currentGroupIndex]) {
+ state.currentGroup = state.groups[state.currentGroupIndex];
+ }
+ });
+ const focus = (id) => {
+ shadowRoot.getElementById(id).focus();
+ };
+ const emojiToDomNode = (emoji) => shadowRoot.getElementById(`emo-${emoji.id}`);
+ const fireEvent = (name, detail) => {
+ refs.rootElement.dispatchEvent(new CustomEvent(name, {
+ detail,
+ bubbles: true,
+ composed: true
+ }));
+ };
+ const compareEmojiArrays = (a, b) => a.id === b.id;
+ const compareCurrentEmojisWithCategories = (a, b) => {
+ const { category: aCategory, emojis: aEmojis } = a;
+ const { category: bCategory, emojis: bEmojis } = b;
+ if (aCategory !== bCategory) {
+ return false;
+ }
+ return arraysAreEqualByFunction(aEmojis, bEmojis, compareEmojiArrays);
+ };
+ const updateCurrentEmojis = (newEmojis) => {
+ if (!arraysAreEqualByFunction(state.currentEmojis, newEmojis, compareEmojiArrays)) {
+ state.currentEmojis = newEmojis;
+ }
+ };
+ const updateSearchMode = (newSearchMode) => {
+ if (state.searchMode !== newSearchMode) {
+ state.searchMode = newSearchMode;
+ }
+ };
+ const updateCurrentEmojisWithCategories = (newEmojisWithCategories) => {
+ if (!arraysAreEqualByFunction(state.currentEmojisWithCategories, newEmojisWithCategories, compareCurrentEmojisWithCategories)) {
+ state.currentEmojisWithCategories = newEmojisWithCategories;
+ }
+ };
+ const unicodeWithSkin = (emoji, currentSkinTone) => currentSkinTone && emoji.skins && emoji.skins[currentSkinTone] || emoji.unicode;
+ const labelWithSkin = (emoji, currentSkinTone) => uniq([
+ emoji.name || unicodeWithSkin(emoji, currentSkinTone),
+ emoji.annotation,
+ ...emoji.shortcodes || EMPTY_ARRAY
+ ].filter(Boolean)).join(", ");
+ const titleForEmoji = (emoji) => emoji.annotation || (emoji.shortcodes || EMPTY_ARRAY).join(", ");
+ const helpers = {
+ labelWithSkin,
+ titleForEmoji,
+ unicodeWithSkin
+ };
+ const events = {
+ onClickSkinToneButton,
+ onEmojiClick,
+ onNavClick,
+ onNavKeydown,
+ onSearchKeydown,
+ onSkinToneOptionsClick,
+ onSkinToneOptionsFocusOut,
+ onSkinToneOptionsKeydown,
+ onSkinToneOptionsKeyup,
+ onSearchInput
+ };
+ const actions = {
+ calculateEmojiGridStyle,
+ updateOnIntersection
+ };
+ let firstRender = true;
+ createEffect(() => {
+ render(shadowRoot, state, helpers, events, actions, refs, abortSignal, actionContext, firstRender);
+ firstRender = false;
+ });
+ if (!state.emojiVersion) {
+ detectEmojiSupportLevel().then((level) => {
+ if (!level) {
+ state.message = state.i18n.emojiUnsupportedMessage;
+ }
+ });
+ }
+ createEffect(() => {
+ async function handleDatabaseLoading() {
+ let showingLoadingMessage = false;
+ const timeoutHandle = setTimeout(() => {
+ showingLoadingMessage = true;
+ state.message = state.i18n.loadingMessage;
+ }, TIMEOUT_BEFORE_LOADING_MESSAGE);
+ try {
+ await state.database.ready();
+ state.databaseLoaded = true;
+ } catch (err) {
+ console.error(err);
+ state.message = state.i18n.networkErrorMessage;
+ } finally {
+ clearTimeout(timeoutHandle);
+ if (showingLoadingMessage) {
+ showingLoadingMessage = false;
+ state.message = "";
+ }
+ }
+ }
+ if (state.database) {
+ handleDatabaseLoading();
+ }
+ });
+ createEffect(() => {
+ state.pickerStyle = `
+ --num-groups: ${state.groups.length};
+ --indicator-opacity: ${state.searchMode ? 0 : 1};
+ --num-skintones: ${NUM_SKIN_TONES};`;
+ });
+ createEffect(() => {
+ if (state.customEmoji && state.database) {
+ updateCustomEmoji();
+ }
+ });
+ createEffect(() => {
+ if (state.customEmoji && state.customEmoji.length) {
+ if (state.groups !== allGroups) {
+ state.groups = allGroups;
+ }
+ } else if (state.groups !== groups) {
+ if (state.currentGroupIndex) {
+ state.currentGroupIndex--;
+ }
+ state.groups = groups;
+ }
+ });
+ createEffect(() => {
+ async function updatePreferredSkinTone() {
+ if (state.databaseLoaded) {
+ state.currentSkinTone = await state.database.getPreferredSkinTone();
+ }
+ }
+ updatePreferredSkinTone();
+ });
+ createEffect(() => {
+ state.skinTones = Array(NUM_SKIN_TONES).fill().map((_, i) => applySkinTone(state.skinToneEmoji, i));
+ });
+ createEffect(() => {
+ state.skinToneButtonText = state.skinTones[state.currentSkinTone];
+ });
+ createEffect(() => {
+ state.skinToneButtonLabel = state.i18n.skinToneLabel.replace("{skinTone}", state.i18n.skinTones[state.currentSkinTone]);
+ });
+ createEffect(() => {
+ async function updateDefaultFavoriteEmojis() {
+ const { database } = state;
+ const favs = (await Promise.all(MOST_COMMONLY_USED_EMOJI.map((unicode) => database.getEmojiByUnicodeOrName(unicode)))).filter(Boolean);
+ state.defaultFavoriteEmojis = favs;
+ }
+ if (state.databaseLoaded) {
+ updateDefaultFavoriteEmojis();
+ }
+ });
+ function updateCustomEmoji() {
+ const { customEmoji, database } = state;
+ const databaseCustomEmoji = customEmoji || EMPTY_ARRAY;
+ if (database.customEmoji !== databaseCustomEmoji) {
+ database.customEmoji = databaseCustomEmoji;
+ }
+ }
+ createEffect(() => {
+ async function updateFavorites() {
+ updateCustomEmoji();
+ const { database, defaultFavoriteEmojis, numColumns } = state;
+ const dbFavorites = await database.getTopFavoriteEmoji(numColumns);
+ const favorites = await summarizeEmojis(uniqBy2([
+ ...dbFavorites,
+ ...defaultFavoriteEmojis
+ ], (_) => _.unicode || _.name).slice(0, numColumns));
+ state.currentFavorites = favorites;
+ }
+ if (state.databaseLoaded && state.defaultFavoriteEmojis) {
+ updateFavorites();
+ }
+ });
+ function calculateEmojiGridStyle(node) {
+ resizeObserverAction(node, abortSignal, () => {
+ {
+ const style = getComputedStyle(refs.rootElement);
+ const newNumColumns = parseInt(style.getPropertyValue("--num-columns"), 10);
+ const newIsRtl = style.getPropertyValue("direction") === "rtl";
+ state.numColumns = newNumColumns;
+ state.isRtl = newIsRtl;
+ }
+ });
+ }
+ function updateOnIntersection(node) {
+ intersectionObserverAction(node, abortSignal, (entries) => {
+ for (const { target, isIntersecting } of entries) {
+ target.classList.toggle("onscreen", isIntersecting);
+ }
+ });
+ }
+ createEffect(() => {
+ async function updateEmojis() {
+ const { searchText, currentGroup, databaseLoaded, customEmoji } = state;
+ if (!databaseLoaded) {
+ state.currentEmojis = [];
+ state.searchMode = false;
+ } else if (searchText.length >= MIN_SEARCH_TEXT_LENGTH2) {
+ const newEmojis = await getEmojisBySearchQuery(searchText);
+ if (state.searchText === searchText) {
+ updateCurrentEmojis(newEmojis);
+ updateSearchMode(true);
+ }
+ } else {
+ const { id: currentGroupId } = currentGroup;
+ if (currentGroupId !== -1 || customEmoji && customEmoji.length) {
+ const newEmojis = await getEmojisByGroup(currentGroupId);
+ if (state.currentGroup.id === currentGroupId) {
+ updateCurrentEmojis(newEmojis);
+ updateSearchMode(false);
+ }
+ }
+ }
+ }
+ updateEmojis();
+ });
+ const resetScrollTopInRaf = () => {
+ rAF(() => resetScrollTopIfPossible(refs.tabpanelElement));
+ };
+ createEffect(() => {
+ const { currentEmojis, emojiVersion } = state;
+ const zwjEmojisToCheck = currentEmojis.filter((emoji) => emoji.unicode).filter((emoji) => hasZwj(emoji) && !supportedZwjEmojis.has(emoji.unicode));
+ if (!emojiVersion && zwjEmojisToCheck.length) {
+ updateCurrentEmojis(currentEmojis);
+ rAF(() => checkZwjSupportAndUpdate(zwjEmojisToCheck));
+ } else {
+ const newEmojis = emojiVersion ? currentEmojis : currentEmojis.filter(isZwjSupported);
+ updateCurrentEmojis(newEmojis);
+ resetScrollTopInRaf();
+ }
+ });
+ function checkZwjSupportAndUpdate(zwjEmojisToCheck) {
+ const allSupported = checkZwjSupport(zwjEmojisToCheck, refs.baselineEmoji, emojiToDomNode);
+ if (allSupported) {
+ resetScrollTopInRaf();
+ } else {
+ state.currentEmojis = [...state.currentEmojis];
+ }
+ }
+ function isZwjSupported(emoji) {
+ return !emoji.unicode || !hasZwj(emoji) || supportedZwjEmojis.get(emoji.unicode);
+ }
+ async function filterEmojisByVersion(emojis) {
+ const emojiSupportLevel = state.emojiVersion || await detectEmojiSupportLevel();
+ return emojis.filter(({ version }) => !version || version <= emojiSupportLevel);
+ }
+ async function summarizeEmojis(emojis) {
+ return summarizeEmojisForUI(emojis, state.emojiVersion || await detectEmojiSupportLevel());
+ }
+ async function getEmojisByGroup(group) {
+ const emoji = group === -1 ? state.customEmoji : await state.database.getEmojiByGroup(group);
+ return summarizeEmojis(await filterEmojisByVersion(emoji));
+ }
+ async function getEmojisBySearchQuery(query) {
+ return summarizeEmojis(await filterEmojisByVersion(await state.database.getEmojiBySearchQuery(query)));
+ }
+ createEffect(() => {
+ });
+ createEffect(() => {
+ function calculateCurrentEmojisWithCategories() {
+ const { searchMode, currentEmojis } = state;
+ if (searchMode) {
+ return [
+ {
+ category: "",
+ emojis: currentEmojis
+ }
+ ];
+ }
+ const categoriesToEmoji = /* @__PURE__ */ new Map();
+ for (const emoji of currentEmojis) {
+ const category = emoji.category || "";
+ let emojis = categoriesToEmoji.get(category);
+ if (!emojis) {
+ emojis = [];
+ categoriesToEmoji.set(category, emojis);
+ }
+ emojis.push(emoji);
+ }
+ return [...categoriesToEmoji.entries()].map(([category, emojis]) => ({ category, emojis })).sort((a, b) => state.customCategorySorting(a.category, b.category));
+ }
+ const newEmojisWithCategories = calculateCurrentEmojisWithCategories();
+ updateCurrentEmojisWithCategories(newEmojisWithCategories);
+ });
+ createEffect(() => {
+ state.activeSearchItemId = state.activeSearchItem !== -1 && state.currentEmojis[state.activeSearchItem].id;
+ });
+ createEffect(() => {
+ const { rawSearchText } = state;
+ rIC(() => {
+ state.searchText = (rawSearchText || "").trim();
+ state.activeSearchItem = -1;
+ });
+ });
+ function onSearchKeydown(event) {
+ if (!state.searchMode || !state.currentEmojis.length) {
+ return;
+ }
+ const goToNextOrPrevious = (previous) => {
+ halt(event);
+ state.activeSearchItem = incrementOrDecrement(previous, state.activeSearchItem, state.currentEmojis);
+ };
+ switch (event.key) {
+ case "ArrowDown":
+ return goToNextOrPrevious(false);
+ case "ArrowUp":
+ return goToNextOrPrevious(true);
+ case "Enter":
+ if (state.activeSearchItem === -1) {
+ state.activeSearchItem = 0;
+ } else {
+ halt(event);
+ return clickEmoji(state.currentEmojis[state.activeSearchItem].id);
+ }
+ }
+ }
+ function onNavClick(event) {
+ const { target } = event;
+ const closestTarget = target.closest(".nav-button");
+ if (!closestTarget) {
+ return;
+ }
+ const groupId = parseInt(closestTarget.dataset.groupId, 10);
+ refs.searchElement.value = "";
+ state.rawSearchText = "";
+ state.searchText = "";
+ state.activeSearchItem = -1;
+ state.currentGroupIndex = state.groups.findIndex((_) => _.id === groupId);
+ }
+ function onNavKeydown(event) {
+ const { target, key } = event;
+ const doFocus = (el) => {
+ if (el) {
+ halt(event);
+ el.focus();
+ }
+ };
+ switch (key) {
+ case "ArrowLeft":
+ return doFocus(target.previousElementSibling);
+ case "ArrowRight":
+ return doFocus(target.nextElementSibling);
+ case "Home":
+ return doFocus(target.parentElement.firstElementChild);
+ case "End":
+ return doFocus(target.parentElement.lastElementChild);
+ }
+ }
+ async function clickEmoji(unicodeOrName) {
+ const emoji = await state.database.getEmojiByUnicodeOrName(unicodeOrName);
+ const emojiSummary = [...state.currentEmojis, ...state.currentFavorites].find((_) => _.id === unicodeOrName);
+ const skinTonedUnicode = emojiSummary.unicode && unicodeWithSkin(emojiSummary, state.currentSkinTone);
+ await state.database.incrementFavoriteEmojiCount(unicodeOrName);
+ fireEvent("emoji-click", {
+ emoji,
+ skinTone: state.currentSkinTone,
+ ...skinTonedUnicode && { unicode: skinTonedUnicode },
+ ...emojiSummary.name && { name: emojiSummary.name }
+ });
+ }
+ async function onEmojiClick(event) {
+ const { target } = event;
+ if (!target.classList.contains("emoji")) {
+ return;
+ }
+ halt(event);
+ const id = target.id.substring(4);
+ clickEmoji(id);
+ }
+ function changeSkinTone(skinTone) {
+ state.currentSkinTone = skinTone;
+ state.skinTonePickerExpanded = false;
+ focus("skintone-button");
+ fireEvent("skin-tone-change", { skinTone });
+ state.database.setPreferredSkinTone(skinTone);
+ }
+ function onSkinToneOptionsClick(event) {
+ const { target: { id } } = event;
+ const match = id && id.match(/^skintone-(\d)/);
+ if (!match) {
+ return;
+ }
+ halt(event);
+ const skinTone = parseInt(match[1], 10);
+ changeSkinTone(skinTone);
+ }
+ function onClickSkinToneButton(event) {
+ state.skinTonePickerExpanded = !state.skinTonePickerExpanded;
+ state.activeSkinTone = state.currentSkinTone;
+ if (state.skinTonePickerExpanded) {
+ halt(event);
+ rAF(() => focus("skintone-list"));
+ }
+ }
+ createEffect(() => {
+ if (state.skinTonePickerExpanded) {
+ refs.skinToneDropdown.addEventListener("transitionend", () => {
+ state.skinTonePickerExpandedAfterAnimation = true;
+ }, { once: true });
+ } else {
+ state.skinTonePickerExpandedAfterAnimation = false;
+ }
+ });
+ function onSkinToneOptionsKeydown(event) {
+ if (!state.skinTonePickerExpanded) {
+ return;
+ }
+ const changeActiveSkinTone = async (nextSkinTone) => {
+ halt(event);
+ state.activeSkinTone = nextSkinTone;
+ };
+ switch (event.key) {
+ case "ArrowUp":
+ return changeActiveSkinTone(incrementOrDecrement(true, state.activeSkinTone, state.skinTones));
+ case "ArrowDown":
+ return changeActiveSkinTone(incrementOrDecrement(false, state.activeSkinTone, state.skinTones));
+ case "Home":
+ return changeActiveSkinTone(0);
+ case "End":
+ return changeActiveSkinTone(state.skinTones.length - 1);
+ case "Enter":
+ halt(event);
+ return changeSkinTone(state.activeSkinTone);
+ case "Escape":
+ halt(event);
+ state.skinTonePickerExpanded = false;
+ return focus("skintone-button");
+ }
+ }
+ function onSkinToneOptionsKeyup(event) {
+ if (!state.skinTonePickerExpanded) {
+ return;
+ }
+ switch (event.key) {
+ case " ":
+ halt(event);
+ return changeSkinTone(state.activeSkinTone);
+ }
+ }
+ async function onSkinToneOptionsFocusOut(event) {
+ const { relatedTarget } = event;
+ if (!relatedTarget || relatedTarget.id !== "skintone-list") {
+ state.skinTonePickerExpanded = false;
+ }
+ }
+ function onSearchInput(event) {
+ state.rawSearchText = event.target.value;
+ }
+ return {
+ $set(newState) {
+ assign(state, newState);
+ },
+ $destroy() {
+ abortController.abort();
+ }
+ };
+}
+var DEFAULT_DATA_SOURCE2 = "https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json";
+var DEFAULT_LOCALE2 = "en";
+var enI18n = {
+ categoriesLabel: "Categories",
+ emojiUnsupportedMessage: "Your browser does not support color emoji.",
+ favoritesLabel: "Favorites",
+ loadingMessage: "Loading…",
+ networkErrorMessage: "Could not load emoji.",
+ regionLabel: "Emoji picker",
+ searchDescription: "When search results are available, press up or down to select and enter to choose.",
+ searchLabel: "Search",
+ searchResultsLabel: "Search results",
+ skinToneDescription: "When expanded, press up or down to select and enter to choose.",
+ skinToneLabel: "Choose a skin tone (currently {skinTone})",
+ skinTonesLabel: "Skin tones",
+ skinTones: [
+ "Default",
+ "Light",
+ "Medium-Light",
+ "Medium",
+ "Medium-Dark",
+ "Dark"
+ ],
+ categories: {
+ custom: "Custom",
+ "smileys-emotion": "Smileys and emoticons",
+ "people-body": "People and body",
+ "animals-nature": "Animals and nature",
+ "food-drink": "Food and drink",
+ "travel-places": "Travel and places",
+ activities: "Activities",
+ objects: "Objects",
+ symbols: "Symbols",
+ flags: "Flags"
+ }
+};
+var baseStyles = ':host{--emoji-size:1.375rem;--emoji-padding:0.5rem;--category-emoji-size:var(--emoji-size);--category-emoji-padding:var(--emoji-padding);--indicator-height:3px;--input-border-radius:0.5rem;--input-border-size:1px;--input-font-size:1rem;--input-line-height:1.5;--input-padding:0.25rem;--num-columns:8;--outline-size:2px;--border-size:1px;--border-radius:0;--skintone-border-radius:1rem;--category-font-size:1rem;display:flex;width:min-content;height:400px}:host,:host(.light){color-scheme:light;--background:#fff;--border-color:#e0e0e0;--indicator-color:#385ac1;--input-border-color:#999;--input-font-color:#111;--input-placeholder-color:#999;--outline-color:#999;--category-font-color:#111;--button-active-background:#e6e6e6;--button-hover-background:#d9d9d9}:host(.dark){color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}@media (prefers-color-scheme:dark){:host{color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}}:host([hidden]){display:none}button{margin:0;padding:0;border:0;background:0 0;box-shadow:none;-webkit-tap-highlight-color:transparent}button::-moz-focus-inner{border:0}input{padding:0;margin:0;line-height:1.15;font-family:inherit}input[type=search]{-webkit-appearance:none}:focus{outline:var(--outline-color) solid var(--outline-size);outline-offset:calc(-1*var(--outline-size))}:host([data-js-focus-visible]) :focus:not([data-focus-visible-added]){outline:0}:focus:not(:focus-visible){outline:0}.hide-focus{outline:0}*{box-sizing:border-box}.picker{contain:content;display:flex;flex-direction:column;background:var(--background);border:var(--border-size) solid var(--border-color);border-radius:var(--border-radius);width:100%;height:100%;overflow:hidden;--total-emoji-size:calc(var(--emoji-size) + (2 * var(--emoji-padding)));--total-category-emoji-size:calc(var(--category-emoji-size) + (2 * var(--category-emoji-padding)))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.hidden{opacity:0;pointer-events:none}.abs-pos{position:absolute;left:0;top:0}.gone{display:none!important}.skintone-button-wrapper,.skintone-list{background:var(--background);z-index:3}.skintone-button-wrapper.expanded{z-index:1}.skintone-list{position:absolute;inset-inline-end:0;top:0;z-index:2;overflow:visible;border-bottom:var(--border-size) solid var(--border-color);border-radius:0 0 var(--skintone-border-radius) var(--skintone-border-radius);will-change:transform;transition:transform .2s ease-in-out;transform-origin:center 0}@media (prefers-reduced-motion:reduce){.skintone-list{transition-duration:.001s}}@supports not (inset-inline-end:0){.skintone-list{right:0}}.skintone-list.no-animate{transition:none}.tabpanel{overflow-y:auto;scrollbar-gutter:stable;-webkit-overflow-scrolling:touch;will-change:transform;min-height:0;flex:1;contain:content}.emoji-menu{display:grid;grid-template-columns:repeat(var(--num-columns),var(--total-emoji-size));justify-content:space-around;align-items:flex-start;width:100%}.emoji-menu.visibility-auto{content-visibility:auto;contain-intrinsic-size:calc(var(--num-columns)*var(--total-emoji-size)) calc(var(--num-rows)*var(--total-emoji-size))}.category{padding:var(--emoji-padding);font-size:var(--category-font-size);color:var(--category-font-color)}.emoji,button.emoji{font-size:var(--emoji-size);display:flex;align-items:center;justify-content:center;border-radius:100%;height:var(--total-emoji-size);width:var(--total-emoji-size);line-height:1;overflow:hidden;font-family:var(--emoji-font-family);cursor:pointer}@media (hover:hover) and (pointer:fine){.emoji:hover,button.emoji:hover{background:var(--button-hover-background)}}.emoji.active,.emoji:active,button.emoji.active,button.emoji:active{background:var(--button-active-background)}.onscreen .custom-emoji::after{content:"";width:var(--emoji-size);height:var(--emoji-size);background-repeat:no-repeat;background-position:center center;background-size:contain;background-image:var(--custom-emoji-background)}.nav,.nav-button{align-items:center}.nav{display:grid;justify-content:space-between;contain:content}.nav-button{display:flex;justify-content:center}.nav-emoji{font-size:var(--category-emoji-size);width:var(--total-category-emoji-size);height:var(--total-category-emoji-size)}.indicator-wrapper{display:flex;border-bottom:1px solid var(--border-color)}.indicator{width:calc(100%/var(--num-groups));height:var(--indicator-height);opacity:var(--indicator-opacity);background-color:var(--indicator-color);will-change:transform,opacity;transition:opacity .1s linear,transform .25s ease-in-out}@media (prefers-reduced-motion:reduce){.indicator{will-change:opacity;transition:opacity .1s linear}}.pad-top,input.search{background:var(--background);width:100%}.pad-top{height:var(--emoji-padding);z-index:3}.search-row{display:flex;align-items:center;position:relative;padding-inline-start:var(--emoji-padding);padding-bottom:var(--emoji-padding)}.search-wrapper{flex:1;min-width:0}input.search{padding:var(--input-padding);border-radius:var(--input-border-radius);border:var(--input-border-size) solid var(--input-border-color);color:var(--input-font-color);font-size:var(--input-font-size);line-height:var(--input-line-height)}input.search::placeholder{color:var(--input-placeholder-color)}.favorites{overflow-y:auto;scrollbar-gutter:stable;display:flex;flex-direction:row;border-top:var(--border-size) solid var(--border-color);contain:content}.message{padding:var(--emoji-padding)}';
+var PROPS = [
+ "customEmoji",
+ "customCategorySorting",
+ "database",
+ "dataSource",
+ "i18n",
+ "locale",
+ "skinToneEmoji",
+ "emojiVersion"
+];
+var EXTRA_STYLES = `:host{--emoji-font-family:${FONT_FAMILY}}`;
+var PickerElement = class extends HTMLElement {
+ constructor(props) {
+ super();
+ this.attachShadow({ mode: "open" });
+ const style = document.createElement("style");
+ style.textContent = baseStyles + EXTRA_STYLES;
+ this.shadowRoot.appendChild(style);
+ this._ctx = {
+ // Set defaults
+ locale: DEFAULT_LOCALE2,
+ dataSource: DEFAULT_DATA_SOURCE2,
+ skinToneEmoji: DEFAULT_SKIN_TONE_EMOJI,
+ customCategorySorting: DEFAULT_CATEGORY_SORTING,
+ customEmoji: null,
+ i18n: enI18n,
+ emojiVersion: null,
+ ...props
+ };
+ for (const prop of PROPS) {
+ if (prop !== "database" && Object.prototype.hasOwnProperty.call(this, prop)) {
+ this._ctx[prop] = this[prop];
+ delete this[prop];
+ }
+ }
+ this._dbFlush();
+ }
+ connectedCallback() {
+ if (!this._cmp) {
+ this._cmp = createRoot(this.shadowRoot, this._ctx);
+ }
+ }
+ disconnectedCallback() {
+ qM(() => {
+ if (!this.isConnected && this._cmp) {
+ this._cmp.$destroy();
+ this._cmp = void 0;
+ const { database } = this._ctx;
+ database.close().catch((err) => console.error(err));
+ }
+ });
+ }
+ static get observedAttributes() {
+ return ["locale", "data-source", "skin-tone-emoji", "emoji-version"];
+ }
+ attributeChangedCallback(attrName, oldValue, newValue) {
+ this._set(
+ // convert from kebab-case to camelcase
+ // see https://github.com/sveltejs/svelte/issues/3852#issuecomment-665037015
+ attrName.replace(/-([a-z])/g, (_, up) => up.toUpperCase()),
+ // convert string attribute to float if necessary
+ attrName === "emoji-version" ? parseFloat(newValue) : newValue
+ );
+ }
+ _set(prop, newValue) {
+ this._ctx[prop] = newValue;
+ if (this._cmp) {
+ this._cmp.$set({ [prop]: newValue });
+ }
+ if (["locale", "dataSource"].includes(prop)) {
+ this._dbFlush();
+ }
+ }
+ _dbCreate() {
+ const { locale, dataSource, database } = this._ctx;
+ if (!database || database.locale !== locale || database.dataSource !== dataSource) {
+ this._set("database", new Database({ locale, dataSource }));
+ }
+ }
+ // Update the Database in one microtask if the locale/dataSource change. We do one microtask
+ // so we don't create two Databases if e.g. both the locale and the dataSource change
+ _dbFlush() {
+ qM(() => this._dbCreate());
+ }
+};
+var definitions = {};
+for (const prop of PROPS) {
+ definitions[prop] = {
+ get() {
+ if (prop === "database") {
+ this._dbCreate();
+ }
+ return this._ctx[prop];
+ },
+ set(val) {
+ if (prop === "database") {
+ throw new Error("database is read-only");
+ }
+ this._set(prop, val);
+ }
+ };
+}
+Object.defineProperties(PickerElement.prototype, definitions);
+if (!customElements.get("emoji-picker")) {
+ customElements.define("emoji-picker", PickerElement);
+}
+export {
+ Database,
+ PickerElement as Picker
+};
+//# sourceMappingURL=emoji-picker-element.js.map
diff --git a/node_modules/.vite/deps/emoji-picker-element.js.map b/node_modules/.vite/deps/emoji-picker-element.js.map
new file mode 100644
index 0000000..14e6720
--- /dev/null
+++ b/node_modules/.vite/deps/emoji-picker-element.js.map
@@ -0,0 +1,7 @@
+{
+ "version": 3,
+ "sources": ["../../emoji-picker-element/database.js", "../../emoji-picker-element/picker.js"],
+ "sourcesContent": ["function assertNonEmptyString (str) {\n if (typeof str !== 'string' || !str) {\n throw new Error('expected a non-empty string, got: ' + str)\n }\n}\n\nfunction assertNumber (number) {\n if (typeof number !== 'number') {\n throw new Error('expected a number, got: ' + number)\n }\n}\n\nconst DB_VERSION_CURRENT = 1;\nconst DB_VERSION_INITIAL = 1;\nconst STORE_EMOJI = 'emoji';\nconst STORE_KEYVALUE = 'keyvalue';\nconst STORE_FAVORITES = 'favorites';\nconst FIELD_TOKENS = 'tokens';\nconst INDEX_TOKENS = 'tokens';\nconst FIELD_UNICODE = 'unicode';\nconst INDEX_COUNT = 'count';\nconst FIELD_GROUP = 'group';\nconst FIELD_ORDER = 'order';\nconst INDEX_GROUP_AND_ORDER = 'group-order';\nconst KEY_ETAG = 'eTag';\nconst KEY_URL = 'url';\nconst KEY_PREFERRED_SKINTONE = 'skinTone';\nconst MODE_READONLY = 'readonly';\nconst MODE_READWRITE = 'readwrite';\nconst INDEX_SKIN_UNICODE = 'skinUnicodes';\nconst FIELD_SKIN_UNICODE = 'skinUnicodes';\n\nconst DEFAULT_DATA_SOURCE = 'https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json';\nconst DEFAULT_LOCALE = 'en';\n\n// like lodash's uniqBy but much smaller\nfunction uniqBy (arr, func) {\n const set = new Set();\n const res = [];\n for (const item of arr) {\n const key = func(item);\n if (!set.has(key)) {\n set.add(key);\n res.push(item);\n }\n }\n return res\n}\n\nfunction uniqEmoji (emojis) {\n return uniqBy(emojis, _ => _.unicode)\n}\n\nfunction initialMigration (db) {\n function createObjectStore (name, keyPath, indexes) {\n const store = keyPath\n ? db.createObjectStore(name, { keyPath })\n : db.createObjectStore(name);\n if (indexes) {\n for (const [indexName, [keyPath, multiEntry]] of Object.entries(indexes)) {\n store.createIndex(indexName, keyPath, { multiEntry });\n }\n }\n return store\n }\n\n createObjectStore(STORE_KEYVALUE);\n createObjectStore(STORE_EMOJI, /* keyPath */ FIELD_UNICODE, {\n [INDEX_TOKENS]: [FIELD_TOKENS, /* multiEntry */ true],\n [INDEX_GROUP_AND_ORDER]: [[FIELD_GROUP, FIELD_ORDER]],\n [INDEX_SKIN_UNICODE]: [FIELD_SKIN_UNICODE, /* multiEntry */ true]\n });\n createObjectStore(STORE_FAVORITES, undefined, {\n [INDEX_COUNT]: ['']\n });\n}\n\nconst openIndexedDBRequests = {};\nconst databaseCache = {};\nconst onCloseListeners = {};\n\nfunction handleOpenOrDeleteReq (resolve, reject, req) {\n // These things are almost impossible to test with fakeIndexedDB sadly\n /* istanbul ignore next */\n req.onerror = () => reject(req.error);\n /* istanbul ignore next */\n req.onblocked = () => reject(new Error('IDB blocked'));\n req.onsuccess = () => resolve(req.result);\n}\n\nasync function createDatabase (dbName) {\n const db = await new Promise((resolve, reject) => {\n const req = indexedDB.open(dbName, DB_VERSION_CURRENT);\n openIndexedDBRequests[dbName] = req;\n req.onupgradeneeded = e => {\n // Technically there is only one version, so we don't need this `if` check\n // But if an old version of the JS is in another browser tab\n // and it gets upgraded in the future and we have a new DB version, well...\n // better safe than sorry.\n /* istanbul ignore else */\n if (e.oldVersion < DB_VERSION_INITIAL) {\n initialMigration(req.result);\n }\n };\n handleOpenOrDeleteReq(resolve, reject, req);\n });\n // Handle abnormal closes, e.g. \"delete database\" in chrome dev tools.\n // No need for removeEventListener, because once the DB can no longer\n // fire \"close\" events, it will auto-GC.\n // Unfortunately cannot test in fakeIndexedDB: https://github.com/dumbmatter/fakeIndexedDB/issues/50\n /* istanbul ignore next */\n db.onclose = () => closeDatabase(dbName);\n return db\n}\n\nfunction openDatabase (dbName) {\n if (!databaseCache[dbName]) {\n databaseCache[dbName] = createDatabase(dbName);\n }\n return databaseCache[dbName]\n}\n\nfunction dbPromise (db, storeName, readOnlyOrReadWrite, cb) {\n return new Promise((resolve, reject) => {\n // Use relaxed durability because neither the emoji data nor the favorites/preferred skin tone\n // are really irreplaceable data. IndexedDB is just a cache in this case.\n const txn = db.transaction(storeName, readOnlyOrReadWrite, { durability: 'relaxed' });\n const store = typeof storeName === 'string'\n ? txn.objectStore(storeName)\n : storeName.map(name => txn.objectStore(name));\n let res;\n cb(store, txn, (result) => {\n res = result;\n });\n\n txn.oncomplete = () => resolve(res);\n /* istanbul ignore next */\n txn.onerror = () => reject(txn.error);\n })\n}\n\nfunction closeDatabase (dbName) {\n // close any open requests\n const req = openIndexedDBRequests[dbName];\n const db = req && req.result;\n if (db) {\n db.close();\n const listeners = onCloseListeners[dbName];\n /* istanbul ignore else */\n if (listeners) {\n for (const listener of listeners) {\n listener();\n }\n }\n }\n delete openIndexedDBRequests[dbName];\n delete databaseCache[dbName];\n delete onCloseListeners[dbName];\n}\n\nfunction deleteDatabase (dbName) {\n return new Promise((resolve, reject) => {\n // close any open requests\n closeDatabase(dbName);\n const req = indexedDB.deleteDatabase(dbName);\n handleOpenOrDeleteReq(resolve, reject, req);\n })\n}\n\n// The \"close\" event occurs during an abnormal shutdown, e.g. a user clearing their browser data.\n// However, it doesn't occur with the normal \"close\" event, so we handle that separately.\n// https://www.w3.org/TR/IndexedDB/#close-a-database-connection\nfunction addOnCloseListener (dbName, listener) {\n let listeners = onCloseListeners[dbName];\n if (!listeners) {\n listeners = onCloseListeners[dbName] = [];\n }\n listeners.push(listener);\n}\n\n// list of emoticons that don't match a simple \\W+ regex\n// extracted using:\n// require('emoji-picker-element-data/en/emojibase/data.json').map(_ => _.emoticon).filter(Boolean).filter(_ => !/^\\W+$/.test(_))\nconst irregularEmoticons = new Set([\n ':D', 'XD', \":'D\", 'O:)',\n ':X', ':P', ';P', 'XP',\n ':L', ':Z', ':j', '8D',\n 'XO', '8)', ':B', ':O',\n ':S', \":'o\", 'Dx', 'X(',\n 'D:', ':C', '>0)', ':3',\n '3', '<3', '\\\\M/', ':E',\n '8#'\n]);\n\nfunction extractTokens (str) {\n return str\n .split(/[\\s_]+/)\n .map(word => {\n if (!word.match(/\\w/) || irregularEmoticons.has(word)) {\n // for pure emoticons like :) or :-), just leave them as-is\n return word.toLowerCase()\n }\n\n return word\n .replace(/[)(:,]/g, '')\n .replace(/’/g, \"'\")\n .toLowerCase()\n }).filter(Boolean)\n}\n\nconst MIN_SEARCH_TEXT_LENGTH = 2;\n\n// This is an extra step in addition to extractTokens(). The difference here is that we expect\n// the input to have already been run through extractTokens(). This is useful for cases like\n// emoticons, where we don't want to do any tokenization (because it makes no sense to split up\n// \">:)\" by the colon) but we do want to lowercase it to have consistent search results, so that\n// the user can type ':P' or ':p' and still get the same result.\nfunction normalizeTokens (str) {\n return str\n .filter(Boolean)\n .map(_ => _.toLowerCase())\n .filter(_ => _.length >= MIN_SEARCH_TEXT_LENGTH)\n}\n\n// Transform emoji data for storage in IDB\nfunction transformEmojiData (emojiData) {\n const res = emojiData.map(({ annotation, emoticon, group, order, shortcodes, skins, tags, emoji, version }) => {\n const tokens = [...new Set(\n normalizeTokens([\n ...(shortcodes || []).map(extractTokens).flat(),\n ...(tags || []).map(extractTokens).flat(),\n ...extractTokens(annotation),\n emoticon\n ])\n )].sort();\n const res = {\n annotation,\n group,\n order,\n tags,\n tokens,\n unicode: emoji,\n version\n };\n if (emoticon) {\n res.emoticon = emoticon;\n }\n if (shortcodes) {\n res.shortcodes = shortcodes;\n }\n if (skins) {\n res.skinTones = [];\n res.skinUnicodes = [];\n res.skinVersions = [];\n for (const { tone, emoji, version } of skins) {\n res.skinTones.push(tone);\n res.skinUnicodes.push(emoji);\n res.skinVersions.push(version);\n }\n }\n return res\n });\n return res\n}\n\n// helper functions that help compress the code better\n\nfunction callStore (store, method, key, cb) {\n store[method](key).onsuccess = e => (cb && cb(e.target.result));\n}\n\nfunction getIDB (store, key, cb) {\n callStore(store, 'get', key, cb);\n}\n\nfunction getAllIDB (store, key, cb) {\n callStore(store, 'getAll', key, cb);\n}\n\nfunction commit (txn) {\n /* istanbul ignore else */\n if (txn.commit) {\n txn.commit();\n }\n}\n\n// like lodash's minBy\nfunction minBy (array, func) {\n let minItem = array[0];\n for (let i = 1; i < array.length; i++) {\n const item = array[i];\n if (func(minItem) > func(item)) {\n minItem = item;\n }\n }\n return minItem\n}\n\n// return an array of results representing all items that are found in each one of the arrays\n//\n\nfunction findCommonMembers (arrays, uniqByFunc) {\n const shortestArray = minBy(arrays, _ => _.length);\n const results = [];\n for (const item of shortestArray) {\n // if this item is included in every array in the intermediate results, add it to the final results\n if (!arrays.some(array => array.findIndex(_ => uniqByFunc(_) === uniqByFunc(item)) === -1)) {\n results.push(item);\n }\n }\n return results\n}\n\nasync function isEmpty (db) {\n return !(await get(db, STORE_KEYVALUE, KEY_URL))\n}\n\nasync function hasData (db, url, eTag) {\n const [oldETag, oldUrl] = await Promise.all([KEY_ETAG, KEY_URL]\n .map(key => get(db, STORE_KEYVALUE, key)));\n return (oldETag === eTag && oldUrl === url)\n}\n\nasync function doFullDatabaseScanForSingleResult (db, predicate) {\n // This batching algorithm is just a perf improvement over a basic\n // cursor. The BATCH_SIZE is an estimate of what would give the best\n // perf for doing a full DB scan (worst case).\n //\n // Mini-benchmark for determining the best batch size:\n //\n // PERF=1 pnpm build:rollup && pnpm test:adhoc\n //\n // (async () => {\n // performance.mark('start')\n // await $('emoji-picker').database.getEmojiByShortcode('doesnotexist')\n // performance.measure('total', 'start')\n // console.log(performance.getEntriesByName('total').slice(-1)[0].duration)\n // })()\n const BATCH_SIZE = 50; // Typically around 150ms for 6x slowdown in Chrome for above benchmark\n return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {\n let lastKey;\n\n const processNextBatch = () => {\n emojiStore.getAll(lastKey && IDBKeyRange.lowerBound(lastKey, true), BATCH_SIZE).onsuccess = e => {\n const results = e.target.result;\n for (const result of results) {\n lastKey = result.unicode;\n if (predicate(result)) {\n return cb(result)\n }\n }\n if (results.length < BATCH_SIZE) {\n return cb()\n }\n processNextBatch();\n };\n };\n processNextBatch();\n })\n}\n\nasync function loadData (db, emojiData, url, eTag) {\n try {\n const transformedData = transformEmojiData(emojiData);\n await dbPromise(db, [STORE_EMOJI, STORE_KEYVALUE], MODE_READWRITE, ([emojiStore, metaStore], txn) => {\n let oldETag;\n let oldUrl;\n let todo = 0;\n\n function checkFetched () {\n if (++todo === 2) { // 2 requests made\n onFetched();\n }\n }\n\n function onFetched () {\n if (oldETag === eTag && oldUrl === url) {\n // check again within the transaction to guard against concurrency, e.g. multiple browser tabs\n return\n }\n // delete old data\n emojiStore.clear();\n // insert new data\n for (const data of transformedData) {\n emojiStore.put(data);\n }\n metaStore.put(eTag, KEY_ETAG);\n metaStore.put(url, KEY_URL);\n commit(txn);\n }\n\n getIDB(metaStore, KEY_ETAG, result => {\n oldETag = result;\n checkFetched();\n });\n\n getIDB(metaStore, KEY_URL, result => {\n oldUrl = result;\n checkFetched();\n });\n });\n } finally {\n }\n}\n\nasync function getEmojiByGroup (db, group) {\n return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {\n const range = IDBKeyRange.bound([group, 0], [group + 1, 0], false, true);\n getAllIDB(emojiStore.index(INDEX_GROUP_AND_ORDER), range, cb);\n })\n}\n\nasync function getEmojiBySearchQuery (db, query) {\n const tokens = normalizeTokens(extractTokens(query));\n\n if (!tokens.length) {\n return []\n }\n\n return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {\n // get all results that contain all tokens (i.e. an AND query)\n const intermediateResults = [];\n\n const checkDone = () => {\n if (intermediateResults.length === tokens.length) {\n onDone();\n }\n };\n\n const onDone = () => {\n const results = findCommonMembers(intermediateResults, _ => _.unicode);\n cb(results.sort((a, b) => a.order < b.order ? -1 : 1));\n };\n\n for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n const range = i === tokens.length - 1\n ? IDBKeyRange.bound(token, token + '\\uffff', false, true) // treat last token as a prefix search\n : IDBKeyRange.only(token); // treat all other tokens as an exact match\n getAllIDB(emojiStore.index(INDEX_TOKENS), range, result => {\n intermediateResults.push(result);\n checkDone();\n });\n }\n })\n}\n\n// This could have been implemented as an IDB index on shortcodes, but it seemed wasteful to do that\n// when we can already query by tokens and this will give us what we're looking for 99.9% of the time\nasync function getEmojiByShortcode (db, shortcode) {\n const emojis = await getEmojiBySearchQuery(db, shortcode);\n\n // In very rare cases (e.g. the shortcode \"v\" as in \"v for victory\"), we cannot search because\n // there are no usable tokens (too short in this case). In that case, we have to do an inefficient\n // full-database scan, which I believe is an acceptable tradeoff for not having to have an extra\n // index on shortcodes.\n\n if (!emojis.length) {\n const predicate = _ => ((_.shortcodes || []).includes(shortcode.toLowerCase()));\n return (await doFullDatabaseScanForSingleResult(db, predicate)) || null\n }\n\n return emojis.filter(_ => {\n const lowerShortcodes = (_.shortcodes || []).map(_ => _.toLowerCase());\n return lowerShortcodes.includes(shortcode.toLowerCase())\n })[0] || null\n}\n\nasync function getEmojiByUnicode (db, unicode) {\n return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => (\n getIDB(emojiStore, unicode, result => {\n if (result) {\n return cb(result)\n }\n getIDB(emojiStore.index(INDEX_SKIN_UNICODE), unicode, result => cb(result || null));\n })\n ))\n}\n\nfunction get (db, storeName, key) {\n return dbPromise(db, storeName, MODE_READONLY, (store, txn, cb) => (\n getIDB(store, key, cb)\n ))\n}\n\nfunction set (db, storeName, key, value) {\n return dbPromise(db, storeName, MODE_READWRITE, (store, txn) => {\n store.put(value, key);\n commit(txn);\n })\n}\n\nfunction incrementFavoriteEmojiCount (db, unicode) {\n return dbPromise(db, STORE_FAVORITES, MODE_READWRITE, (store, txn) => (\n getIDB(store, unicode, result => {\n store.put((result || 0) + 1, unicode);\n commit(txn);\n })\n ))\n}\n\nfunction getTopFavoriteEmoji (db, customEmojiIndex, limit) {\n if (limit === 0) {\n return []\n }\n return dbPromise(db, [STORE_FAVORITES, STORE_EMOJI], MODE_READONLY, ([favoritesStore, emojiStore], txn, cb) => {\n const results = [];\n favoritesStore.index(INDEX_COUNT).openCursor(undefined, 'prev').onsuccess = e => {\n const cursor = e.target.result;\n if (!cursor) { // no more results\n return cb(results)\n }\n\n function addResult (result) {\n results.push(result);\n if (results.length === limit) {\n return cb(results) // done, reached the limit\n }\n cursor.continue();\n }\n\n const unicodeOrName = cursor.primaryKey;\n const custom = customEmojiIndex.byName(unicodeOrName);\n if (custom) {\n return addResult(custom)\n }\n // This could be done in parallel (i.e. make the cursor and the get()s parallelized),\n // but my testing suggests it's not actually faster.\n getIDB(emojiStore, unicodeOrName, emoji => {\n if (emoji) {\n return addResult(emoji)\n }\n // emoji not found somehow, ignore (may happen if custom emoji change)\n cursor.continue();\n });\n };\n })\n}\n\n// trie data structure for prefix searches\n// loosely based on https://github.com/nolanlawson/substring-trie\n\nconst CODA_MARKER = ''; // marks the end of the string\n\nfunction trie (arr, itemToTokens) {\n const map = new Map();\n for (const item of arr) {\n const tokens = itemToTokens(item);\n for (const token of tokens) {\n let currentMap = map;\n for (let i = 0; i < token.length; i++) {\n const char = token.charAt(i);\n let nextMap = currentMap.get(char);\n if (!nextMap) {\n nextMap = new Map();\n currentMap.set(char, nextMap);\n }\n currentMap = nextMap;\n }\n let valuesAtCoda = currentMap.get(CODA_MARKER);\n if (!valuesAtCoda) {\n valuesAtCoda = [];\n currentMap.set(CODA_MARKER, valuesAtCoda);\n }\n valuesAtCoda.push(item);\n }\n }\n\n const search = (query, exact) => {\n let currentMap = map;\n for (let i = 0; i < query.length; i++) {\n const char = query.charAt(i);\n const nextMap = currentMap.get(char);\n if (nextMap) {\n currentMap = nextMap;\n } else {\n return []\n }\n }\n\n if (exact) {\n const results = currentMap.get(CODA_MARKER);\n return results || []\n }\n\n const results = [];\n // traverse\n const queue = [currentMap];\n while (queue.length) {\n const currentMap = queue.shift();\n const entriesSortedByKey = [...currentMap.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1);\n for (const [key, value] of entriesSortedByKey) {\n if (key === CODA_MARKER) { // CODA_MARKER always comes first; it's the empty string\n results.push(...value);\n } else {\n queue.push(value);\n }\n }\n }\n return results\n };\n\n return search\n}\n\nconst requiredKeys$1 = [\n 'name',\n 'url'\n];\n\nfunction assertCustomEmojis (customEmojis) {\n const isArray = customEmojis && Array.isArray(customEmojis);\n const firstItemIsFaulty = isArray &&\n customEmojis.length &&\n (!customEmojis[0] || requiredKeys$1.some(key => !(key in customEmojis[0])));\n if (!isArray || firstItemIsFaulty) {\n throw new Error('Custom emojis are in the wrong format')\n }\n}\n\nfunction customEmojiIndex (customEmojis) {\n assertCustomEmojis(customEmojis);\n\n const sortByName = (a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;\n\n //\n // all()\n //\n const all = customEmojis.sort(sortByName);\n\n //\n // search()\n //\n const emojiToTokens = emoji => {\n const set = new Set();\n if (emoji.shortcodes) {\n for (const shortcode of emoji.shortcodes) {\n for (const token of extractTokens(shortcode)) {\n set.add(token);\n }\n }\n }\n return set\n };\n const searchTrie = trie(customEmojis, emojiToTokens);\n const searchByExactMatch = _ => searchTrie(_, true);\n const searchByPrefix = _ => searchTrie(_, false);\n\n // Search by query for custom emoji. Similar to how we do this in IDB, the last token\n // is treated as a prefix search, but every other one is treated as an exact match.\n // Then we AND the results together\n const search = query => {\n const tokens = extractTokens(query);\n const intermediateResults = tokens.map((token, i) => (\n (i < tokens.length - 1 ? searchByExactMatch : searchByPrefix)(token)\n ));\n return findCommonMembers(intermediateResults, _ => _.name).sort(sortByName)\n };\n\n //\n // byShortcode, byName\n //\n const shortcodeToEmoji = new Map();\n const nameToEmoji = new Map();\n for (const customEmoji of customEmojis) {\n nameToEmoji.set(customEmoji.name.toLowerCase(), customEmoji);\n for (const shortcode of (customEmoji.shortcodes || [])) {\n shortcodeToEmoji.set(shortcode.toLowerCase(), customEmoji);\n }\n }\n\n const byShortcode = shortcode => shortcodeToEmoji.get(shortcode.toLowerCase());\n const byName = name => nameToEmoji.get(name.toLowerCase());\n\n return {\n all,\n search,\n byShortcode,\n byName\n }\n}\n\nconst isFirefoxContentScript = typeof wrappedJSObject !== 'undefined';\n\n// remove some internal implementation details, i.e. the \"tokens\" array on the emoji object\n// essentially, convert the emoji from the version stored in IDB to the version used in-memory\nfunction cleanEmoji (emoji) {\n if (!emoji) {\n return emoji\n }\n // if inside a Firefox content script, need to clone the emoji object to prevent Firefox from complaining about\n // cross-origin object. See: https://github.com/nolanlawson/emoji-picker-element/issues/356\n /* istanbul ignore if */\n if (isFirefoxContentScript) {\n emoji = structuredClone(emoji);\n }\n delete emoji.tokens;\n if (emoji.skinTones) {\n const len = emoji.skinTones.length;\n emoji.skins = Array(len);\n for (let i = 0; i < len; i++) {\n emoji.skins[i] = {\n tone: emoji.skinTones[i],\n unicode: emoji.skinUnicodes[i],\n version: emoji.skinVersions[i]\n };\n }\n delete emoji.skinTones;\n delete emoji.skinUnicodes;\n delete emoji.skinVersions;\n }\n return emoji\n}\n\nfunction warnETag (eTag) {\n if (!eTag) {\n console.warn('emoji-picker-element is more efficient if the dataSource server exposes an ETag header.');\n }\n}\n\nconst requiredKeys = [\n 'annotation',\n 'emoji',\n 'group',\n 'order',\n 'version'\n];\n\nfunction assertEmojiData (emojiData) {\n if (!emojiData ||\n !Array.isArray(emojiData) ||\n !emojiData[0] ||\n (typeof emojiData[0] !== 'object') ||\n requiredKeys.some(key => (!(key in emojiData[0])))) {\n throw new Error('Emoji data is in the wrong format')\n }\n}\n\nfunction assertStatus (response, dataSource) {\n if (Math.floor(response.status / 100) !== 2) {\n throw new Error('Failed to fetch: ' + dataSource + ': ' + response.status)\n }\n}\n\nasync function getETag (dataSource) {\n const response = await fetch(dataSource, { method: 'HEAD' });\n assertStatus(response, dataSource);\n const eTag = response.headers.get('etag');\n warnETag(eTag);\n return eTag\n}\n\nasync function getETagAndData (dataSource) {\n const response = await fetch(dataSource);\n assertStatus(response, dataSource);\n const eTag = response.headers.get('etag');\n warnETag(eTag);\n const emojiData = await response.json();\n assertEmojiData(emojiData);\n return [eTag, emojiData]\n}\n\n// TODO: including these in blob-util.ts causes typedoc to generate docs for them,\n// even with --excludePrivate ¯\\_(ツ)_/¯\n/** @private */\n/**\n * Convert an `ArrayBuffer` to a binary string.\n *\n * Example:\n *\n * ```js\n * var myString = blobUtil.arrayBufferToBinaryString(arrayBuff)\n * ```\n *\n * @param buffer - array buffer\n * @returns binary string\n */\nfunction arrayBufferToBinaryString(buffer) {\n var binary = '';\n var bytes = new Uint8Array(buffer);\n var length = bytes.byteLength;\n var i = -1;\n while (++i < length) {\n binary += String.fromCharCode(bytes[i]);\n }\n return binary;\n}\n/**\n * Convert a binary string to an `ArrayBuffer`.\n *\n * ```js\n * var myBuffer = blobUtil.binaryStringToArrayBuffer(binaryString)\n * ```\n *\n * @param binary - binary string\n * @returns array buffer\n */\nfunction binaryStringToArrayBuffer(binary) {\n var length = binary.length;\n var buf = new ArrayBuffer(length);\n var arr = new Uint8Array(buf);\n var i = -1;\n while (++i < length) {\n arr[i] = binary.charCodeAt(i);\n }\n return buf;\n}\n\n// generate a checksum based on the stringified JSON\nasync function jsonChecksum (object) {\n const inString = JSON.stringify(object);\n let inBuffer = binaryStringToArrayBuffer(inString);\n\n // this does not need to be cryptographically secure, SHA-1 is fine\n const outBuffer = await crypto.subtle.digest('SHA-1', inBuffer);\n const outBinString = arrayBufferToBinaryString(outBuffer);\n const res = btoa(outBinString);\n return res\n}\n\nasync function doCheckForUpdates (db, dataSource) {\n // just do a simple HEAD request first to see if the eTags match\n let emojiData;\n let eTag = await getETag(dataSource);\n if (!eTag) { // work around lack of ETag/Access-Control-Expose-Headers\n const eTagAndData = await getETagAndData(dataSource);\n eTag = eTagAndData[0];\n emojiData = eTagAndData[1];\n if (!eTag) {\n eTag = await jsonChecksum(emojiData);\n }\n }\n if (await hasData(db, dataSource, eTag)) ; else {\n if (!emojiData) {\n const eTagAndData = await getETagAndData(dataSource);\n emojiData = eTagAndData[1];\n }\n await loadData(db, emojiData, dataSource, eTag);\n }\n}\n\nasync function loadDataForFirstTime (db, dataSource) {\n let [eTag, emojiData] = await getETagAndData(dataSource);\n if (!eTag) {\n // Handle lack of support for ETag or Access-Control-Expose-Headers\n // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers#Browser_compatibility\n eTag = await jsonChecksum(emojiData);\n }\n\n await loadData(db, emojiData, dataSource, eTag);\n}\n\nasync function checkForUpdates (db, dataSource) {\n try {\n await doCheckForUpdates(db, dataSource);\n } catch (err) {\n // Checking for updates is not a critical operation, and it can fail if e.g. the picker is quickly removed and\n // re-added to the DOM. In those cases, we may get an IndexedDB InvalidStateError because we are attempting to close\n // the database connection, possibly while another request is inflight. So there's effectively no way to prevent\n // InvalidStateErrors unless we were to carefully sequence our IndexedDB operations. Much more simply, we can just\n // ignore IndexedDB InvalidStateErrors here and give users one less useless error message in their console.\n if (err.name !== 'InvalidStateError') {\n throw err\n }\n }\n}\n\nclass Database {\n constructor ({ dataSource = DEFAULT_DATA_SOURCE, locale = DEFAULT_LOCALE, customEmoji = [] } = {}) {\n this.dataSource = dataSource;\n this.locale = locale;\n this._dbName = `emoji-picker-element-${this.locale}`;\n this._db = undefined;\n this._lazyUpdate = undefined;\n this._custom = customEmojiIndex(customEmoji);\n\n this._clear = this._clear.bind(this);\n this._ready = this._init();\n }\n\n async _init () {\n const db = this._db = await openDatabase(this._dbName);\n\n addOnCloseListener(this._dbName, this._clear);\n const dataSource = this.dataSource;\n const empty = await isEmpty(db);\n\n if (empty) {\n await loadDataForFirstTime(db, dataSource);\n } else { // offline-first - do an update asynchronously\n this._lazyUpdate = checkForUpdates(db, dataSource);\n }\n }\n\n async ready () {\n const checkReady = async () => {\n if (!this._ready) {\n this._ready = this._init();\n }\n return this._ready\n };\n await checkReady();\n // There's a possibility of a race condition where the element gets added, removed, and then added again\n // with a particular timing, which would set the _db to undefined.\n // We *could* do a while loop here, but that seems excessive and could lead to an infinite loop.\n if (!this._db) {\n await checkReady();\n }\n }\n\n async getEmojiByGroup (group) {\n assertNumber(group);\n await this.ready();\n return uniqEmoji(await getEmojiByGroup(this._db, group)).map(cleanEmoji)\n }\n\n async getEmojiBySearchQuery (query) {\n assertNonEmptyString(query);\n await this.ready();\n const customs = this._custom.search(query);\n const natives = uniqEmoji(await getEmojiBySearchQuery(this._db, query)).map(cleanEmoji);\n return [\n ...customs,\n ...natives\n ]\n }\n\n async getEmojiByShortcode (shortcode) {\n assertNonEmptyString(shortcode);\n await this.ready();\n const custom = this._custom.byShortcode(shortcode);\n if (custom) {\n return custom\n }\n return cleanEmoji(await getEmojiByShortcode(this._db, shortcode))\n }\n\n async getEmojiByUnicodeOrName (unicodeOrName) {\n assertNonEmptyString(unicodeOrName);\n await this.ready();\n const custom = this._custom.byName(unicodeOrName);\n if (custom) {\n return custom\n }\n return cleanEmoji(await getEmojiByUnicode(this._db, unicodeOrName))\n }\n\n async getPreferredSkinTone () {\n await this.ready();\n return (await get(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE)) || 0\n }\n\n async setPreferredSkinTone (skinTone) {\n assertNumber(skinTone);\n await this.ready();\n return set(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE, skinTone)\n }\n\n async incrementFavoriteEmojiCount (unicodeOrName) {\n assertNonEmptyString(unicodeOrName);\n await this.ready();\n return incrementFavoriteEmojiCount(this._db, unicodeOrName)\n }\n\n async getTopFavoriteEmoji (limit) {\n assertNumber(limit);\n await this.ready();\n return (await getTopFavoriteEmoji(this._db, this._custom, limit)).map(cleanEmoji)\n }\n\n set customEmoji (customEmojis) {\n this._custom = customEmojiIndex(customEmojis);\n }\n\n get customEmoji () {\n return this._custom.all\n }\n\n async _shutdown () {\n await this.ready(); // reopen if we've already been closed/deleted\n try {\n await this._lazyUpdate; // allow any lazy updates to process before closing/deleting\n } catch (err) { /* ignore network errors (offline-first) */ }\n }\n\n // clear references to IDB, e.g. during a close event\n _clear () {\n // We don't need to call removeEventListener or remove the manual \"close\" listeners.\n // The memory leak tests prove this is unnecessary. It's because:\n // 1) IDBDatabases that can no longer fire \"close\" automatically have listeners GCed\n // 2) we clear the manual close listeners in databaseLifecycle.js.\n this._db = this._ready = this._lazyUpdate = undefined;\n }\n\n async close () {\n await this._shutdown();\n await closeDatabase(this._dbName);\n }\n\n async delete () {\n await this._shutdown();\n await deleteDatabase(this._dbName);\n }\n}\n\nexport { Database as default };\n", "import Database from './database.js';\n\n// via https://unpkg.com/browse/emojibase-data@6.0.0/meta/groups.json\nconst allGroups = [\n [-1, '✨', 'custom'],\n [0, '😀', 'smileys-emotion'],\n [1, '👋', 'people-body'],\n [3, '🐱', 'animals-nature'],\n [4, '🍎', 'food-drink'],\n [5, '🏠️', 'travel-places'],\n [6, '⚽', 'activities'],\n [7, '📝', 'objects'],\n [8, '⛔️', 'symbols'],\n [9, '🏁', 'flags']\n].map(([id, emoji, name]) => ({ id, emoji, name }));\n\nconst groups = allGroups.slice(1);\n\nconst MIN_SEARCH_TEXT_LENGTH = 2;\nconst NUM_SKIN_TONES = 6;\n\n/* istanbul ignore next */\nconst rIC = typeof requestIdleCallback === 'function' ? requestIdleCallback : setTimeout;\n\n// check for ZWJ (zero width joiner) character\nfunction hasZwj (emoji) {\n return emoji.unicode.includes('\\u200d')\n}\n\n// Find one good representative emoji from each version to test by checking its color.\n// Ideally it should have color in the center. For some inspiration, see:\n// https://about.gitlab.com/blog/2018/05/30/journey-in-native-unicode-emoji/\n//\n// Note that for certain versions (12.1, 13.1), there is no point in testing them explicitly, because\n// all the emoji from this version are compound-emoji from previous versions. So they would pass a color\n// test, even in browsers that display them as double emoji. (E.g. \"face in clouds\" might render as\n// \"face without mouth\" plus \"fog\".) These emoji can only be filtered using the width test,\n// which happens in checkZwjSupport.js.\nconst versionsAndTestEmoji = {\n '': 16, // face with bags under eyes\n '🫨': 15.1, // shaking head, technically from v15 but see note above\n '🫠': 14,\n '🥲': 13.1, // smiling face with tear, technically from v13 but see note above\n '🥻': 12.1, // sari, technically from v12 but see note above\n '🥰': 11,\n '🤩': 5,\n '👱♀️': 4,\n '🤣': 3,\n '👁️🗨️': 2,\n '😀': 1,\n '😐️': 0.7,\n '😃': 0.6\n};\n\nconst TIMEOUT_BEFORE_LOADING_MESSAGE = 1000; // 1 second\nconst DEFAULT_SKIN_TONE_EMOJI = '🖐️';\nconst DEFAULT_NUM_COLUMNS = 8;\n\n// Based on https://fivethirtyeight.com/features/the-100-most-used-emojis/ and\n// https://blog.emojipedia.org/facebook-reveals-most-and-least-used-emojis/ with\n// a bit of my own curation. (E.g. avoid the \"OK\" gesture because of connotations:\n// https://emojipedia.org/ok-hand/)\nconst MOST_COMMONLY_USED_EMOJI = [\n '😊',\n '😒',\n '❤️',\n '👍️',\n '😍',\n '😂',\n '😭',\n '☺️',\n '😔',\n '😩',\n '😏',\n '💕',\n '🙌',\n '😘'\n];\n\n// It's important to list Twemoji Mozilla before everything else, because Mozilla bundles their\n// own font on some platforms (notably Windows and Linux as of this writing). Typically, Mozilla\n// updates faster than the underlying OS, and we don't want to render older emoji in one font and\n// newer emoji in another font:\n// https://github.com/nolanlawson/emoji-picker-element/pull/268#issuecomment-1073347283\nconst FONT_FAMILY = '\"Twemoji Mozilla\",\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",' +\n '\"Noto Color Emoji\",\"EmojiOne Color\",\"Android Emoji\",sans-serif';\n\n/* istanbul ignore next */\nconst DEFAULT_CATEGORY_SORTING = (a, b) => a < b ? -1 : a > b ? 1 : 0;\n\n// Test if an emoji is supported by rendering it to canvas and checking that the color is not black\n// See https://about.gitlab.com/blog/2018/05/30/journey-in-native-unicode-emoji/\n// and https://www.npmjs.com/package/if-emoji for inspiration\n// This implementation is largely borrowed from if-emoji, adding the font-family\n\n\nconst getTextFeature = (text, color) => {\n const canvas = document.createElement('canvas');\n canvas.width = canvas.height = 1;\n\n const ctx = canvas.getContext('2d', {\n // Improves the performance of `getImageData()`\n // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getContextAttributes#willreadfrequently\n willReadFrequently: true\n });\n ctx.textBaseline = 'top';\n ctx.font = `100px ${FONT_FAMILY}`;\n ctx.fillStyle = color;\n ctx.scale(0.01, 0.01);\n ctx.fillText(text, 0, 0);\n\n return ctx.getImageData(0, 0, 1, 1).data\n};\n\nconst compareFeatures = (feature1, feature2) => {\n const feature1Str = [...feature1].join(',');\n const feature2Str = [...feature2].join(',');\n // This is RGBA, so for 0,0,0, we are checking that the first RGB is not all zeroes.\n // Most of the time when unsupported this is 0,0,0,0, but on Chrome on Mac it is\n // 0,0,0,61 - there is a transparency here.\n return feature1Str === feature2Str && !feature1Str.startsWith('0,0,0,')\n};\n\nfunction testColorEmojiSupported (text) {\n // Render white and black and then compare them to each other and ensure they're the same\n // color, and neither one is black. This shows that the emoji was rendered in color.\n const feature1 = getTextFeature(text, '#000');\n const feature2 = getTextFeature(text, '#fff');\n return feature1 && feature2 && compareFeatures(feature1, feature2)\n}\n\n// rather than check every emoji ever, which would be expensive, just check some representatives from the\n// different emoji releases to determine what the font supports\n\nfunction determineEmojiSupportLevel () {\n const entries = Object.entries(versionsAndTestEmoji);\n try {\n // start with latest emoji and work backwards\n for (const [emoji, version] of entries) {\n if (testColorEmojiSupported(emoji)) {\n return version\n }\n }\n } catch (e) { // canvas error\n } finally {\n }\n // In case of an error, be generous and just assume all emoji are supported (e.g. for canvas errors\n // due to anti-fingerprinting add-ons). Better to show some gray boxes than nothing at all.\n return entries[0][1] // first one in the list is the most recent version\n}\n\n// Check which emojis we know for sure aren't supported, based on Unicode version level\nlet promise;\nconst detectEmojiSupportLevel = () => {\n if (!promise) {\n // Delay so it can run while the IDB database is being created by the browser (on another thread).\n // This helps especially with first load – we want to start pre-populating the database on the main thread,\n // and then wait for IDB to commit everything, and while waiting we run this check.\n promise = new Promise(resolve => (\n rIC(() => (\n resolve(determineEmojiSupportLevel()) // delay so ideally this can run while IDB is first populating\n ))\n ));\n }\n return promise\n};\n// determine which emojis containing ZWJ (zero width joiner) characters\n// are supported (rendered as one glyph) rather than unsupported (rendered as two or more glyphs)\nconst supportedZwjEmojis = new Map();\n\nconst VARIATION_SELECTOR = '\\ufe0f';\nconst SKINTONE_MODIFIER = '\\ud83c';\nconst ZWJ = '\\u200d';\nconst LIGHT_SKIN_TONE = 0x1F3FB;\nconst LIGHT_SKIN_TONE_MODIFIER = 0xdffb;\n\n// TODO: this is a naive implementation, we can improve it later\n// It's only used for the skintone picker, so as long as people don't customize with\n// really exotic emoji then it should work fine\nfunction applySkinTone (str, skinTone) {\n if (skinTone === 0) {\n return str\n }\n const zwjIndex = str.indexOf(ZWJ);\n if (zwjIndex !== -1) {\n return str.substring(0, zwjIndex) +\n String.fromCodePoint(LIGHT_SKIN_TONE + skinTone - 1) +\n str.substring(zwjIndex)\n }\n if (str.endsWith(VARIATION_SELECTOR)) {\n str = str.substring(0, str.length - 1);\n }\n return str + SKINTONE_MODIFIER + String.fromCodePoint(LIGHT_SKIN_TONE_MODIFIER + skinTone - 1)\n}\n\nfunction halt (event) {\n event.preventDefault();\n event.stopPropagation();\n}\n\n// Implementation left/right or up/down navigation, circling back when you\n// reach the start/end of the list\nfunction incrementOrDecrement (decrement, val, arr) {\n val += (decrement ? -1 : 1);\n if (val < 0) {\n val = arr.length - 1;\n } else if (val >= arr.length) {\n val = 0;\n }\n return val\n}\n\n// like lodash's uniqBy but much smaller\nfunction uniqBy (arr, func) {\n const set = new Set();\n const res = [];\n for (const item of arr) {\n const key = func(item);\n if (!set.has(key)) {\n set.add(key);\n res.push(item);\n }\n }\n return res\n}\n\n// We don't need all the data on every emoji, and there are specific things we need\n// for the UI, so build a \"view model\" from the emoji object we got from the database\n\nfunction summarizeEmojisForUI (emojis, emojiSupportLevel) {\n const toSimpleSkinsMap = skins => {\n const res = {};\n for (const skin of skins) {\n // ignore arrays like [1, 2] with multiple skin tones\n // also ignore variants that are in an unsupported emoji version\n // (these do exist - variants from a different version than their base emoji)\n if (typeof skin.tone === 'number' && skin.version <= emojiSupportLevel) {\n res[skin.tone] = skin.unicode;\n }\n }\n return res\n };\n\n return emojis.map(({ unicode, skins, shortcodes, url, name, category, annotation }) => ({\n unicode,\n name,\n shortcodes,\n url,\n category,\n annotation,\n id: unicode || name,\n skins: skins && toSimpleSkinsMap(skins)\n }))\n}\n\n// import rAF from one place so that the bundle size is a bit smaller\nconst rAF = requestAnimationFrame;\n\n// \"Svelte action\"-like utility to detect layout changes via ResizeObserver.\n// If ResizeObserver is unsupported, we just use rAF once and don't bother to update.\n\n\nlet resizeObserverSupported = typeof ResizeObserver === 'function';\n\nfunction resizeObserverAction (node, abortSignal, onUpdate) {\n let resizeObserver;\n if (resizeObserverSupported) {\n resizeObserver = new ResizeObserver(onUpdate);\n resizeObserver.observe(node);\n } else { // just run once, don't bother trying to track it\n rAF(onUpdate);\n }\n\n // cleanup function (called on destroy)\n abortSignal.addEventListener('abort', () => {\n if (resizeObserver) {\n resizeObserver.disconnect();\n }\n });\n}\n\n// get the width of the text inside of a DOM node, via https://stackoverflow.com/a/59525891/680742\nfunction calculateTextWidth (node) {\n // skip running this in jest/vitest because we don't need to check for emoji support in that environment\n /* istanbul ignore else */\n {\n const range = document.createRange();\n range.selectNode(node.firstChild);\n return range.getBoundingClientRect().width\n }\n}\n\nlet baselineEmojiWidth;\n\n/**\n * Check if the given emojis containing ZWJ characters are supported by the current browser (don't render\n * as double characters) and return true if all are supported.\n * @param zwjEmojisToCheck\n * @param baselineEmoji\n * @param emojiToDomNode\n */\nfunction checkZwjSupport (zwjEmojisToCheck, baselineEmoji, emojiToDomNode) {\n let allSupported = true;\n for (const emoji of zwjEmojisToCheck) {\n const domNode = emojiToDomNode(emoji);\n // sanity check to make sure the node is defined properly\n /* istanbul ignore if */\n if (!domNode) {\n // This is a race condition that can occur when the component is unmounted/remounted\n // It doesn't really matter what we do here since the old context is not going to render anymore.\n // Just bail out of emoji support detection and return `allSupported=true` since the rendering context is gone\n continue\n }\n const emojiWidth = calculateTextWidth(domNode);\n if (typeof baselineEmojiWidth === 'undefined') { // calculate the baseline emoji width only once\n baselineEmojiWidth = calculateTextWidth(baselineEmoji);\n }\n // On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard\n // against are the ones that are 2x the size, because those are truly broken (person with red hair = person with\n // floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.)\n // So here we set the threshold at 1.8 times the size of the baseline emoji.\n const supported = emojiWidth / 1.8 < baselineEmojiWidth;\n supportedZwjEmojis.set(emoji.unicode, supported);\n\n if (!supported) {\n allSupported = false;\n }\n }\n return allSupported\n}\n\n// like lodash's uniq\n\nfunction uniq (arr) {\n return uniqBy(arr, _ => _)\n}\n\n// Note we put this in its own function outside Picker.js to avoid Svelte doing an invalidation on the \"setter\" here.\n// At best the invalidation is useless, at worst it can cause infinite loops:\n// https://github.com/nolanlawson/emoji-picker-element/pull/180\n// https://github.com/sveltejs/svelte/issues/6521\n// Also note tabpanelElement can be null if the element is disconnected immediately after connected\nfunction resetScrollTopIfPossible (element) {\n /* istanbul ignore else */\n if (element) { // Makes me nervous not to have this `if` guard\n element.scrollTop = 0;\n }\n}\n\nfunction getFromMap (cache, key, func) {\n let cached = cache.get(key);\n if (!cached) {\n cached = func();\n cache.set(key, cached);\n }\n return cached\n}\n\nfunction toString (value) {\n return '' + value\n}\n\nfunction parseTemplate (htmlString) {\n const template = document.createElement('template');\n template.innerHTML = htmlString;\n return template\n}\n\nconst parseCache = new WeakMap();\nconst domInstancesCache = new WeakMap();\n// This needs to be a symbol because it needs to be different from any possible output of a key function\nconst unkeyedSymbol = Symbol('un-keyed');\n\n// Not supported in Safari <=13\nconst hasReplaceChildren = 'replaceChildren' in Element.prototype;\nfunction replaceChildren (parentNode, newChildren) {\n /* istanbul ignore else */\n if (hasReplaceChildren) {\n parentNode.replaceChildren(...newChildren);\n } else { // minimal polyfill for Element.prototype.replaceChildren\n parentNode.innerHTML = '';\n parentNode.append(...newChildren);\n }\n}\n\nfunction doChildrenNeedRerender (parentNode, newChildren) {\n let oldChild = parentNode.firstChild;\n let oldChildrenCount = 0;\n // iterate using firstChild/nextSibling because browsers use a linked list under the hood\n while (oldChild) {\n const newChild = newChildren[oldChildrenCount];\n // check if the old child and new child are the same\n if (newChild !== oldChild) {\n return true\n }\n oldChild = oldChild.nextSibling;\n oldChildrenCount++;\n }\n // if new children length is different from old, we must re-render\n return oldChildrenCount !== newChildren.length\n}\n\nfunction patchChildren (newChildren, instanceBinding) {\n const { targetNode } = instanceBinding;\n let { targetParentNode } = instanceBinding;\n\n let needsRerender = false;\n\n if (targetParentNode) { // already rendered once\n needsRerender = doChildrenNeedRerender(targetParentNode, newChildren);\n } else { // first render of list\n needsRerender = true;\n instanceBinding.targetNode = undefined; // placeholder node not needed anymore, free memory\n instanceBinding.targetParentNode = targetParentNode = targetNode.parentNode;\n }\n // avoid re-rendering list if the dom nodes are exactly the same before and after\n if (needsRerender) {\n replaceChildren(targetParentNode, newChildren);\n }\n}\n\nfunction patch (expressions, instanceBindings) {\n for (const instanceBinding of instanceBindings) {\n const {\n targetNode,\n currentExpression,\n binding: {\n expressionIndex,\n attributeName,\n attributeValuePre,\n attributeValuePost\n }\n } = instanceBinding;\n\n const expression = expressions[expressionIndex];\n\n if (currentExpression === expression) {\n // no need to update, same as before\n continue\n }\n\n instanceBinding.currentExpression = expression;\n\n if (attributeName) { // attribute replacement\n if (expression === null) {\n // null is treated as a special case by the framework - we don't render an attribute at all in this case\n targetNode.removeAttribute(attributeName);\n } else {\n // attribute value is not null; set a new attribute\n const newValue = attributeValuePre + toString(expression) + attributeValuePost;\n targetNode.setAttribute(attributeName, newValue);\n }\n } else { // text node / child element / children replacement\n let newNode;\n if (Array.isArray(expression)) { // array of DOM elements produced by tag template literals\n patchChildren(expression, instanceBinding);\n } else if (expression instanceof Element) { // html tag template returning a DOM element\n newNode = expression;\n targetNode.replaceWith(newNode);\n } else { // primitive - string, number, etc\n // nodeValue is faster than textContent supposedly https://www.youtube.com/watch?v=LY6y3HbDVmg\n // note we may be replacing the value in a placeholder text node\n targetNode.nodeValue = toString(expression);\n }\n if (newNode) {\n instanceBinding.targetNode = newNode;\n }\n }\n }\n}\n\nfunction parse (tokens) {\n let htmlString = '';\n\n let withinTag = false;\n let withinAttribute = false;\n let elementIndexCounter = -1; // depth-first traversal order\n\n const elementsToBindings = new Map();\n const elementIndexes = [];\n\n let skipTokenChars = 0;\n for (let i = 0, len = tokens.length; i < len; i++) {\n const token = tokens[i];\n htmlString += token.slice(skipTokenChars);\n\n if (i === len - 1) {\n break // no need to process characters - no more expressions to be found\n }\n\n for (let j = 0; j < token.length; j++) {\n const char = token.charAt(j);\n switch (char) {\n case '<': {\n const nextChar = token.charAt(j + 1);\n if (nextChar === '/') { // closing tag\n // leaving an element\n elementIndexes.pop();\n } else { // not a closing tag\n withinTag = true;\n elementIndexes.push(++elementIndexCounter);\n }\n break\n }\n case '>': {\n withinTag = false;\n withinAttribute = false;\n break\n }\n case '=': {\n withinAttribute = true;\n break\n }\n }\n }\n\n const elementIndex = elementIndexes[elementIndexes.length - 1];\n const bindings = getFromMap(elementsToBindings, elementIndex, () => []);\n\n let attributeName;\n let attributeValuePre;\n let attributeValuePost;\n if (withinAttribute) {\n // I never use single-quotes for attribute values in HTML, so just support double-quotes or no-quotes\n const attributePreMatch = /(\\S+)=\"?([^\"=]*)$/.exec(token);\n attributeName = attributePreMatch[1];\n attributeValuePre = attributePreMatch[2];\n const attributePostMatch = /^([^\">]*)(\"?)/.exec(tokens[i + 1]);\n attributeValuePost = attributePostMatch[1];\n // Optimization: remove the attribute itself, so we don't create a default attribute which is either empty or just\n // the \"pre\" text, e.g. `
` or `
`. It will be replaced by the expression anyway.\n htmlString = htmlString.slice(0, -1 * attributePreMatch[0].length);\n skipTokenChars = attributePostMatch[0].length;\n } else {\n skipTokenChars = 0;\n }\n\n const binding = {\n attributeName,\n attributeValuePre,\n attributeValuePost,\n expressionIndex: i\n };\n\n bindings.push(binding);\n\n if (!withinTag && !withinAttribute) {\n // Add a placeholder text node, so we can find it later. Note we only support one dynamic child text node\n htmlString += ' ';\n }\n }\n\n const template = parseTemplate(htmlString);\n\n return {\n template,\n elementsToBindings\n }\n}\n\nfunction applyBindings (bindings, element, instanceBindings) {\n for (let i = 0; i < bindings.length; i++) {\n const binding = bindings[i];\n\n const targetNode = binding.attributeName\n ? element // attribute binding, just use the element itself\n : element.firstChild; // not an attribute binding, so has a placeholder text node\n\n const instanceBinding = {\n binding,\n targetNode,\n targetParentNode: undefined,\n currentExpression: undefined\n };\n\n instanceBindings.push(instanceBinding);\n }\n}\n\nfunction traverseAndSetupBindings (rootElement, elementsToBindings) {\n const instanceBindings = [];\n\n let topLevelBindings;\n if (elementsToBindings.size === 1 && (topLevelBindings = elementsToBindings.get(0))) {\n // Optimization for the common case where there's only one element and one binding\n // Skip creating a TreeWalker entirely and just handle the root DOM element\n applyBindings(topLevelBindings, rootElement, instanceBindings);\n } else {\n // traverse dom\n const treeWalker = document.createTreeWalker(rootElement, NodeFilter.SHOW_ELEMENT);\n\n let element = rootElement;\n let elementIndex = -1;\n do {\n const bindings = elementsToBindings.get(++elementIndex);\n if (bindings) {\n applyBindings(bindings, element, instanceBindings);\n }\n } while ((element = treeWalker.nextNode()))\n }\n\n return instanceBindings\n}\n\nfunction parseHtml (tokens) {\n // All templates and bound expressions are unique per tokens array\n const { template, elementsToBindings } = getFromMap(parseCache, tokens, () => parse(tokens));\n\n // When we parseHtml, we always return a fresh DOM instance ready to be updated\n const dom = template.cloneNode(true).content.firstElementChild;\n const instanceBindings = traverseAndSetupBindings(dom, elementsToBindings);\n\n return function updateDomInstance (expressions) {\n patch(expressions, instanceBindings);\n return dom\n }\n}\n\nfunction createFramework (state) {\n const domInstances = getFromMap(domInstancesCache, state, () => new Map());\n let domInstanceCacheKey = unkeyedSymbol;\n\n function html (tokens, ...expressions) {\n // Each unique lexical usage of map() is considered unique due to the html`` tagged template call it makes,\n // which has lexically unique tokens. The unkeyed symbol is just used for html`` usage outside of a map().\n const domInstancesForTokens = getFromMap(domInstances, tokens, () => new Map());\n const updateDomInstance = getFromMap(domInstancesForTokens, domInstanceCacheKey, () => parseHtml(tokens));\n\n return updateDomInstance(expressions) // update with expressions\n }\n\n function map (array, callback, keyFunction) {\n return array.map((item, index) => {\n const originalCacheKey = domInstanceCacheKey;\n domInstanceCacheKey = keyFunction(item);\n try {\n return callback(item, index)\n } finally {\n domInstanceCacheKey = originalCacheKey;\n }\n })\n }\n\n return { map, html }\n}\n\nfunction render (container, state, helpers, events, actions, refs, abortSignal, actionContext, firstRender) {\n const { labelWithSkin, titleForEmoji, unicodeWithSkin } = helpers;\n const { html, map } = createFramework(state);\n\n function emojiList (emojis, searchMode, prefix) {\n return map(emojis, (emoji, i) => {\n return html``\n // It's important for the cache key to be unique based on the prefix, because the framework caches based on the\n // unique tokens + cache key, and the same emoji may be used in the tab as well as in the fav bar\n }, emoji => `${prefix}-${emoji.id}`)\n }\n\n const section = () => {\n return html`