flyink13 / Emoji: Оптимизация + поиск по наборам

// ==UserScript==
// @name         Emoji: Оптимизация + поиск по наборам
// @namespace    http://vk.com/
// @description  Прокачивает понель смайлов ВКонтакте: При открытии стикеров по умолчанию выбирает смайлы (их открывает быстрее), появился поиск, при вводе в поиск переключает наборы, по табу выходит из поиска, при выходе из поиска переключает обратно в смайлы и сбрасывает поиск, отприсовывает только найденные наборы, вместо всех
// @version      0.4
// @author       Flyink13
// @match        https://vk.com/*
// @icon         https://www.google.com/s2/favicons?domain=vk.com
// @grant        none
// @copyright 2021, flyink13 (https://openuserjs.org/users/flyink13)
// @license MIT
// @updateURL https://openuserjs.org/meta/flyink13/Emoji_Оптимизация_+_поиск_по_наборам.meta.js
// @downloadURL https://openuserjs.org/install/flyink13/Emoji_Оптимизация_+_поиск_по_наборам.user.js
// ==/UserScript==
/* global Emoji, ls, data, domPN, parseLatKeys */

function Main(ttl = 2) {
    // Отключаем у фреймов
    if (window != parent) {
        return;
    }

    // Проверяем, что модуль инициализирован
    if (typeof Emoji !== 'object') {
        // Мне лень писать нормальное условие, и так сойдет
        setTimeout(Main, ttl, Math.min(60000, ttl * 2));
        return;
    }

    const KEYS = {
        LEFT: 37,
        UP: 38,
        RIGHT: 39,
        DOWN: 40,
        TAB: 9,
    };

    // Запоминаем функции, которые будем заменять
    Emoji.getTabsCode_orginal = Emoji.getTabsCode;
    Emoji.ttClick_orginal = Emoji.ttClick;
    Emoji.hide_orginal = Emoji.hide;
    Emoji.getTabCont_original = Emoji.getTabCont;
    Emoji.tabSwitch_original = Emoji.tabSwitch;

    // Заменяем функцию получения вкладок набора стикеров
    Emoji.getTabsCode = (emoji, optId) => {
        // Функцию вызывают 2 раза, первый раз разрешаем, там смыйлы и еще что-то
        if (emoji !== window.emojiStickers) {
            return Emoji.getTabsCode_orginal(emoji, optId);
        }

        // По умолчанию рисуем избранное
        const systemPacks = window.emojiStickers.filter(([id]) => id < 0);
        const systemHtml = Emoji.getTabsCode_orginal(systemPacks, optId);

        // Поле ввода + контейнер с результатами
        return `
                <input
                class="searchPackInput"
                style=""
                placeholder="Поиск"
                data-opt-id="${optId}"
                onkeydown="Emoji.onSearchPacksInputKeyDown(event);"
                oninput="Emoji.onSearchPacksInputChange(event);"
                />
                <div class="searchPackContainer">${systemHtml}</div>
                `;
    }

    // Заменяем функцию открытия панели
    Emoji.ttClick = (optId, obj, needHide, needShow, ev, tabKey) => {
        ls && ls.set('stickers_tab', Emoji.TAB_EMOJI);
        const response = Emoji.ttClick_orginal(optId, obj, needHide, needShow, ev, tabKey);

        if (!obj || !obj.parentElement) {
            return;
        }

        if (needShow) {
            const input = obj.parentElement.parentElement.querySelector('.searchPackInput');
            setTimeout(() => input && input.focus(), Emoji.SHOW_TT_TIMEOUT);
        }

        if (needHide) {
            Emoji.dropSearch(optId);
        }

        return response;
    }

    // Заменяем функцию скрытия панели
    Emoji.hide = (obj, ev, force) => {
        let optId = data(domPN(obj), 'optId');
        Emoji.dropSearch(optId);
        return Emoji.hide_orginal(obj, ev, force);
    }

    Emoji.dropSearch = (optId) => {
        if (!Emoji.opts[optId] || !Emoji.opts[optId].tt) {
            return;
        }

        const tt = Emoji.opts[optId].tt;
        const txt = Emoji.opts[optId].txt;
        const selId = Emoji.TAB_EMOJI;
        const tabEl = tt.querySelector(`.emoji_tab_${selId}`);
        const input = tt.querySelector(`.searchPackInput`);

        // Возвращаем фокус на поле ввода
        Emoji.editableFocus(txt, false, true, void(0), true);

        setTimeout(() => {
            // Сбрасываем поле ввода
            input.value = '';
            Emoji.onSearchPacksInputChange({ target: input });

            // Переключаем вкладку на смайлы
            Emoji.tabSwitch(tabEl, selId, optId);
        }, Emoji.HIDE_TT_TIMEOUT);
    }

    Emoji.getTabCont = (optId, selId) => {
        const backup = window.emojiStickers;
        const activePacks = {};

        activePacks[selId] = true;
        Emoji.opts[optId].searchResponse.forEach(([id]) => {
            activePacks[id] = true;
        });

        window.emojiStickers = backup.filter(([id]) => activePacks[id]).splice(0, 8);
        const res = Emoji.getTabCont_original(optId, selId);
        window.emojiStickers = backup;
        return res;
    }


    Emoji.tabSwitch = (obj, selId, optId, ...args) => {
        const res = Emoji.tabSwitch_original(obj, selId, optId, ...args);
        Emoji.opts[optId].stickersInited = false;
        return res;
    }

    Emoji.onSearchPacksInputKeyDown = (event) => {
        const input = event.target;
        const optId = parseInt(input.dataset.optId, 10);

        if ([KEYS.UP, KEYS.DOWN, KEYS.LEFT, KEYS.RIGHT].indexOf(event.keyCode) === -1) {
            event.stopPropagation();
        }

        if (event.keyCode !== KEYS.TAB || !Emoji.opts[optId]) {
            return;
        }


        Emoji.ttClick(optId, input, true);
    }

    Emoji.onSearchPacksInputChange = (event) => {
        const input = event.target;
        const optId = parseInt(input.dataset.optId, 10);
        const container = input.parentElement.querySelector('.searchPackContainer');

        const searchResponse = window.emojiStickers.filter(([id]) => (
            (
                // При пустом поиске показываем избранное и последние наборы стикеров
                !input.value && id < 0
            ) || (
                // Ищем от двух символов
                input.value.length &&
                Emoji.stickers[id] && Emoji.stickers[id].title &&
                Emoji.stickers[id].title.toLowerCase().includes(input.value.toLowerCase())
            )
        )).splice(0, 8);

        container.innerHTML = Emoji.getTabsCode_orginal(searchResponse, optId);
        Emoji.opts[optId].searchResponse = searchResponse;

        if (input.value && searchResponse.length) {
            const selId = searchResponse[0][0];
            const obj = container.querySelector(`.emoji_tab_${selId}`);
            Emoji.tabSwitch(obj, selId, optId);
        }
    }

    // Немного костыльных стилей 3-)
    const style = `
.searchPackInput {
    transition: all linear .2s;
    padding: 8px 12px;
    border: 0px;
    margin: 1px;
    width: 120px;
    float: left;
    background: #fff;
    position: absolute;
    top: -32px;
    left: -2px;
    z-index: 1;
    border-radius: 2px;
    box-shadow: 1px 2px 5px 0px rgb(0 0 0 / 20%);
}

.searchPackInput:placeholder-shown {
    top: 2px;
    left: 0px;
    padding: 8px 10px;
    box-shadow: 0px 0px 0px 0px rgb(0 0 0 / 0%);
    background: #eff2f5;
}

.searchPackContainer{
    width: 220px;
    overflow: hidden;
}

.emoji_block_cont,
.emoji_tabs_wrap {
    overflow: visible;
}

.emoji_tab_-2 {
    margin-left: 150px;
}
    `;

    document.head.appendChild(document.createElement("style")).innerHTML = style;
}


(function injectScript() {
    const script = document.createElement('script');
    const code = '(' + Main + ')();';
    script.appendChild(document.createTextNode(code));
    (document.body || document.head || document.documentElement).appendChild(script);
})();