/** * Вотчер для отслеживания изменений полей формы и управления режимом обхода валидации. * * Используется для валидации с поддержкой игнорирования: * 1. Добавляет функцию addGetParam для динамического добавления GET параметров к form action * 2. Отслеживает изменения всех типов полей в форме: * - Стандартные: input (все типы кроме submit), textarea, select * - CodeMirror редакторы (div.codemirror) * - Редактируемое содержимое (contenteditable элементы) * 3. При изменении данных: * - Удаляет класс force-ignore-validation ко кнопок (возвращает нормальный цвет) * - Удаляет GET параметр из action кнопок * - Скрывает сообщения об ошибках валидации (.errornote, .errorlist) * 4. Визуально показывает пользователю статус обхода валидации цветом кнопок * * Универсальное решение: работает для любых форм в админке, не только для лейблов. * * ВАЖНО: Этот скрипт работает независимо от порядка загрузки и инициализации CodeMirror. * Используется стратегия проверки readyState и MutationObserver для динамического отслеживания * появления новых элементов, включая CodeMirror div.codemirror. */ // Функция для добавления GET параметра к form action кнопки function addGetParam(button, key, value) { const form = button.form; // Находим форму, в которой лежит кнопка const baseAction = form.getAttribute('action'); // Получаем текущий action // Удаляем старый параметр, если он есть let cleanAction = baseAction.split('?')[0]; // Проверяем, есть ли уже в action другие GET-параметры const separator = cleanAction === baseAction ? '?' : '&'; // Динамически прописываем измененный URL в formAction кнопки button.formAction = baseAction + separator + key + '=' + value; } // Функция для добавления класса force-ignore-validation ко всем submit-кнопкам формы // Используется при клике на кнопку "Я проверил и уверен!" function markSubmitButtonsToIgnoreValidation() { // Находим все submit-кнопки на странице и добавляем им класс // form-field-watcher.js потом отследит добавление класса через MutationObserver // и добавит соответствующие onclick обработчики document.querySelectorAll('input[type=submit]').forEach(function (btn) { btn.classList.add('force-ignore-validation'); }); } // ===== ОСНОВНАЯ ФУНКЦИЯ ИНИЦИАЛИЗАЦИИ ВОТЧЕРА ===== function initFormWatcher() { // Находим все submit-кнопки администратора let submitButtons = document.querySelectorAll('input[type=submit]'); // Если нет submit-кнопок, выходим (не админская форма) if (submitButtons.length === 0) { return; } // Находим форму let form = document.querySelector('form'); if (!form) { return; } // Сохраняем оригинальный action формы let originalAction = form.getAttribute('action'); // Функция для добавления onclick обработчика function addOnclickHandler(btn) { // Проверяем есть ли уже onclick if (!btn.getAttribute('onclick')) { btn.setAttribute('onclick', 'if (this.classList.contains("force-ignore-validation")) { ' + 'const form = this.form; ' + 'const baseAction = form.getAttribute("action") || ""; ' + 'let cleanAction = baseAction.split("?")[0]; ' + 'const separator = cleanAction === baseAction ? "?" : "&"; ' + 'form.setAttribute("action", baseAction + separator + "ignore_validate=1"); ' + '}'); } } // Функция для удаления onclick обработчика function removeOnclickHandler(btn) { btn.removeAttribute('onclick'); } // Отслеживаем изменения всех типов полей в форме // ВАЖНО: Используем MutationObserver для динамического отслеживания появления CodeMirror элементов let watchedInputs = new WeakSet(); function attachWatchersToInputs() { let formInputs = document.querySelectorAll('input:not([type=submit]), textarea, select, .codemirror, [contenteditable]'); // Функция которая срабатывает при любом изменении function handleChange() { // При изменении любого поля: // 1. Удаляем класс force-ignore-validation (кнопки вернут нормальный цвет) // 2. Удаляем onclick обработчик // 3. Восстанавливаем оригинальный action формы submitButtons.forEach(function (btn) { if (btn.classList.contains('force-ignore-validation')) { btn.classList.remove('force-ignore-validation'); removeOnclickHandler(btn); } }); form.setAttribute('action', originalAction); // Скрываем сообщения об ошибках валидации let errorNotes = document.querySelectorAll('.errornote, .errorlist'); errorNotes.forEach(function (errorElement) { errorElement.style.display = 'none'; }); } formInputs.forEach(function (input) { // Избегаем двойного присоединения слушателей (используем WeakSet) if (!watchedInputs.has(input)) { watchedInputs.add(input); // Слушаем оба события: 'change' для обычных input/select // и 'input' для CodeMirror и других редакторов input.addEventListener('change', handleChange); input.addEventListener('input', handleChange); } }); } // Первый раз присоединяем слушателей к уже существующим элементам attachWatchersToInputs(); // Мониторим изменение класса force-ignore-validation на кнопках // Используем MutationObserver для отслеживания добавления/удаления класса const classObserver = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { let btn = mutation.target; if (btn.tagName === 'INPUT' && btn.type === 'submit') { // Если класс добавлен - навешиваем onclick if (btn.classList.contains('force-ignore-validation')) { addOnclickHandler(btn); } // Если класс удален - удаляем onclick else if (btn.getAttribute('onclick')) { removeOnclickHandler(btn); } } } }); }); // Настраиваем observer для отслеживания изменения класса на кнопках submitButtons.forEach(function (btn) { classObserver.observe(btn, {attributes: true, attributeFilter: ['class']}); }); // ===== ДИНАМИЧЕСКОЕ ОТСЛЕЖИВАНИЕ ПОЯВЛЕНИЯ НОВЫХ ЭЛЕМЕНТОВ (особенно CodeMirror) ===== // Используем MutationObserver для отслеживания появления новых элементов формы // Это нужно на случай если CodeMirror инициализируется ПОСЛЕ загрузки скрипта const formObserver = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { // Проверяем появились ли новые узлы (добавлены новые элементы) if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { // Проверяем есть ли среди добавленных элементов CodeMirror div или input/textarea let hasNewFormElements = false; mutation.addedNodes.forEach(function (node) { if (node.nodeType === 1) { // Node.ELEMENT_NODE if (node.classList && (node.classList.contains('codemirror') || node.tagName === 'INPUT' || node.tagName === 'TEXTAREA' || node.tagName === 'SELECT')) { hasNewFormElements = true; } // Проверяем в потомках if (node.querySelector && (node.querySelector('.codemirror') || node.querySelector('input:not([type=submit])') || node.querySelector('textarea') || node.querySelector('select'))) { hasNewFormElements = true; } } }); // Если нашли новые элементы формы, повторно присоединяем слушателей if (hasNewFormElements) { attachWatchersToInputs(); } } }); }); // Настраиваем observer для отслеживания изменений в форме (появления новых элементов) formObserver.observe(form, {childList: true, subtree: true}); } // ===== ИНИЦИАЛИЗАЦИЯ: Работает в любых условиях (до или после DOMContentLoaded) ===== // Стратегия: // 1. Если document.readyState === 'loading', ждем DOMContentLoaded // 2. Если document.readyState === 'interactive' или 'complete', запускаем сразу // 3. Используем requestIdleCallback если доступен, иначе setTimeout if (document.readyState === 'loading') { // DOM еще загружается, ждем события document.addEventListener('DOMContentLoaded', initFormWatcher); } else { // DOM уже загружен (скрипт загрузился после DOMContentLoaded) // Запускаем инициализацию асинхронно чтобы все элементы успели загрузиться if (typeof requestIdleCallback !== 'undefined') { // Используем requestIdleCallback для загрузки в свободное время браузера requestIdleCallback(initFormWatcher, {timeout: 2000}); } else { // Fallback: используем setTimeout с минимальной задержкой setTimeout(initFormWatcher, 0); } }