Slack Mod: Улучшение редактора


Введение

В моем предыдущем блоге я сделал работающий живой css-редактор для slack, вдохновленный модами для discord. В конце блога я привел следующий список ограничений, которые имеет мод:

  • CSS не сохраняет новые строки
  • CSS не сохраняется между перезагрузками
  • Инъекция осуществляется вручную и жестко закодирована в системе пользователя
  • Выбор пользовательского css в настройках, а затем выбор другой категории приводит к проблеме
  • Нет подсветки синтаксиса

Этот блог будет посвящен устранению следующих ограничений:

  • CSS не сохраняет новые строки
  • CSS не сохраняется между перезагрузками
  • Нет подсветки синтаксиса

Как это делают другие моды?

Прошлой ночью во сне мне приснилась идея, что просмотр того, как моды Discord добавляют пользовательский CSS, может быть полезен.

Вместо того, чтобы тратить целую вечность на решение проблем методом проб и ошибок, чтобы сделать свой собственный редактор, мы можем основываться на работе других людей, которые уже решили эти проблемы.

Изучение кода CustomCSS от GooseMod

Я решил покопаться в коде css-редактора GooseMod в поисках вдохновения и идей.

Уже в первых нескольких строках мы можем начать находить сходство между нашим и их кодом:

main/index.js lines 8-12

const updateCSS = (c) => {
  styleEl.innerHTML = '';

  styleEl.appendChild(document.createTextNode(c));
};
Вход в полноэкранный режим Выход из полноэкранного режима

Это эквивалентно нашему методу updateCurrentCSS:

const updateCustomCSS = newCSS => { 
    document.querySelector("#SlackMod-Custom-CSS").innerText = newCSS; 
}
Войти в полноэкранный режим Выход из полноэкранного режима

Глядя на то, что они делают по-другому, я подумал, не является ли решение проблемы несохранения новых строк в CSS таким же простым, как изменение нашего метода с перенаправления innerText на добавление текстовых узлов вместо этого.
Чтобы проверить эту идею, я изменил свои методы следующим образом:

// method to quickly change css
const updateCustomCSS = newCSS => { 
    document.querySelector("#SlackMod-Custom-CSS").innerHTML = "" 
    document.querySelector("#SlackMod-Custom-CSS").appendChild(document.createTextNode(newCSS); 
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Я также изменил свой метод get, чтобы использовать innerHTML вместо innerText:

// method to quickly get inner css
const getCustomCSS = () => { 
    return document.querySelector("#SlackMod-Custom-CSS").innerHTML
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Шокирующе, но это сработало! Мой текстовый редактор действительно сохранил новые строки! Я бы никогда не подумал попробовать это.

Если посмотреть на оставшийся код, похоже, что они используют библиотеку под названием «ace» для создания красивого текстового редактора.

Сначала они импортируют библиотеки, которые будут использовать:

main/index.js lines 22-27

// Setup ace
eval(await (await fetch(`https://ajaxorg.github.io/ace-builds/src-min-noconflict/ace.js`)).text()); // Load Ace main

eval(await (await fetch(`https://ajaxorg.github.io/ace-builds/src-min-noconflict/theme-monokai.js`)).text()); // Load Monokai theme

eval(await (await fetch(`https://ajaxorg.github.io/ace-builds/src-min-noconflict/mode-css.js`)).text()); // Load CSS lang
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем они вызывают функцию из API goosemod для добавления категории настроек (то, что я не имею возможности сделать):

main/index.js строки 29-34 (отредактировано для ясности)

goosemodScope.settings.createItem('Custom CSS', [
    `(v${version})`, { type: 'custom', element: () => {
        // code that makes and returns the node they 
        // want to be in the settings menu
Вход в полноэкранный режим Выход из полноэкранного режима

Внутри этого, они инициализируют div, который будет содержать их редактор с некоторым css:

main/index.js строки 35-43 (отредактировано для ясности и краткости)

// add div for ace to be in
const el = document.createElement('div');
// give it an id so ace can see it
el.id = 'gm-editor';
// size it to user's window
el.style.width = '90%';
el.style.height = '85vh';
// set its default value
el.innerHTML = css;
Вход в полноэкранный режим Выход из полноэкранного режима

Затем, они делают 2 вещи:

  • запускают новый поток, который ждет 10 мс, перед инициализацией Ace
  • возвращают div

Это предположительно потому, что они не могут инициализировать редактор до тех пор, пока div, в котором он находится
в котором он находится, существует.

main/index.js строки 45-63 (отредактировано для ясности и краткости)

// instantly ran async function (async function() {..})();
(async function() {
    // wait 10 milliseconds (they wrote a method for this earlier)
    await sleep(10);

    // point ace to the id we gave our div
    const editor = ace.edit('gm-editor');
    const session = editor.getSession();
    // configure ace's ????, syntax highlighting, theme
    session.setUseWorker(false); // Tell Ace not to use Workers
    session.setMode('ace/mode/css'); // Set lang to CSS
    editor.setTheme('ace/theme/monokai'); // Set theme to Monokai
    // equivalent to addEventListener("input")
    session.on('change', () => {
        // some magic with localstorage we will get into later
        const val = session.getValue();
        css = val;
        updateCSS(val);
    });
})();

return el;
Вход в полноэкранный режим Выход из полноэкранного режима

Нам не нужна эта асинхронная функция, потому что мы можем просто добавить div в документ перед инициализацией редактора.

Реализация того, что мы узнали

Из того, что я прочитал, кажется, что инициализация Ace будет легкой, как только мы импортируем его.

Импортирование Ace

Было бы неплохо попытаться сделать то же самое, что сделал GooseMod, но, к сожалению, похоже, что Slack заблокировал eval() на недоверенном тексте.

Попытка запустить в консоли:

eval(await (await fetch(`https://ajaxorg.github.io/ace-builds/src-min-noconflict/ace.js`)).text()); // Load Ace main
Войти в полноэкранный режим Выйти из полноэкранного режима

дает нам эту ошибку:

Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script
Войти в полноэкранный режим Выйти из полноэкранного режима

Далее я решил попробовать добавить <script> в заголовок:

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = "https://ajaxorg.github.io/ace-builds/src-min-noconflict/ace.js";
document.head.appendChild(script);
Enter fullscreen mode Выйти из полноэкранного режима

Попытка запустить это дает нам гораздо более ясную, описательную ошибку:

Я думаю, это означает, что скрипты разрешены только с URL, которые соответствуют одному из следующих пунктов:

  • https://*.sdkassets.chime.aws
  • https://*.slack.quip.systems
  • https://quip-cdn.com
  • https://slack-prod.qvpc-cdn.com
  • https://a.slack-edge.com/
  • https://b.slack-edge.com/

Затем я подумал, что могу просто добавить URL для наших библиотек в аргумент scripts=[] нашего инжектирующего кода python:

inject(slack_location, devtools=True, timeout=600, scripts=[
    # libraries
    "https://ajaxorg.github.io/ace-builds/src-min-noconflict/ace.js",
    "https://ajaxorg.github.io/ace-builds/src-min-noconflict/theme-monokai.js",
    "https://ajaxorg.github.io/ace-builds/src-min-noconflict/mode-css.js",

    # our injected code
    inject_location
]) 
Вход в полноэкранный режим Выйти из полноэкранного режима

Но нет, библиотека Electron Inject python ожидает, что в скрипты будут передаваться только реальные файлы.

Улучшение нашего инжектирующего скрипта

Я потратил некоторое время на добавление динамической загрузки библиотеки в наш сценарий запуска.

Вот основная идея в псевдокоде:

for each library file we need:
    check if its already downloaded to a libs folder
    if not
        download it
        move it to the libs folder
    add path to lib to the scripts argument 
Вход в полноэкранный режим Выход из полноэкранного режима

Я не собираюсь разбирать это построчно, но python читает довольно хорошо, и это хорошо прокомментировано.

# scripts we will inject are added to this array
scripts = []
# get the directory this python code is in
# or the Current Working Directory
cwd = getcwd()
# make the libs folder if it doesn't exist
libsFolder = f"{cwd}/libs"
if not exists(libsFolder):
    makedirs(libsFolder)
# array of libraries to install
libURLs = [
    "https://ajaxorg.github.io/ace-builds/src-min-noconflict/ace.js",
    "https://ajaxorg.github.io/ace-builds/src-min-noconflict/theme-dracula.js",
    "https://ajaxorg.github.io/ace-builds/src-min-noconflict/mode-css.js"]
for libURL in libURLs:
    # gets the last substring that follows a /
    fileName = libURL.split("/")[-1]
    print(f"nGetting library file: {fileName}")

    # check if we have already downloaded it
    if exists(f"libs/{fileName}"):
        print(f"tfound {fileName} in libs")
    else:
        print(f"t{fileName} not in libs, downloading")
        # download it
        download(libURL)
        # move it into libs
        rename(f"{cwd}/{fileName}", f"{libsFolder}/{fileName}")
    # add path to file to list of scripts to inject
    # we use realpath() here to get the fill directory to the file
    # because sometimes electron inject gets screwy with relative files
    scripts.append(f"{libsFolder}/{fileName}")
# add our own code as the last entry in scripts
# so all the scripts are loaded once our code runs
scripts.append(f"{cwd}/inject.js")
Вход в полноэкранный режим Выход из полноэкранного режима

Создание нашего редактора с помощью Ace

Хотя мы не можем использовать код GooseMod непосредственно для импорта, его код для настройки ace может быть практически скопирован вместо нашего старого кода textarea.

Учитывая, что мы используем эту часть их кода полностью, я должен включить лицензию.

Мне не понравилось, как много места занимает лицензия MIT в моем коде, поэтому я спросил разработчика, который ее написал, действительно ли мне нужно ее туда помещать:

Основываясь на их ответе, я решил включить этот комментарий:

/*
Based on GooseMod CustomCSS, which is under the MIT License
https://github.com/GooseMod-Modules/CustomCSS/blob/64969856598a2cc2980988046e0ff266d64fa943/index.js#L35-L65
*/
Войти в полноэкранный режим Выйти из полноэкранного режима

Итак, что было изменено, кроме имен переменных/методов?

Во-первых, я изменил width/height css на css, который я написал для старого редактора:

cssEditor.style.width = "100%";
cssEditor.style.height = "calc(100% - 0.5rem)";
// set its default value
cssEditor.innerHTML = getCustomCSS();
Вход в полноэкранный режим Выход из полноэкранного режима

Я использовал старый код, который у меня был для добавления редактора в панель содержимого настроек.

// add editor to settings content pane
document.querySelector(".p-prefs_dialog__panel").replaceChildren(cssEditor)
Войти в полноэкранный режим Выход из полноэкранного режима

Поскольку мы добавляем редактор к домену мгновенно, нам не нужны все эти асинхронные функции ожидания.

Я также изменил установленную здесь тему с Monokai на Dracula, так как она выглядит лучше.

// point ace to the id we gave our div
const editor = ace.edit('slackMod-editor');
const session = editor.getSession();
// configure syntax highlighting, theme
session.setMode('ace/mode/css'); // Set lang to CSS
editor.setTheme('ace/theme/dracula'); // Set theme to Dracula
// when we change the content of it, update css
session.on('change', () => {
    updateCustomCSS(session.getValue());
});
Вход в полноэкранный режим Выход из полноэкранного режима

Делаем CSS постоянным

Просмотр кода Custom CSS от GooseMod очень помог в улучшении нашего редактора, но там не было никаких хороших подсказок о том, что нужно сделать для постоянного сохранения.

Поиск того, что использовать

Я попросил помощи в этом вопросе в GooseMod discord и получил ответ из одного слова: localstorage.

Изучив этот вопрос, я нашел несколько отличных примеров в документации Mozilla по localstorage. Это должно быть так же просто, как:

// get
localStorage.getItem("slackMod-CSS")

// set
window.localStorage.setItem("slackMod-CSS", css);
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы хотим иметь значение по умолчанию для нашего пользовательского css; поэтому я быстро проверил, что возвращается, когда значение еще не назначено.

Использование

Я начал с добавления этих функций к уже существующим методам get/set:

// method to quickly change css
const updateCustomCSS = newCSS => { 
    // update in storage
    window.localStorage.setItem("slackMod-CSS", newCSS);
    // update currently applied CSS
    document.querySelector("#SlackMod-Custom-CSS").innerHTML = "" 
    document.querySelector("#SlackMod-Custom-CSS").appendChild(document.createTextNode(newCSS)); 
}
// method to quickly get inner css
const getCustomCSS = () => { 
    return window.localStorage.getItem("slackMod-CSS")
}
Войти в полноэкранный режим Выход из полноэкранного режима

Затем я заменил код для установки содержимого по умолчанию в нашем CSS на следующий:

if (window.localStorage.getItem("slackMod-CSS") == null) {
    // use default CSS
    styleSheet.innerText = "/*Write Custom CSS here!*/"
    window.localStorage.setItem("slackMod-CSS", "/*Write Custom CSS here!*/")
} else {
    // get saved CSS
    styleSheet.innerText = window.localStorage.getItem("slackMod-CSS")
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это сработало!

Небольшая доработка: Установка значка категории

Я начал с измерения всех других SVG иконок категорий, и оказалось, что все они примерно 15×15.

Затем я зашел на https://feathericons.com/?query=code, настроил его так близко к 15×15, как только смог (16x), загрузил его и скопировал HTML. Я изменил размер на 15×15 вручную.

Наконец, я скопировал innerHTML одной из кнопок категорий. С его помощью я удалил старый SVG внутри и заменил его своим собственным, а также изменил текст span на Custom CSS:

// Proper Label and Icon
customTab.innerHTML = 
    `<div class="c-tabs__tab_icon--left" data-qa="tabs_item_render_icon">
    <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather feather-code"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
    </div>
    <span>Custom CSS</span>`
Войти в полноэкранный режим Выход из полноэкранного режима

Мне кажется, что это крошечное изменение в корне меняет то, насколько отполированным кажется редактор CSS.

Что теперь?

Я исправил большинство проблем, описанных в моем последнем сообщении в блоге, вот что у меня осталось:

  • Выбор пользовательского css в настройках, а затем выбор другой категории приводит к проблеме
    • Я до сих пор совершенно не знаю, как это исправить. Я потратил целых 3 часа, пробуя разные вещи…
  • Инъекция осуществляется вручную и жестко закодирована в системе пользователя.
    • Я думаю, что сделаю свой следующий блог об этом, а затем учебник о том, как установить его на свой собственный slack!

Оцените статью
devanswers.ru
Добавить комментарий