Пользовательские компоненты темы (инструкция)

На моем форуме я хотел предоставить пользователям возможности настройки по умолчанию темы. Я хотел дать им возможность установить фон для форума. Я мог бы создать разные темы для этого, но это потребовало бы довольно много дополнительной работы. И хотя у тем есть настройки, они применяются ко всем пользователям, а не конкретно к каждому отдельному пользователю. Однако существует способ реализовать настраиваемые пользователем параметры для тем, и это достигается путем «злоупотребления» пользовательскими полями.

В моей теме у пользователя на самом деле есть 3 настройки для обоев (изображение, смешивание с цветом фона и полупрозрачность контента), но в этом посте я сосредоточусь только на одной настройке — изображении обоев.

Шаг 1: создание пользовательского поля

У пользователя есть возможность выбрать один из нескольких вариантов обоев или вообще не выбирать их. Для пользовательского поля я создал необязательное выпадающее меню со следующими вариантами:

  1. none — никаких обоев, это важно
  2. Default — если пользователь ничего не выбрал
  3. Alternative One
  4. Alternative Two

Это необязательное поле, поэтому значение по умолчанию будет пустым. Для пустого значения я хочу, чтобы использовались обои по умолчанию.

Шаг 2: CSS

В файле common/common.scss я определил следующее:

:root {
  --wallpaper-default: url(#{$img-wallpaper-default});
  --wallpaper-alternative-one: url(#{$img-wallpaper-alternative-one});
  --wallpaper-alternative-two: url(#{$img-wallpaper-alternative-two});
  /* по умолчанию none, чтобы избежать возможного начального переключения изображений */
  --wallpaper: none;
}

body {
  background-image: var(--wallpaper);
}

В :root содержатся переменные, указывающие на различные изображения обоев, которые являются ресурсами темы. Фоновое изображение устанавливается в переменную --wallpaper, которая изначально равна none. Таким образом, по умолчанию при загрузке CSS обои отсутствуют. Это сделано для устранения возможного «мерцания», когда пользователь выбирает обои, отличные от исходных.

Каждый выбираемый вариант обоев получает переменную --wallpaper-xx-yy-zz. Значения пользовательских полей — это обычный текст. На основе этого значения я выберу правильную CSS-переменную для использования в качестве фонового изображения.

Шаг 3: настройки темы

В коде JavaScript мне нужно знать пользовательское поле, содержащее настройку обоев. Самый простой способ получить это — обратиться непосредственно к ID пользовательского поля. Поэтому вам нужно создать настройку темы для конфигурации этого ID пользовательского поля.

wallpaper_user_field:
  default: -1
  type: integer

Шаг 4: установка обоев

Для этого вам нужно написать немного JavaScript. Поскольку этот код должен выполняться как можно раньше, создайте инициализатор javascripts/discourse/initializer/your-theme-component.js.

Начнем с основ:

import { setOwner } from '@ember/owner';
import { withPluginApi } from 'discourse/lib/plugin-api';

const PLUGIN_ID = 'your-theme-component';

class YourThemeWallpaper {
  api;
  styleRoot;

  constructor(owner, api) {
    setOwner(this, owner);
    this.api = api;
    // получаем элемент `html`
    this.styleRoot = document.body.parentElement.style;
    this.loadWallpaperSettings();
  }

  // позже будет добавлена дополнительная логика
}

export default {
  name: PLUGIN_ID,
  initialize(owner) {
    withPluginApi('1.34.0', (api) => {
      this.instance = new YourThemeWallpaper(owner, api);
    });
  }
}

Это шаблонный код для добавления JavaScript-компонента темы. Переменная styleRoot должна ссылаться на элемент html документа, который мы будем использовать для изменения эффективного CSS.

Функция loadWallpaperSettings выполнит фактическую загрузку настроек пользователя и выглядит примерно так:

loadWallpaperSettings() {
  let user = this.api.getCurrentUser();
  if (user) {
    this.api.container.lookup('store:main').find('user', user.username).then((user) => {
      let wp = user.user_fields[settings.wallpaper_user_field] || '';
      this.setWallpaper(wp);
    });
  } else {
    this.setWallpaper('');
  }
}

Чтобы получить значения пользовательских полей, необходимо сделать дополнительный API-запрос, который может занять некоторое время. После его завершения мы получаем значение пользовательского поля и вызываем setWallpaper с этим значением. settings.wallpaper_user_field — это ссылка на настройку темы из шага #3. Если настройка не была сконфигурирована, поле wp по умолчанию будет пустой строкой.

Если текущего пользователя нет, мы вызываем функцию setWallpaper с пустой строкой, чтобы загрузить обои по умолчанию.

Функция setWallpaper фактически обновляет обои:

setWallpaper(wallpaper) {
  if (!wallpaper || wallpaper === '') {
    // используем обои по умолчанию, если ничего не задано
    wallpaper = 'default';
  }
  // нормализуем значение, чтобы оно соответствовало CSS-переменной
  wallpaper = wallpaper.toLowerCase().replaceAll(/[^a-z0-9]+/g, '-');
  if (wallpaper === 'none') {
    this.styleRoot.setProperty('--wallpaper', 'none');
  } else {
    this.styleRoot.setProperty('--wallpaper', 'var(--wallpaper-'+wallpaper+')');
  }
}

Если настроенная настройка обоев равна "none", мы удаляем фоновое изображение. В противном случае значение нормализуется, чтобы соответствовать CSS-переменной. Таким образом, “Alternative One” приведет к установке CSS-переменной --wallpaper в var(--wallpaper-alternative-one). Если значение пользовательского поля не соответствует CSS-переменной, это фактически приведет к отсутствию обоев, что вполне приемлемо.

Шаг 5: ускорение загрузки

Вышеописанная настройка работает отлично. Однако дополнительный API-запрос может привести к тому, что обои будут установлены довольно поздно, что не дает хорошего результата. Чтобы решить эту проблему, я использую Локальное хранилище браузера для сохранения и извлечения настроек.

В конструкторе, перед загрузкой настроек обоев пользователя, сначала попробуйте установить обои на основе данных из локального хранилища. При загрузке настроек пользователя обновите локальное хранилище тем, что было сконфигурировано.

const WALLPAPER_KEY = 'your_theme_wallpaper';

constructor(owner, api) {
  setOwner(this, owner);
  this.api = api;
  // получаем элемент `html`
  this.styleRoot = document.body.parentElement.style;
  this.loadLocalStorage();
  this.loadWallpaperSettings();
}

loadWallpaperSettings() {
  let user = this.api.getCurrentUser();
  if (user) {
    this.api.container.lookup('store:main').find('user', user.username).then((user) => {
      let wp = user.user_fields[settings.wallpaper_user_field] || '';
      localStorage.setItem(WALLPAPER_KEY, wp);
      this.setWallpaper(wp);
    });
  } else {
    localStorage.removeItem(WALLPAPER_KEY);
    this.setWallpaper('');
  }
}

loadLocalStorage() {
  let data = localStorage.getItem(WALLPAPER_KEY);
  if (data) {
    this.setWallpaper(data);
  }
}

Это значительно улучшает раннюю установку обоев при загрузке Discourse. Фактически функция setWallpaper будет вызываться дважды. Но поскольку значение редко меняется, это не будет заметно. Мерцание произойдет только при следующей загрузке, если пользователь изменил обои. Именно поэтому начальное значение обоев в CSS установлено в none, а не в значение по умолчанию.

Шаг 6: живой предпросмотр

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

Для реализации этого потребуется немного дополнительных ухищрений путем модификации компонентов пользовательских полей.

В конце конструктора я добавил вызов this.setupLivePreview(), который содержит следующее:

setupLivePreview() {
  api.onPageChange((url, title) => {
    if (this.wallpaperPreviewed) {
      this.wallpaperPreviewed = false;
      this.loadWallpaperSettings();
    }
  });

  let _this = this;
  api.modifyClass('component:user-fields/dropdown', {
    pluginId: PLUGIN_ID,
    didUpdateAttrs() {
      if (this.field.id == settings.wallpaper_user_field) {
        _this.wallpaperPreviewed = true;
        _this.setWallpaper(this.value);
      }
    }
  });		
}

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

Далее — настоящая магия. Мы модифицируем компонент выпадающего списка пользовательских полей, добавляя дополнительный код в функцию didUpdateAttrs. Это будет вызвано, если пользовательское поле получит новое значение. Когда это произойдет, мы установим переменную wallpaperPreviewed в true, чтобы при изменении страницы были загружены сохраненные настройки. Затем мы вызываем функцию setWallpaper, чтобы показать выбранные в данный момент обои.

То же самое можно сделать и для других типов пользовательских полей. Например, для текстового поля вы модифицируете класс component:user-fields/text.

Заключение

С помощью этого вы даете пользователям некоторый контроль над тем, как компоненты темы могут влиять на их опыт, используя пользовательские поля.

Единственная проблема заключается в том, что пользовательские поля находятся в разделе Настройки > Профиль вместо Настройки > Интерфейс, где, вероятно, было бы удобнее их разместить.

10 лайков