Собственный тип поля в системных настройках

Заметка о том, как добавить в системные настройки MODX собственный тип поля в виде выпадающего списка

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

Для начала стоит посмотреть на то, как выбор типа системной настройки выглядит сейчас. Если зайти в системные настройки и нажать большую зеленую кнопку “Создать новый параметр”, то откроется окно, где можно выбрать тип поля у будущей записи. Этот список конечный и нигде в системе нельзя его отредактировать.

Список типов системных настроек в MODX Revolution

У себя в компонентах системные настройки с собственным типом поля я создавал программно (специальным скриптом, который выполняется при установке пакета), поэтому после установки они уже были доступны для редактирования и ими можно было пользоваться. Но вот как вручную добавить настройку с новым типом? Хороший вопрос! Пришлось в очередной раз “хакнуть” ExtJS. 🙂

Собственный выпадающий список

Для начала нужно реализовать сам выпадающий список со своими значениями, которые будут использоваться при редактировании системной настройки. В качестве примера я выбрал простой список возрастов детей по западной классификации (что первое под руку попалось).

  • Infants (0-1 year)
  • Toddlers (1-3 years)
  • Preschoolers (3-5 years)
  • Middle Childhood (9-11 years)
  • Young Teens (12-14 years)
  • Teenagers (15-17 years)

Список значений – это обычный компонент ComboBox. Примеров реализации подобных выпадающих списков в достатке в коде MODX, можно смело подсматривать и копировать. Если хотите вникнуть в суть вопроса глубже, можно посмотреть документацию по этому компоненту.

Теперь необходимо создать файл с компонентом где-нибудь в assets/components/scf/js/mgr/childhood-periods.combo.js. Вы можете использовать любой путь, в общем-то, я же выбрал тот, который обычно принят в компонентах. Имя файла тоже не важно, но постфикс .combo.js позволяет потом легче находить нужные компоненты по имени файла, когда таких компонентов в дополнении много.

Компоненты в ExtJS – это обычные функции (замыкания по сути), которые перегружают собственный конструктор описанной там же конфигурацией, а затем ExtJS их регистрирует у себя во внутренней системе и вызывает при обращении к ним.

let ChildhoodPeriods = function(config) {
  config = config || {};

  Ext.applyIf(config, { /* Здесь будет конфигурация компонента */});
	
  ChildhoodPeriods.superclass.constructor.call(this, config);
};

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

ChildhoodPeriods = function(config) {
  config = config || {};

  /* Чуть позже сюда добавим хранилище */
  const store = null;

  Ext.applyIf(config, {
	store: store,
	name: 'childhood-periods',
    hiddenName: 'childhood-periods',
    displayField: 'caption',
    valueField: 'key',
    mode: 'local',
    triggerAction: 'all',
    editable: false,
    selectOnFocus: false,
    preventRender: true
  });

  ChildhoodPeriods.superclass.constructor.call(this, config);
};

По сути, store – это отдельный ExtJS компонент, который отвечает за загрузку и предоставление данных. В нашем случае, мы используем простое локальное хранилище, но есть возможность через хранилище загружать данные с других ресурсов в сети, включая процессоры в MODX (смотрите примеры в коде системы). При определении хранилища важно заполнить два свойства, это определение списка полей - fields и значения в самих полях непосредственно - data. Здесь важно отметить, что такая структура работает как распределенная карта (map) и значения в data присваиваются полям в том порядке, как они указаны в коде. Т.е. в случае первой записи, key будет содержать ‘inf’, caption – ‘Infants (0-1 year)’.

// Замените в коде выше строку "const store = null;" кодом, который ниже

const store = new Ext.data.SimpleStore({
  fields: ['key', 'caption'],
  data: [
	['inf', 'Infants (0-1 year)'],
    ['tdl', 'Toddlers (1-3 years)'],
    ['prs', 'Preschoolers (3-5 years)'],
    ['mch', 'Middle Childhood (9-11 years)'],
    ['ytn', 'Young Teens (12-14 years)'],
    ['tng', 'Teenagers (15-17 years)'],
  ]
});

Компонент со списком значений готов, осталось его зарегистрировать. Следующий код говорит ExtJS, что теперь в системе под именем childhood-periods будет доступен компонент со списком значений и теперь его можно свободно использовать по необходимости. В первой строке, что важно, указывается, что компонент должен все остальные функции наследовать от выпадающего списка, встроенного в MODX.

Ext.extend(ChildhoodPeriods, MODx.combo.ComboBox);
Ext.reg('childhood-periods', ChildhoodPeriods);

Полный код этого компонента доступен в GitHub Gists.

Регистрация своего типа ввода

Эта задача оказалось самой интересной, потому что ранее я с ней не сталкивался и пришлось немного попотеть, чтобы найти удачное решение. Самое простое решение было бы написать свой компонент для этого списка и зарегистрировать его вместо системного. Но как я говорил, список конечный и определен в коде системы, следовательно, при обновлении там могут появиться еще записи, которые всегда будут переопределяться нашим компонентом и списком. Еще одна проблема в том, что нам нужно этот системный список копировать в свой плагин, что только бесполезно раздувает код.

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

Но есть нюанс, потому что в ExtJS компоненты создаются по требованию и на момент загрузки страницы существует только определение типа, но сам компонент, при попытке к нему обратиться, выдаст undefined. Поэтому, чтобы решить эту проблему, делаем трюк к созданием компонента вручную.

let TypesListSpec = Ext.ComponentMgr.create(
  Ext.ComponentMgr.types['modx-combo-xtype-spec']
);

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

TypesListSpec.initialConfig.store.add(record);

Здесь стоит уточнить, что запись, это не просто объект или массив, а это именно объект класса Record и просто так создать его нельзя, не зная всех параметров. Но к счастью, хранилища в ExtJS предоставляют удобные замыкания-конструкторы (recordType) для создания подобных записей.

const record = new TypesListSpec.initialConfig.store.recordType(
  {d: 'Childhood Periods', v: 'childhood-periods'}
);

Теперь нужно собрать это всё вместе и создать новый компонент со списком типов, где в качестве хранилища определить только что отредактированное, с новыми элементами. И не забыть зарегистрировать новый компонент вместо старого.

let TypesListSpec = Ext.ComponentMgr.create(
  Ext.ComponentMgr.types['modx-combo-xtype-spec']
);

const record = new TypesListSpec.initialConfig.store.recordType(
  {d: 'Childhood Periods', v: 'childhood-periods'}
);

TypesListSpec.initialConfig.store.add(record);

TypesList = function(config) {
  config = config || {};
  Ext.applyIf(config,{
    store: TypesListSpec.initialConfig.store, // указываем свое хранилище
    displayField: 'd',
    valueField: 'v',
    mode: 'local',
    name: 'xtype',
    hiddenName: 'xtype',
    triggerAction: 'all',
    editable: false,
    selectOnFocus: false,
    value: 'textfield'
  });
  TypesList.superclass.constructor.call(this, config);
};

Ext.extend(TypesList, Ext.form.ComboBox);
Ext.reg('modx-combo-xtype-spec', TypesList);

Осталось сохранить этот код в файл и подключить в плагине, о чем ниже. Имя файла у меня такое assets/components/csf/js/mgr/ss-xtypes-loader.js.

Полный код доступен для копирования в GitHub Gists.

Регистрация frontend-компонентов через плагин

Система плагинов в MODX уникальна тем, что позволяет достаточно легко внедриться в процесс формирования страницы и загрузить дополнительный код в нужный промежуток времени. Таким образом, у нас есть возможность загрузить наши плагины и компоненты до начала генерации страницы панели управления, тем самым изменить поведение по умолчанию.

Для того, чтобы загрузить наши файлы, нужно создать плагин, который будет реагировать на событие OnManagerPageBeforeRender. Важно не забыть отметить это событие на вкладке “Системные события” при создании плагина. Затем в коде самого плагина указать, какие файлы подключить. Полный код плагина чуть длиннее, но за кадром остались лишь дополнительные проверки, которые позволяют плагину срабатывать только когда есть необходимость. Полный код плагина на GitHub Gists.

<?php

$pathPrefix = MODX_ASSETS_URL . 'components/csf/js/mgr/';

$modx->controller->addLastJavascript($pathPrefix . 'childhood-periods.combo.js');
$modx->controller->addLastJavascript($pathPrefix . 'ss-xtypes-loader.js');

Особенности и недостатки

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

Вторая особенность и недостаток системных настроек в том, что они хранят только значение. В случае же использования ComboBox, при редактировании настройки вы видим то, что указали в поле caption, когда писали свой компонент, но в качестве значения будет сохранено то, что было указано в key. Это может сбивать с толку первое время, но придется с этим смириться.

Итоги

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

Новый тип поля доступен для выбора

Редактирование значения в виде списка по двойному клику

Редактирование значения в виде списка в окне редактирования

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

Все исходные файлы к этой заметке смотрите в GitHub Gists.

Если информация была вам полезна, вы всегда можете выразить благодарность финансово с помощью кнопки под этой заметкой или подписаться на мой Patreon.

На чытанне спатрэбілася 7 хвілін Нататка змяшчае 1388 слоў

Сябры, акрамя блога, я амаль што рэгулярна пішу адмысловую рассылку пра праграмванне і тэхналогіі, дзе раз на двы тыдні збіраю самыя цікавыя навіны тэхналогій і раблю агляд цікавых інструментаў, якія мне трапіліся. Таму хутчэй падпісвайцеся, каб не прапусціць штосці цікавае наступным разам!