new

mpcVisualEditor

Визуальный редактор контента
Версия 1.0.8-rc
Дата выпуска 12.06.2026
Загрузки 0
Просмотры 5
Внимание, этот компонент несовместим с MODX 3.
НЕ РАБОТАЕТ С 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.8-rc

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

  • Новый редактор 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}) с загрузкой/добавлением/удалением. Источник правды — конфиг-запись (fieldConfigRecord): неизменённыеsrc/poster/sources[].srcшлём КЛЮЧАМИ лексикона (бэкmergeMediaRecordих не трогает, превью — из DOM), изменённые/новые → загруженный URL. Запись переносится целиком (Object.assignпо ключам конфига) с переопределением правленых полей — литеральные ключи (title/width/height/controls/…) НЕ теряются (mergeMediaRecord лексиконит только src/poster/sources[].src; прочее — литерал). Бэк:ImageUploadHandlerобобщён подkind(image|video|audio, по умолчанию image — обратная совместимость): белые списки расширений video/audio, mime-проверка через finfo (ogg/webm-контейнеры — мягко), подпапкиvideos//audios/; новыйprocessUpload-параметр$defaultBase. Лексиконmpcve_err_upload_extобобщён («Недопустимый тип файла»). Фронт:editors/media.js(new),api.js(uploadMedia(file,kind)),editors/index.js(+media),constants.js(TYPE_HINT),overlay.css(.mpcve-media__*`). Загрузка нескольких источников + замена основного файла — рабочие. picture (M12) — без изменений.
  • [M14] Ссылки: блокировка навигации в edit-mode + редактор адреса (href). (1) В режиме правки клик по любому (внутренние/внешние, маркированные и нет) больше НЕ переходит по ссылке — `bindClicks` (app.js) гасит навигацию для немаркированных ссылок (`e.target.closest('a')` → preventDefault), а маркированные открывают редактор. (2) Маркер (data-mpc-field/rfield/tv) НА самом теге/→ каттер кладёт значение поля в `href` (Cutter.php), значит значение поля = АДРЕС ссылки: `editorTypeFor` (address.js) для тега a/link возвращает тип `link` (детерминированно, приоритет над ftype), новый редактор `editors/link.js` правит href через `field/save` (бэк не менялся — пишет URL так же, как при нарезке, лексикон-aware round-trip). Текст ссылки правится отдельным маркером на элементе ВНУТРИ (обычный текст-редактор) — без изменений. Осталось по п.2 TODO: атрибут-поля кнопок (id целевой модалки и пр.) — нужен data-mpc-attr-маркер на нарезке (сейчас зарезервирован в Cutter, не используется), вынесено отдельным шагом.
  • [M11.6.1] Фиксы вложенных списков (по следам тестирования): (1) клон новой строки снимает с картинок lazyload/expand-триггеры (data-lazy/data-svg, хелпер blankImg) — иначе expand.js сайта падал на пустой картинке клона (getImgData: fetch несуществующего пути → нет ` →removeAttributeof undefined). (2) Лексиконизация полей вложенной строки нового элемента — см. mpc 2.5.20-rc (fieldUsesLexiconKeysрасширен): раньше «значения сохраняются, но после reload пустые» (литерал в конфиге + чанк{$f|lexicon}`).
  • [M11.6] Вложенные списки + JS-рендер добавления строки (без автоперезагрузки). (1) ВЛОЖЕННЫЕ списки теперь редактируются построчно: openRowsEditor больше их не отклоняет; listAddress для контейнера data-mpc-field-N строит path (спуск к родительской строке через buildRowPath по уровню N), parentField = имя самого списка → бэк mutateRows по path (mpc 2.5.19). Детект списка обобщён: listFieldAttr/itemAttrForLevel, isListEl/listRows работают по уровню вложенности (строки уровня N — data-mpc-item-N), фильтр closest не захватывает строки более глубоких списков. (2) Операции отражаются в DOM средствами JS, БЕЗ автоперезагрузки (раньше каждая операция дёргала window.location.reload()): delete — убираем узел; move (↑/↓) — переставляем узлы; add — клон первой строки как ШАБЛОН с очисткой значений (cloneBlankRow: скаляры → пусто+CSS-плейсхолдер, медиа → прозрачный 1×1, контейнеры вложенных списков НЕ чистим — структура строк сохраняется, бэк сидирует тем же deep-clear) + markFieldsWithin (поля клона сразу редактируемы). Так в новом элементе дочерний список заполняем сразу. Перезагрузка — вручную (кнопка «Обновить» в панели), когда нужен точный рендер шаблона (лексикон/условные блоки). Разметка поля вынесена в mark.js (markEl/markFieldsWithin) — общая для app.js и rows.js без цикла импортов. Файлы: address.js, mark.js (new), editors/rows.js, app.js.
  • [M10.3] Панель скрытых полей: исключение настроек по бэку, диспатч по типу, показ ЗНАЧЕНИЙ лексикона. Три правки: (1) Утечка полей таба «Настройки» (hide_section, «копировать содержимое» и т.п.) — хардкод STRUCTURAL был неполон. Теперь fields/types (FieldTypesHandler) дополнительно отдаёт settings = имена полей таба «Настройки» (formtabs index 0) из живого mpc_base; фронт исключает их (S.settingsFields, isSettingsField). (2) Раньше все скрытые поля рисовались как input/textarea. Теперь контрол выбирается по ТИПУ поля (controlHtml/wireControl в panels.js): image → превью + загрузка файла (значение-путь, image/upload); richtext → contenteditable (форматирование видно); прочее → input/textarea. (3) В лексиконном режиме конфиг хранит КЛЮЧ — панель показывала ключ (а сохранение без правки записало бы сам ключ в значение лексикона — порча). Теперь config/get дополнительно отдаёт карты lexicons по уровням (resource/global), фронт показывает ЗНАЧЕНИЕ (lexValue); запись прежним путём — бэк пишет значение в лексикон под существующим ключом (round-trip корректен). Бэк: LexiconWriter::all() (дамп key→value), FieldWriter::readLexicons(), Mpcve::readLexicons(), ConfigGetHandler. Фронт: state.js (+settingsFields/lexicons), app.js/api.js (приём), panels.js, overlay.css (.mpcve-hpanel__rte/__img/__thumb).
  • [M10.2] Скрытые поля СЕКЦИИ — обобщение с фикс-набора стилей на вычисляемый диф. Раньше панель секции показывала фиксированно inline_styles/class_names. Теперь: показываем стилевые поля (всегда, как раньше — каскад resource-first) ПЛЮС скрытые КОНТЕНТНЫЕ поля = ключи config-объекта секции МИНУС весь таб «Настройки»/служебные/css_file_path (= STRUCTURAL, стили вынесены отдельной веткой) МИНУС ВИДИМЫЕ (имена из [data-mpc-field]/[data-mpc-rfield]/[data-mpc-tv] этой секции). Уровень: контент static→global иначе resource; стили — resource-first (каскад M10.1). v1 — скаляры; скрытые migx-списки/медиа пропускаются (нет DOM-контейнера для построчного редактора, реально

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