mpcVisualEditor

Визуальный редактор контента
Версия 1.0.17-rc
Дата выпуска 22.06.2026
Загрузки 2
Просмотры 264
НЕ РАБОТАЕТ С MODX 3 Пока

mpcVisualEditor (mpcVE)

Вступление

Визуальный фронт-редактор контента для сайтов на migxpageconfigurator (mpc). Администратор правит содержимое страниц прямо на фронте (inline), не заходя в админку. Сохранение идёт в те же хранилища, что использует mpc: TV mpc_config, lexicon-файлы (переводы), нативные поля ресурса и произвольные TV.

Что умеет

  • Inline-редактирование прямо на странице, по одному полю: правка сохраняется сразу, без перезагрузки и без перезаписи всей страницы.
  • Поля любого типа: текст, textarea, форматированный текст (RTE), ссылки, изображения, picture/video/audio, а также TV-типы listbox, checkbox, tags, число, дата, файл.
  • Списки (повторяющиеся блоки): добавление, удаление и перетаскивание строк, загрузка картинки в строку.
  • Боковая панель секций: порядок (drag-drop), показ/скрытие секции, переключение «статическая секция».
  • Панель скрытых полей: правка полей, которых нет в разметке страницы (настройки секции, стили, произвольные поля). Требует mpc 2.5.4+.
  • Встроенный файловый менеджер (на modMediaSource): обзор, загрузка, создание папок, переименование и удаление, выбор существующего файла — прямо из редакторов изображения, медиа и файловых TV.
  • История изменений: кто, когда и что менял, с пословным diff и откатом. Логируются правки и из редактора, и из админки.
  • Блокировка ресурса: пока страницу правит один администратор, другой не откроет её на редактирование (TTL = idle-таймаут, настраивается).
  • Мультиязычность: правка значений лексиконов с учётом текущего языка.
  • Кнопки в тулбаре: очистить кэш сайта, открыть текущий ресурс в админке.


Требования и зависимости

  • MODX Revolution 2.8.x
  • PHP 7.4+
  • migxpageconfigurator (mpc) 2.4.0+ — ЖЁСТКАЯ зависимость. mpcVE надстраивается над mpc и использует его address-space, edit-mode рендер и фасад. Для панели скрытых полей нужен mpc 2.5.4+.
  • pdoTools (приходит вместе с mpc; нужен для рендера страниц).


Как пользоваться

  1. Дайте роли пользователей право (permission) mpcve_edit — оно проверяется при входе в режим правки. При сохранении дополнительно проверяется право save на конкретный ресурс (нельзя править чужое в обход).
  2. Откройте нужную страницу с query-параметром:
    ?mpcedit=1
    (имя параметра задаётся настройкой mpcve_edit_param, по умолчанию mpcedit).
  3. На странице появятся маркеры редактируемых блоков и тулбар. Кликаете блок — правите в открывшемся редакторе — изменение сохраняется сразу в хранилище mpc.

Мастер-выключатель всего пакета — системная настройка mpcve_active. Для работы также требуется включённая настройка mpc_edit_mode (пакет migxpageconfigurator) и перенарезка страниц — иначе в чанках нет data-mpc-* маркеров и редактор не подключится (предупреждение пишется в системный лог).

Системные настройки

  • mpcve_active — включить/выключить пакет (по умолчанию: Да).
  • mpcve_edit_param — query-параметр входа в режим правки (по умолчанию mpcedit
    ?mpcedit=1
    ).
  • mpcve_permission — имя требуемого permission (по умолчанию mpcve_edit).
  • mpcve_max_upload — лимит размера загружаемого изображения, байт (по умолчанию 10485760 = 10 МБ; 0 — без лимита).
  • mpcve_lock_ttl — TTL блокировки ресурса = idle-таймаут, сек (по умолчанию 300).
  • mpcve_allowed_attrs — белый список HTML-атрибутов для очистки содержимого редактируемого поля (по умолчанию class,href,src,alt,title). Обработчики
    on*
    ,
    javascript:
    и опасный
    style
    режутся всегда, независимо от этой настройки.

1.0.17-rc

  • ИСПРАВЛЕНО: кнопка-триггер скрытых полей блока/строки иногда была не видна — она рендерилась внутрь блока с position:absolute и обрезалась overflow:hidden, схлопывалась нулевой высотой строки или перекрывалась оформлением. Теперь триггеры выносятся в отдельный портал-слой (position:fixed поверх страницы), а позиция каждой кнопки считается от getBoundingClientRect блока и пересчитывается на скролле/ресайзе. Файлы: js/panels.js, css/overlay.css.

1.0.16-rc

  • ИСПРАВЛЕНО: thumbnailType источника читался через getOption() (без передачи свойств источника он всегда возвращал дефолт 'png') → для JPEG искался кандидат base.png, конвертированный base.webp не находился, происходил откат на исходный .jpg, которого после конвертации нет → 404. Теперь свойство читается штатно через getPropertyList()['thumbnailType'].

1.0.15-rc

  • ИЗМЕНЕНО: финальный URL/путь загруженного файла после возможной конвертации (jpg→webp плагином проекта) резолвится строго ОТ ИСТОЧНИКА файлов, без знания о конкретном плагине. Два детерминированных кандидата: имя от ядра (fileHandler->sanitizePath) и base.; если на диске оба (конвертер не удалил оригинал) — берётся новее по mtime. Прежний контракт через свойство источника (sgUploads) убран — mpc больше не знает о проектных плагинах.

1.0.14-rc

  • ИЗМЕНЕНО: каноническая папка типа резолвится строго ОТНОСИТЕЛЬНО базы источника файлов mpc (по mpc_download_paths), без префикса удалённой настройки mpc_media_path. Поведение на штатной установке не меняется (настройка и так была пустой). Требует mpc 2.5.47-rc+.

1.0.13-rc

Загрузка файлов редактора — единый нативный пайплайн MODX.

  • ИЗМЕНЕНО: все загрузки редактора идут через ОДИН экшен files/upload и нативный modMediaSource::uploadObjectsToContainer() (тот же путь, что у файл-менеджера MODX): события OnFileManagerBeforeUpload/Upload отрабатывают, проектные плагины применяют свою политику (whitelist, транслитерация, конвертация формата). Отдельный экшен image/upload и класс ImageUploadHandler удалены — их роль выполняет FileManagerHandler.
  • НОВОЕ: загрузка и «Выбрать существующий» идут в ПАПКУ ТЕКУЩЕГО файла поля (drag-drop кладёт картинку рядом с уже выбранной; менеджер открывается в той же папке). Нет текущего значения → файл уходит в каноническую папку типа из mpc_download_paths. Новый эндпоинт files/locate (url→папка источника).
  • НОВОЕ: гибридная защита «тип ↔ папка» — в каноническую папку типа (images/videos/ audios из mpc_download_paths) нельзя загрузить файл другого типа.
  • ИЗМЕНЕНО: дедуп по содержимому убран; коллизии имён обрабатывает ядро MODX (при upload_check_exists=true — отказ «файл уже существует», без перезаписи).
  • ВНУТРЕННЕЕ: общее ядро MediaLibrary (списки расширений, accept, canonicalDir, sanitize/SVG/mime, контракт результата загрузки) — единый источник истины.
  • NB: нативный путь проверяет СИСТЕМНУЮ настройку upload_maxsize (дефолт 1 МБ) — на проектах без поднятого лимита крупные файлы отклонит ядро.

1.0.12-rc

  • НОВОЕ: у списков отдельная всегда-видимая кнопка-хэндл ☰ (правый верхний угол обёртки), открывающая редактор строк. Раньше, когда обёртка списка и её элементы одинаковой ширины, hover-пиктограммы налезали друг на друга и до «порядок/ добавить/удалить» было не докликаться (клик ловил самый глубокий элемент).
  • НОВОЕ: информер копий лексикона — элементы с data-mpc-copy вне секции получают бейдж 📋 и тултип «Это копия лексикона. Оригинал: …» (адрес из data-mpc-copy).
  • ИСПРАВЛЕНО: в панели «Скрытые поля» появлялось мнимое поле _origin = resource. Сайдбар добавлял служебный _origin прямо в общие объекты секций S.configData; теперь тегируется клон, а панель защитно игнорирует _-ключи.

1.0.11-rc

Инлайн-правка произвольных лексиконных ключей (data-mpc-lexicon).

  • НОВОЕ: элемент с маркером data-mpc-lexicon="topic:key" (вне секции) становится редактируемым — клик открывает инлайн-правку содержимого (текст/HTML; через data-mpc-ftype можно textarea/richtext). Сохранение через field/save с адресом type=lexicon (топик и ключ читаются из маркера). Требует mpc 2.5.43-rc+.
  • На элементе data-mpc-section маркер data-mpc-lexicon НЕ трактуется как произвольный ключ (это префикс лексикона секции) — фильтр по data-mpc-section.

1.0.10-rc

Правка скрытых/вырезанных полей секции и MIGX-списков из сайдбара.

  • НОВОЕ: панель полей секции (config-driven) — правит поля, вырезанные из вёрстки (data-mpc-remove) и вспомогательные, включая MIGX-СПИСКИ (строки + поля строк, любой вложенности). Доступна из сайдбара «Секции» (кнопка «✎ поля» у секции) и из панели «Скрытые поля» блока на странице. Редактор строк (add/delete/move + правка полей строки) работает по адресу/конфигу, без DOM-элемента списка; медиа/картинки/picture в строках открываются своими редакторами.
  • ИСПРАВЛЕНО: медиа/picture-редактор для записей ВНУТРИ строк списка открывался пустым — fieldConfigRecord не шёл по path к строке и не обрабатывал уже распарсенные вложенные массивы. Теперь подхватывает источники/постер/картинку.

1.0.9-rc

Положение панели редактора и её сворачивание.

  • НОВОЕ: системная настройка mpcve_toolbar_position — где закреплена панель (тулбар) редактора: top (вверху, по умолчанию) или bottom (внизу). При bottom отступ страницы и баннер блокировки переезжают соответственно. Значение прокидывается на фронт через window.mpcVEConfig (Mpcve::getClientConfig).
  • НОВОЕ: кнопка сворачивания панели — сворачивает тулбар в компактный хэндл в левом углу (остаётся только кнопка-разворот), не резервируя место под контент. Состояние запоминается в cookie mpcve_collapsed и переживает перезагрузку.

1.0.8-rc

Color-picker, контакты (иконки/класс), настройки, порядок секций, откат строк.

  • Логирование через mxLogger: новый класс MpcVEServices\Logger (на сервисе Mpcve). Прямые $this->modx->log(ERROR/WARN, ...) в хендлерах/коннекторе/плагине заменены на logger. mxLogger — единственный приёмник (стандартный журнал MODX не дублируется; лишь разовое «установите mxLogger», если он не установлен). Логирование включено из коробки: mpcve_debug=Да, mpcve_log_level=error. Тэги mpcvisualeditor + mpc + этап (page/contact/cache/changelog/connector/prerender).
  • Новый редактор color.js: пикер цвета + hex для полей config/TV (тип color/colorpicker). Значение хранится БЕЗ # (как админка MODX). FieldTypesHandler: color/colorpicker → редактор color.
  • Контакты (contact.js/ContactSaveHandler): image-иконка через file manager (поле icon); правка class-режим (иконка-классом) с фильтром служебных классов mpcve-*.
  • Настройки (info.js/SettingsListHandler): схлоп системной+контекстной записи в одну эффективную строку (target определяет, куда писать); виджет для image-xtype (file manager + превью); colorpicker без #; убран показ типа поля в модалке и панели.
  • Секции (sidebar.js): порядок по position (-1 вверху, своя position приоритетнее типовой), клик по имени → скролл к секции на странице с подсветкой, поиск секции в DOM по MIGX_formname.
  • Списки (rows.js): последняя строка очищается, а не удаляется (структура-образец сохраняется); inherit-диалог при добавлении строки в наследуемую секцию (api.js).
  • История (RowOpHandler/LogHandler/Mpcve): откат row-операций (add/delete/move) — восстановление поля-списка к снимку до операции.
  • CacheClearHandler: кнопка «Кэш» дополнительно чистит core/cache/system_settings/ (настройки/ClientConfig подхватываются без ручного сброса).

1.0.7-rc

Настройки: список из исходных шаблонов + тип виджета из БД; фикс дубля контакта.

  • Панель «Настройки» теперь из settings/list: сервер сканирует ИСХОДНЫЕ шаблоны (mpc_path_to_src) на data-mpc-info → видны ВСЕ настройки, включая удалённые из вёрстки (data-mpc-remove). Тип/значение — из БД (settingMeta).
  • Редактор настройки подбирает виджет по xtype из БД: combo-boolean → Да/Нет, colorpickerfield → color, code/textarea → textarea, иначе текст. Защищённые — с 🔒 без сохранения. И панель, и инлайн-клик берут тип из settings/list (кэш api.loadSettingsList/findSetting).
  • Фикс дубля контакта при смене значения (см. mpc 2.5.34 — ckey identity). Файлы: SettingsListHandler.php (new), Connector.php, editors/info.js (rewrite), settings.js (rewrite), api.js, overlay.css.

1.0.6-rc

Редактирование контактов (data-mpc-cfield) из редактора.

  • Правка полей контакта со страницы: value / caption / attributes. Клик по data-mpc-cfield → модалка-textarea с предупреждением «меняется на всех страницах». Запись через ContactSaveHandler (contact/save) → mpc Mpc::saveContact (грабер-логика: identity/лексиконы/мёрж в TV «Контакты»).
  • ТРЕБУЕТСЯ data-mpc-key: контакты без ключа не помечаются редактируемыми (identity по md5(value) нестабилен) — подсказка «добавьте ключ». Под правом mpcve_edit_global (как настройки).
  • Аудит в истории (action=contact, revertable=0). Файлы: ContactSaveHandler.php, Connector.php, editors/contact.js, mark.js, app.js, editors/index.js.

1.0.5-rc

Редактирование служебных настроек (data-mpc-info) из редактора.

  • Правка глобальных настроек сайта (системные/контекстные/ClientConfig) прямо со страницы: инлайн по видимому data-mpc-info + панель «⚙ Настройки» в тулбаре со списком ВСЕХ data-mpc-info страницы (включая — favicon/метрики, по которым нельзя кликнуть). Редактор — модалка-textarea с предупреждением «меняется на всех страницах».
  • Новое право mpcve_edit_global (PermissionChecker::canEditGlobal, прокинуто во фронт как editGlobal): без него настройки не помечаются редактируемыми и кнопка/панель скрыты. Регистрируется резолвером в политике Administrator.
  • Запись через InfoSaveHandler (action info/save) → mpc InformationUpdater::saveSetting (blacklist защищённых). Аудит в истории (revertable=0). Файлы: InfoSaveHandler.php, PermissionChecker.php, Mpcve.php, Connector.php, editors/info.js, settings.js, mark.js, app.js, constants.js, overlay.css, resolvers/1permissions.php.

1.0.4-rc

Санитайз вставки, откат медиа-правок, работа с секциями типа в сайдбаре.

  • Санитайз при вставке ВЕЗДЕ: инлайн-текст (text.js) и textarea вставляют только text/plain; RTE (rte.js) чистит буфер через sanitizeHtml (разрешённые теги/атрибуты остаются, style/on*/чужая разметка срезаются). Иначе вставка из IDE тащила стили в поле и в лексикон. + saveField прогоняет keepHtml-значение через sanitizeHtml перед отправкой.
  • Медиа-редакторы (image/picture/media) теперь шлют old в field/save → правки картинок/видео стали откатываемыми с диффом в истории (раньше — только аудит). file.js уже слал old.
  • Сайдбар «Секции»: объединённый список — свои секции + наследуемые из ТИПА под 🔒 (клик копирует секцию в текущий ресурс). Сверху массовые операции (только для не-типов): «Дополнить из типа» (merge), «Перезаписать из типа» (overwrite, с подтверждением), «Очистить» (с подтверждением). На странице-типе — только свои секции. Файлы: sidebar.js, overlay.css, Mpcve.php (copySectionFromType/copySectionsFromType/isTypeResource), ConfigGetHandler.php (отдаёт type+isType), SectionOpHandler.php (ops copy_one/from_type/clear).

1.0.3-rc

Фикс: смена картинки в простом image-TV ломала шаблон.

  • editors/image.js (isRecordImage): data-mpc-tv трактуется как путь-строка, а не migx-запись. Раньше редактор слал в простой image-TV массив [{MIGX_id,src,alt,title,width,height}], тот попадал в src="{$resource.tvs.}" и валил Fenom при запекании parsed/ («Unexpected token ':' near '{"MIGX_id":'»). migx-TV в редакторе пока не поддержаны (M29), так что любой data-mpc-tv — путь; запись-массив остаётся только для config-полей (data-mpc-field). В пару к серверному барьеру FieldWriter::writeTv (mpc 2.5.30).
  • docs/readme.txt переоформлен в едином стиле с mpc (allowed-tags разметка).

1.0.2-rc

Документация и порядок в системных настройках.

  • readme.txt переписан: что умеет / требования и зависимости / как пользоваться / системные настройки.
  • TODO.md (dev-роадмап) убран из файлов пакета.
  • Все 6 системных настроек получили название и описание (ru/en) — раньше показывались сырыми ключами без описаний. Сгруппированы по областям: «Основные» (mpcve_general) и «Редактор» (mpcve_editor), с русскими заголовками.

1.0.0-alpha (в разработке)

Визуальный фронт-редактор контента для проектов на migxpageconfigurator (mpc). Зависит от mpc 2.4.0+ (для панели скрытых полей — 2.5.4+). Ведётся поэтапно; в прод — после перехода на -rc.

  • [M26 шаг 3] Модалка истории изменений (вместо панели). changelog.js переписан: модалка с ТАБЛИЦЕЙ (время/юзер/уровень/секция·поле/было→стало/откат). Фильтры: секция → зависимое поле (опции из самих записей), источник (редактор/админка по source), сортировка по дате (↑/↓). Инлайн-ДИФФ старое↔новое по словам (LCS, красным / зелёным; фолбэк «старое→новое» при >300 токенов). Клик по диффу — развернуть полное содержимое (CSS max-height). Откат (↩) — как был (log/revert + reload). Источник из админки помечен ⚙. Бэк LogHandler::list теперь отдаёт source/level (из address). CSS .mpcve-clogm*. Файлы: changelog.js (переписан), LogHandler.php, overlay.css. M26 (полный лог редактор+админка) завершён.
  • [M26 шаг 2] Аудит правок из АДМИНКИ в лог изменений (закрыта половинчатость). Handlers/AdminAudit: OnBeforeDocFormSave снимает СТАРЫЕ значения прямым SQL из БД (на OnBefore нативные поля уже = новые из формы, TV ещё старые → object-state ненадёжен); OnDocFormSave в начале (ДО re-cut handleFile, иначе ре-граб перетёр бы новое) диффит old↔new и пишет в {prefix}mpcve_changelog. Трекаем: все скаляр-поля mpc_config (config=mpc-контент) + rfield/tv из манифеста mpc_tracked_fields. УРОВЕНЬ (resource/type/global по сохраняемому ресурсу: id==sbp→global, parent==sbp→type, иначе resource) и source=admin кладём в address JSON — без ALTER таблицы; откат уровне-aware (LogHandler→writeField). Списки/записи — лог коротко (revertable=0). Событие OnBeforeDocFormSave ЗАРЕГИСТРИРОВАНО на стенде (modPluginEvent, plugin id=1) — ДОБАВИТЬ в build-декларацию плагина для пакета. Проверено симуляцией (longtitle + hero.content → записи с level=type, source=admin). Файлы: Handlers/AdminAudit.php (new), Plugins/OnBeforeDocFormSave.php (new), Plugins/OnDocFormSave.php. Осталось шаг 3 — модалка истории (фильтры из манифеста, source-фильтр, инлайн-дифф).
  • [M26 шаг 1] Манифест трекаемых полей (для полного лога изменений: редактор + админка). На нарезке SectionProcessor::rebuildTrackedFields собирает из data-mpc-* маркеров шаблона набор трекаемых полей (field — верхний уровень секции, kind=list для списков; rfield/tv — по всему html) и пишет в таблицу {prefix}mpc_tracked_fields (helper Handlers/TrackedFields, raw PDO, авто-создание). Глобальный союз со всех шаблонов, строки тегированы template_id → на нарезке replace-by-template (без стейла). Уровень (resource/type/global) в манифесте НЕ хранится — определяется при записи лога по сохраняемому ресурсу (для отката). Назначение: membership-фильтр админ-лога (особенно rfield/tv, которых нет в mpc_config) + источник выпадаек «секция→поля» модалки истории. Шаги 2-3 (админ-лог diff+level в OnDocFormSave, модалка) — далее. Файлы: Handlers/TrackedFields.php (new), Grabber/SectionProcessor.php.
  • [mpc fix] Умный мерж при перенарезке путал секции с одинаковым data-mpc-section. Симптом: две секции data-mpc-section="features" (оригинал data-mpc-lexicon="features" + копия data-mpc-lexicon="features_copy" data-mpc-copy) — после СОХРАНЕНИЯ ресурса (OnDocFormSave→ре-граб с умным мержем) оригинал начинал показывать заголовок копии. Причина: SectionProcessor::indexConfigBySection и lookup существующей секции для мержа ключевались по MIGX_formname (="features" у обеих) → коллизия → обе получали $existing одной секции (копии, последней в индексе), её значения затирали свежий граб оригинала. На ПЕРВОЙ нарезке existing пуст → мержа нет → было верно (отсюда «после нарезки ок, после сохранения нет»). Фикс: ключ мержа sectionMergeKey = MIGX_formname|lexicon_prefix (data-mpc-lexicon различает оригинал/копию; префикс всегда непуст — фоллбэк sectionName, однотипные секции матчатся как прежде). Файл: Grabber/SectionProcessor.php. ВНИМАНИЕ: уже испорченный конфиг лечится полной перенарезкой (mgr_tpl, updContent — шаблон перезаписывает), т.к. умный мерж сохраняет существующее значение как «правку админа».
  • [M25.2] Инвалидация parsed при правке значения (фикс «значение не меняется после правки»). Причина: для нестатичных секций значения ЗАПЕКАЮТСЯ в parsed/.tpl на eager-пассе ($listbox на финальном per-request пассе уже нет — ленивый резолв возможен только в статичных секциях через getStaticSection), а parsed регенерится ТОЛЬКО при отсутствии файла. Теперь parsed правленого ресурса сносится при сохранении → пересборка с новым значением при следующем заходе. Редактор: FieldWriter::afterSaveinvalidateParsed (файл ресурса; а если ресурс-ТИП/донор parent=staticBlocksPage или level=global → весь parsed, т.к. влияет на наследующих). Админка: Plugins/OnDocFormSave — то же (донор → clearCache, иначе deleteParsedConfigFile). Файлы: FieldWriter.php, Plugins/OnDocFormSave.php.
  • [M27.1] Скрытые поля секции — ЯВНЫЙ список исключений (со слов пользователя) вместо хардкод-STRUCTURAL. isSectionExcluded = S.settingsFields (таб «Настройки секции» из mpc_base) + css_file_path (путь к файлу стилей) + migx-служебка (MIGX_id/MIGX_formname/limit) + стилевые (показаны отдельной веткой с каскад-уровнем). ВСЁ прочее из конфига, чего нет в DOM (data-mpc-field), → в скрытые, включая кастомные поля (раньше хардкод-STRUCTURAL мог не учесть). STRUCTURAL оставлен только для служебных под-полей СТРОК списков (itemHidden). Файл: panels.js.
  • [M27] Стили секции: поле props («Дополнительные свойства», textarea) добавлено в редактируемую стилевую ветку панели скрытых полей (SECTION_STYLE_FIELDS += props, и в STRUCTURAL чтобы не дублировалось в контентной ветке). css_file_path остаётся неправимым (путь к файлу). inline_styles/class_names были раньше. Файл: constants.js.
  • [M25.3] listbox-multiple доведён. Значение хранится СТРОКОЙ ключей через "||" (формат MODX/migx — admin-сетка корректно round-trip'ит). ВАЖНО (фикс багов): сначала пробовал хранить массивом — admin-migx-виджет КОРНЕВОГО поля стрингифил массив-объект в "[\"tg\",\"ph\"]" → {foreach} по строке ломался (корневое не сохранялось/не рендерилось; вложенное в JSON строки списка переживало). Решение — строка "||" + | split в Fenom. Капшены опций лексиконизируются и для multiple (writeListboxOptions($multiple=true)). Плейсхолдер (PlaceholderProcessor): лексиконы ВКЛ → `{set $mpcItems = $field | split: '||'}{foreach $mpcItems as $item}##('{prefix}_{field}_{$item}')|lexicon}{if !$item@last}, {/if}{/foreach}` (капшены через запятую); ВЫКЛ → `{$field | split:'||' | join: ','}`. ContentParser нормализует "tg,ph"/"tg||ph" → "tg||ph". Редактор `listbox.js`: multiple читает DOM по запятой, сохраняет "||"-строку (raw=1). Проверено: root и nested рендерят «Telegram, Phone», editor round-trip (write "dc||em" → «Discord, Email»). Файлы: `Cutter/PlaceholderProcessor.php`, `Grabber/{LexiconManager,FieldValueExtractor,ContentParser}.php`, `FieldWriter.php`, `editors/listbox.js`. Ручной foreach/join в шаблоне не нужен — каттер генерит в.
  • [M25.1] Лексиконизация listbox (одиночного). Схема: капшены опций уезжают в лексикон ключами {sectionPrefix}_{field}_{optionKey} (пустой key → ..._), значение поля = СЫРОЙ ключ опции, на рендере резолв ##'{prefix}_{field}_{$value}' | lexicon}. Условия (грабер и каттер считают одинаково, оба видят элемент+значение): список keyed (Caption==key), текущее значение ∈ ключей, shouldLexiconize('text',…), НЕ multiple. Иначе — сырое {$value} (без лексикона). Реализация: LexiconManager::{getSectionPrefix,parseListboxOptions,writeListboxOptions}; FieldValueExtractor::getValue (listbox-ветка: пишет капшены, хранит сырой ключ); PlaceholderProcessor::{setDefaultPlaceholder,listboxPlaceholder} (плейсхолдер с ключом-конкатом; deferVar → Fenom ~). Save: FieldWriter::writeConfigField уважает address.raw=1 (пропускает лексиконный блок) — редактор listbox.js шлёт raw=1 (значение=ключ, не лексиконить повторно). Фронт listbox.js: текущий выбор по капшену И по ключу (после лексикона DOM показывает капшен), на сохранении обновляет DOM тем же видом. Multiple — без лексикона (значение "k1||k2" несовместимо с конкат-ключом). Действует после перенарезки. Файлы: Grabber/{LexiconManager,FieldValueExtractor}.php, Cutter/PlaceholderProcessor.php, FieldWriter.php, editors/listbox.js.
  • [M25 ФАЗА 2 — фронт mpcVE] Редактор listbox + лимит строк. (1) Новый редактор editors/listbox.js (select / multi-select): опции из data-mpc-values формата "Caption==key||..." парсятся на фронте; "@SELECT ..." (динамика migx) на фронте не резолвится в v1 → ручной ввод значения с подсказкой. Сохранение field/save (значение=ключ; multiple → ключи через "||"). Роутинг типа: address.ftypeToEditor (listbox/listbox-multiple), FieldTypesHandler.editorType (по inputTVtype), реестр editors/index.js, подсказки constants.TYPE_HINT, стиль .mpcve-lb__sel. (2) rows.js: data-mpc-max с контейнера списка → блокировка «+ Добавить» при достижении лимита (кнопка disabled + причина в title, и guard на клик). Файлы: editors/{listbox(new),index,rows}.js, address.js, constants.js, overlay.css, mpcvisualeditor/.../FieldTypesHandler.php. ОГРАНИЧЕНИЯ v1: @SELECT-опции — ручной ввод (резолвер позже); листбокс-значение не лексиконизируем (enum-ключ). Действует после перенарезки (чтобы появился listbox-тип в конфиге) + Ctrl+F5.
  • [M25 ФАЗА 1 — каттер] Атрибуты data-mpc-values и data-mpc-max в нарезке (SectionProcessor). (1) data-mpc-values — опции listbox: значение вставляется КАК ЕСТЬ в inputOptionValues field-определения (migx сам разбирает "Caption==key||..." и "@SELECT ..."), тип поля → listbox (если автор не задал listbox/listbox-multiple через data-mpc-ftype). Новые синтез-типы listbox/listbox-multiple в synthesizeScalar. (2) data-mpc-max на контейнере списка → extended.maxRecords авто-конфига строки (buildListConfigsaveListConfig), migx ограничивает число записей. Правки: fieldAttrs (+values,+max), synthesizeScalar, makeFieldDef, buildListConfig, saveListConfig. Применяется ПОСЛЕ перенарезки шаблона. ФАЗА 2 (фронт mpcVE: редактор-выпадайка + учёт max в rows.js) — отдельно. Файлы: migxpageconfigurator/.../Grabber/SectionProcessor.php.
  • [M24.2] Диалог наследуемой секции — 2 варианта (со слов пользователя): «Скопировать в эту страницу» (inherit=copy) ИЛИ «Открыть страницу-источник и править там» (редирект на ресурс-тип, где секция реально лежит — правка там обычным resource-путём). Убран вариант «Править тип» (запись типа из текущей страницы). Бэк добавляет в ответ inherit_choice typeResourceId+typeUrl (makeUrl(..,'full')); фронт на выбор goto делает window.location на источник (edit-mode держится cookie). Backend-режим inherit=type оставлен в FieldWriter (не используется диалогом). Фикс: на выборе goto возвращаем «вечный» промис (не res), иначе редактор мигал тостом 'section inherited...' за миг до навигации. Файлы: FieldWriter.php, api.js.
  • [M24.1] Фикс: двойной/повторный клик по инлайн-полю внутри перезагружал страницу. bindClicks при уже открытом инлайн-редакторе (contenteditable=true) делал ранний return без preventDefault → второй клик по тексту внутри ссылки-обёртки уходил в навигацию по href. Теперь в этом случае гасим переход, если клик внутри ` (каретка ставится на mousedown — preventDefault на click её не ломает). Файлы:app.js`.
  • [M24] Наследование уровней при записи поля (фикс «empty mpc_config for level resource»). Причина: ресурс с пустым mpc_config (напр. главная res 1) наследует секции от ТИПА (Render мёржит type+resource; лексиконы — каскад global→type→resource в getLexiconFilenames), а FieldWriter::writeConfigField лез прямо в пустой конфиг ресурса. Теперь: если секции нет в конфиге ресурса (наследуется) → по выбору юзера (address.inherit): type — пишем в конфиг/лексикон ТИПА (resolveLevelResource('type'), глобально для шаблона); copyseedSectionIntoResource копирует объект секции из type-конфига в mpc_config ресурса + лексикон-ключи секции (по lexicon_prefix) из файла типа в файл ресурса (тот же ключ → ресурсный перекрывает типовой при мердже, перенарезка не нужна), дальше обычная resource-запись; без выбора — бэк отдаёт {code:'inherit_choice', section}. Фронт (api.js, централизованно для всех редакторов): ловит inherit_choicechoiceDialog (Скопировать в страницу / Править тип / Отмена) → повтор с inherit. Статичная секция (level=global) — без диалога, тост-предупреждение «изменение глобально». Новое: FieldWriter.{configHasSection,seedSectionIntoResource}, dom.choiceDialog, api rawPost+handleFieldSave, CSS .mpcve-confirm__actions. ОГРАНИЧЕНИЕ: покрыт field/save (скаляр/медиа-поля); row/op (добавление строк списка) на наследуемой секции пока не сидирует — следующий шаг. Файлы: migxpageconfigurator/.../FieldWriter.php, mpcvisualeditor api.js/dom.js/overlay.css.
  • [M23.3] Source-режим распространён на textarea + переключатель в richtext. (1) textarea.js: поле с разметкой (el.querySelector('*')) правится как СЫРОЙ HTML (показываем el.innerHTML, на сохранении el.innerHTML = value); плоский текст — прежний multiline plain. Симметрично инлайн text.js (M23.2). (2) richtext.js: кнопка « Код / Визуально» в модалке — переключение визуальный RTE ↔ правка сырого HTML в textarea (теги видны/правятся руками). Содержимое синхронизируется при переключении (через sanitizeHtml по allowedTags), сохраняется HTML активного режима; RTE пересоздаётся на том же host. CSS: .mpcve-modal__spacer (распорка actions), .mpcve-rte__source (моноширинный). Файлы: editors/textarea.js, editors/richtext.js, overlay.css.
  • [M23.2] Инлайн-правка поля с разметкой = правка ИСХОДНОГО HTML. По просьбе пользователя: при правке поля с тегами (, и т.п.) редактор показывает сырой HTML как редактируемый текст (Подобрать подушку), чтобы теги можно было удалить/заменить (раньше contenteditable показывал отрендеренный жирный — самих тегов не видно). openTextEditor: sourceMode = el.querySelector('*') → на открытии el.textContent = innerHTML (теги видны), на закрытии el.innerHTML = el.innerText (сырой HTML → реальная разметка) + saveField (читает innerHTML = разметку). Плоский текст — прежний WYSIWYG. Esc возвращает исходный рендер. Заменяет подход M23.1 (там показывали рендер с кареткой в конец — теги всё равно не были видны). Файлы: editors/text.js.
  • [M23.1] Фикс: при инлайн-правке поля с разметкой форматирование исчезало «на глазах». openTextEditor при открытии выделял ВСЁ содержимое (range.selectNodeContents → select-all); для поля с //… первый же ввод заменял выделение плоским текстом → визуально «осталось только текст». Теперь: поле с разметкой (el.querySelector('*')) — каретку в КОНЕЦ (range.collapse(false)), содержимое целиком сохраняется и правится по месту; плоский текст — прежнее select-all (удобно перепечатать). Вместе с M23 (сохранение innerHTML) инлайн-правка полностью сохраняет вложенную разметку. Файлы: editors/text.js.
  • [M23] Фикс: инлайн-редактор текста срезал HTML-разметку поля. Поле вида грабер сохраняет С разметкой (FieldValueExtractor::getValue: есть дочерние теги → HTML детей), но text.js при сохранении брал el.innerText для всего, кроме richtext → после первой правки ` пропадал, поле становилось плоским текстом. ТеперьsaveFieldотдаётinnerHTML, если в поле есть разметка (el.querySelector('*')) или типrichtext; чистый текст по-прежнемуinnerText(без лишнего экранирования &/ TTL — авто-выход (release + reload в view). Снятие: явный «Завершить» (releaseOnExit) и закрытие вкладки/уход (pagehidenavigator.sendBeaconlock/release). Сеть упала на acquire → правку не блокируем (fail-open). Settingmpcve_lock_ttl(_build/mpcvisualeditor/elements/settings.php). Файлы:Handlers/LockHandler.php(new),Connector.php,lock.js(new),app.js,overlay.css(.mpcve-lockbanner`).
  • [M20] Тулбар: безопасные кнопки «🧹 Кэш» (полная очистка кэша MODX — экшен cache/clearCacheClearHandlercacheManager->refresh()) и «⚙ Админка» (открыть текущий ресурс в админке: managerUrl?a=resource/update&id=` в новой вкладке; `managerUrl добавлен в client-config из настройки manager_url). Перенарезку-из-фронта НЕ делаем (деструктивна — отдельный заход с confirm). Файлы: Mpcve.php (+managerUrl), Handlers/CacheClearHandler.php (new), Connector.php (+экшен), app.js (кнопки тулбара).
  • [M19.1] Фикс сайдбара: структурные операции над секциями через отдельный экшен section/op (RAW-запись массива конфига), а НЕ field/save. Причина багов: field/save для position/hide_section/is_static ЛЕКСИКОНИЗИРОВАЛ значение в ключ (если content-type поля переводимый) — ключ всегда truthy → видимость переключалась в одну сторону, порядок/статичность ломались. Плюс миграции is_static не было вообще. Новый SectionOpHandler + Mpcve::saveConfig/FieldWriter::saveConfig (пишут весь массив секций сырьём, без лексикона, + сброс кэша): move (новый порядок имён → переставить массив + position 1..N), visibility (hide_section), static (is_static + МИГРАЦИЯ: при включении копия секции кладётся в конфиг staticBlocksPage — рендер берёт контент оттуда; при выключении убирается). Reorder — один запрос (был N). Фронт sidebar.js зовёт section/op. Файлы: FieldWriter.php (+saveConfig), Mpcve.php, Handlers/SectionOpHandler.php (new), Connector.php, sidebar.js. Тесты mpc 290/290.
  • [M19] Сайдбар управления секциями (порядок / видимость / статичность). Кнопка «☰ Секции» в тулбаре (в режиме правки) → фиксированный сайдбар справа со списком секций ресурса (из configData.resource, сорт по position). Для каждой: drag-хваталка «⋮⋮» (переупорядочивание), 👁 видимость (hide_section), 📌 статичность (is_static). Всё пишется field/save на уровне resource (стаб секции c position/is_static/hide_section живёт в ресурс-конфиге; рендер Render::parseConfig мёржит type+resource и сортирует uasort по position). Drag → переназначает position 1..N и пишет изменённые. Эффект (скрытие/порядок/статичность меняют РЕНДЕР) — после «Обновить». Файлы: sidebar.js (new), app.js (кнопка тулбара), overlay.css. v1-ограничения: список — секции ресурс-конфига (секции только из type/donor-конфига не показываются); смена is_static флипает флаг через field/save — миграция контента resource↔static зависит от существующего механизма mpc (проверить на данных).
  • [M18] RTE-переработка: тулбар из mpc_allowed_tags, события, картинки, pluggable. (1) Тулбар строится из системной настройки mpc_allowed_tags (прокинута во фронт Mpcve::getClientConfigS.cfg.allowedTags) — кнопка показывается только если её тег разрешён (бессмысленно предлагать тег, который вырежет strip_tags на записи лексикона). Маппинг тег→кнопка: b/i/u/s→exec, small/mark/span→обёртка выделения (нет в execCommand), p/h1..h6/blockquote→formatBlock, ul/ol→списки, a→ссылка, img→картинка; «очистить формат» — всегда. (2) Картинки — через наш загрузчик (api image/upload/uploadMedia), кнопка только если img разрешён. После загрузки — диалог атрибутов (.mpcve-imgdlg: превью, alt, title, замена файла) → вставка ` с атрибутами черезinsertHTML(голыйinsertImageне давал задать атрибуты — фикс по тесту). Клик по уже вставленной картинке открывает тот же диалог для правки её атрибутов. (3) Событияmpcve:rte:nodeadded/noderemoved/nodechanged(MutationObserverна области, всплывают,detail.node[,attribute]) — проект слушает и навешивает классы/обработчики на вставленные теги. (4) **Pluggable**:editors/rte.js— дефолтный провайдер + реестр;window.MpcVE.setRte(provider)подменяет редактор (provider.create(container,{value,allowedTags,upload})→{getHTML,focus,destroy}). (5) КлиентскийsanitizeHtml(html, allowedTags)на save — зеркалитstrip_tagsбэка (разрешённые теги с атрибутами остаются, прочие разворачиваются), применяется в richtext-модалке и панели независимо от провайдера. Fallback: еслиallowedTagsне пришёл (старая кэш-страница) — дефолтный набор тегов (пустой[]= настройка реально пустая = ничего не разрешено). ⚠️ чтобы появились кнопки ссылки/картинки — добавьa,imgвmpc_allowed_tags. Файлы:Mpcve.php,editors/rte.js(переписан),editors/richtext.js,panels.js,mpcve.js,overlay.css`.
  • [M17] Списки: drag-drop переупорядочивание строк (вместо ↑/↓). У каждой строки grip-хваталка «⠿», строка draggable; перетаскивание → серверный move(from→to) (array_splice в ConfigFieldWriter::moveRow — значения/лексикон-ключи едут со строкой) + перестановка узла страницы (splice-семантика: to>from → после ref, иначе перед ref) + перерисовка панели. Кнопки ↑/↓ убраны, ✕/📷 остались. editors/rows.js (wireDrag, dragFrom), overlay.css (.mpcve-rows__grip/--drag/--over). Touch-DnD (мобильные) — позже при необходимости.
  • [M16.5] Скрытые поля: picture/video/audio-ЗАПИСИ редактируются в панели (закрыт хвост — раньше пропускались). Редакторы picture/media получили value-based режим: openPictureEditor(el, override) / openMediaEditor(el, override) — при override={addr[, isVideo]} адрес/запись берутся из конфига (fieldConfigRecord), el может быть null (превью пустые, DOM нет); на странице — как было. Панель (panels.js): recordKind определяет тип записи по форме (вложенный img/sources[].srcset → picture; sources[].src + poster/width → video, иначе audio); для таких полей в строке — кнопка «✎ Редактировать», открывает полный редактор value-based. Панель — сама .mpcve-modal, поэтому сперва закрывается (иначе guard редактора блокирует). v1: только секционные записи (item-level nested — fieldConfigRecord не достаёт). Файлы: editors/picture.js, editors/media.js, panels.js.
  • [M16.4] Фикс: тип редактора для TV/rfield брался по совпадению ИМЕНИ с config-полем (TV subtitle ловил тип одноимённого config-поля → открывался textarea вместо простого текста). Причина: editorTypeFor брал S.typesMap[name] (карта config-полей mpc_base) для ЛЮБОГО адреса. Теперь карта по типу адреса: tv → отдельная карта типов TV (FieldTypesHandler отдаёт tvs = имя TV → тип по modTemplateVar.type); rfield → стандартные типы ресурс-полей MODX (content→richtext, introtext/description→textarea, прочее→text); field → прежняя typesMap. data-mpc-ftype на маркере по-прежнему переопределяет. Файлы: FieldTypesHandler.php (+tvs), state.js (tvTypes), app.js, address.js (RFIELD_TYPES + выбор карты по addr.type).
  • [M16.2] Скрытые поля: одиночные img-ЗАПИСИ теперь редактируются в панели (раньше пропускались — parseRecord отсекал любые записи). isImgRecord (есть src, нет sources/img — так отличаем от picture/video/audio-записей со sources/вложенным img); такой дескриптор идёт типом image с полем record. Контрол image (превью+загрузка) теперь value-based для записей: getValue собирает запись целиком (структура сохраняется), меняет только src при загрузке нового файла — иначе оставляет исходный src (лексикон-ключ → бэк не трогает). Адрес/запись через тот же field/save, что и on-page img-редактор. picture/video/audio-записи в панели пока пропускаются (реже, нужен value-based порт тех редакторов). Файл: panels.js.
  • [M16.1] Скрытые поля правятся ПО ТИПУ (как на странице). RTE-тулбар вынесен в общий editors/rte.js (rteToolbarHtml/wireRteToolbar) — используют и модальный richtext-редактор, и панель скрытых полей. В панели (panels.js) richtext теперь не голый contenteditable, а RTE с форматированием (тулбар над областью, .mpcve-hpanel__rtebox); textarea (тип отдаётся FieldTypesHandler отдельно с M16) — `; text — input; image — загрузка. Файлы:editors/rte.js(new),editors/richtext.js(использует общий),panels.js,overlay.css`.
  • [M16] Текст: инлайн только для простого text; textarea и richtext — в МОДАЛКЕ. Раньше и text, и richtext правились инлайново (contenteditable), многострочный textarea тоже шёл как text. Теперь: (1) text — инлайн contenteditable (как было, простые однострочные); (2) textarea — модалка с ` (значение plain text, обновляемel.textContentбез перезагрузки) — новыйeditors/textarea.js; (3)richtext— модалка с настоящим RTE: тулбар B/I/U/S, H2/H3/¶, маркир./нумер. списки, ссылка (с возвратом выделения после prompt), убрать ссылку, очистить формат — на нативномdocument.execCommand(без сборки/зависимостей); значение = innerHTML, пишетсяfield/save(лексикон-round-trip как у инлайна) — новыйeditors/richtext.js. Детект типа:FieldTypesHandler::editorTypeтеперь отдаётtextareaотдельно отtext;ftypeToEditor(address.js) —textarea/richtext. Реестрeditors/index.js:richtextбольше НЕ роутится в инлайн-текст. Файлы:editors/{textarea,richtext}.js(new),editors/index.js,address.js,constants.js(TYPE_HINT),overlay.css(.mpcve-ta*/.mpcve-rte*),FieldTypesHandler.php`. RTE на execCommand — v1; при необходимости позже заменить на полноценный редактор.
  • [M15.3] Аудио: кнопка-аффорданс «✎» для открытия редактора. M15.2 (force controls) сделал видимым, но клик по нему всё равно не открывал редактор: целиком занят НАТИВНЫМ контрол-баром — UA обрабатывает клик внутри (play/seek) и НЕ шлёт click на host-элемент, поэтому bindClicks не срабатывал (у video есть кликабельный кадр над баром — потому video работал). Теперь attachAudioBadges (app.js) оборачивает каждый audio.mpcve-editable в .mpcve-audio-wrap и вешает кнопку .mpcve-media-badge (✎) → клик открывает editors.media. removeAudioBadges разворачивает при выходе. CSS — overlay.css. force-controls (M15.2) оставлен как гарантия видимости (если секцию перережут с controls=0 → условный рендер не выведет атрибут). Обёртка inline-block со shrink-to-fit схлопывала аудио с темовым width:100% до мин-ширины (бар превращался в огрызок) — JS замеряет ширину аудио ДО оборачивания и задаёт её обёртке (> audio { width:100% }), вид сохраняется.
  • [M15.2] Фиксы по тестированию video/audio: (1) ` безcontrolsимеет нулевую видимую область (0×0) → по нему нельзя кликнуть, чтобы открыть редактор (видео всегда видно — кадр/постер). В режиме правкиmarkElвременно включает нативныеcontrolsна audio (data-mpcve-controls-флаг),unmarkEditableснимает их при выходе. (2) Сайтовыйexpand.js(getImgData) падалCannot read properties of undefined (removeAttribute), если контент по data-lazy/src — не SVG (битый путь/растр/медиа): добавлен null-guardif (!svg) return. Файлы:mark.js,app.js,migxpageconfigurator/js/web/expand.js`.
  • [M15.1] Булевы атрибуты video/audio стали редактируемыми (чекбоксы controls/autoplay/loop/muted) — после того как каттер начал рендерить их УСЛОВНО (mpc 2.5.22-rc: renderBoolAttrs{if $rec.controls}controls{/if}, 0 реально выключает). Редактор шлёт 1/0; preload (mpc теперь грабит строкой) правится селектом. Работает, если атрибут был в разметке на нарезке. Фронт: editors/media.js (вернули чекбоксы + boolOf/attrInit), overlay.css (убрал заглушку-note).
  • [M15] Редактор VIDEO/AUDIO (одиночные). Клик по / (тип media) → модалка: основной файл (загрузка), постер (только video, картинка), preload (селект), булевы атрибуты (controls/autoplay/loop/muted — см. M15.1) и массив источников ` ({type, media, src}) с загрузкой/добавлением/удалением. Источник правды — конфиг-запись (field

Последние обсуждения в сообществе MODX.pro