diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py index 31d8776..a33eee2 100644 --- a/lpon_site/frontend/admin.py +++ b/lpon_site/frontend/admin.py @@ -468,17 +468,19 @@ class MusicStyleAdmin(RequestInFormMixin, admin.ModelAdmin): }), ) - # Дополнительный CSS для валидации и вотчер для отслеживания изменений - class Media: - css = {'all': ('css/validation-override.css', )} # Стили для обхода валидации - js = ('js/form-field-watcher.js', ) # Вотчер для отслеживания изменений полей формы + # Обогащение Media из формы (CodeMirrorFormMixin) + # Используем наследование для расширения, а не перекрытия + # Это гарантирует что все CSS/JS из формы будут загружены + class Media(MusicStyleAdminForm.Media): + css = {'all': (*MusicStyleAdminForm.Media.css['all'], 'css/validation-override.css',)} + js = (*MusicStyleAdminForm.Media.js, 'js/form-field-watcher.js',) def save_model( - self, - request: HttpRequest, - obj: TbMusicStyle, - form: forms.ModelForm, - change: bool, + self, + request: HttpRequest, + obj: TbMusicStyle, + form: forms.ModelForm, + change: bool, ) -> None: """ Переопределяем save_model для добавления информативных сообщений в админку. @@ -584,10 +586,12 @@ class ArtistAdmin(RequestInFormMixin, admin.ModelAdmin): }), ) - # Дополнительный CSS для валидации и вотчер для отслеживания изменений - class Media: - css = {'all': ('css/validation-override.css', )} # Стили для обхода валидации - js = ('js/form-field-watcher.js', ) # Вотчер для отслеживания изменений полей формы + # Обогащение Media из формы (CodeMirrorFormMixin) + # Используем наследование для расширения, а не перекрытия + # Это гарантирует что все CSS/JS из формы будут загружены + class Media(ArtistAdminForm.Media): + css = {'all': (*ArtistAdminForm.Media.css['all'], 'css/validation-override.css', )} + js = (*ArtistAdminForm.Media.js, 'js/form-field-watcher.js', ) def save_model( self, @@ -703,10 +707,12 @@ class LabelAdmin(RequestInFormMixin, admin.ModelAdmin): }), ) - # Дополнительный CSS для валидации и вотчер для отслеживания изменений - class Media: - css = {'all': ('css/validation-override.css', )} # Стили для обхода валидации - js = ('js/form-field-watcher.js', ) # Вотчер для отслеживания изменений полей формы + # Обогащение Media из формы (CodeMirrorFormMixin) + # Используем наследование для расширения, а не перекрытия + # Это гарантирует что все CSS/JS из формы будут загружены + class Media(LabelAdminForm.Media): + css = {'all': (*LabelAdminForm.Media.css['all'], 'css/validation-override.css', )} + js = (*LabelAdminForm.Media.js, 'js/form-field-watcher.js', ) def save_model( self, diff --git a/public/static/js/form-field-watcher.js b/public/static/js/form-field-watcher.js index 681f880..7305161 100644 --- a/public/static/js/form-field-watcher.js +++ b/public/static/js/form-field-watcher.js @@ -14,6 +14,10 @@ * 4. Визуально показывает пользователю статус обхода валидации цветом кнопок * * Универсальное решение: работает для любых форм в админке, не только для лейблов. + * + * ВАЖНО: Этот скрипт работает независимо от порядка загрузки и инициализации CodeMirror. + * Используется стратегия проверки readyState и MutationObserver для динамического отслеживания + * появления новых элементов, включая CodeMirror div.codemirror. */ // Функция для добавления GET параметра к form action кнопки @@ -42,7 +46,8 @@ function markSubmitButtonsToIgnoreValidation() { }); } -document.addEventListener('DOMContentLoaded', function () { +// ===== ОСНОВНАЯ ФУНКЦИЯ ИНИЦИАЛИЗАЦИИ ВОТЧЕРА ===== +function initFormWatcher() { // Находим все submit-кнопки администратора let submitButtons = document.querySelectorAll('input[type=submit]'); @@ -80,39 +85,51 @@ document.addEventListener('DOMContentLoaded', function () { } // Отслеживаем изменения всех типов полей в форме - let formInputs = document.querySelectorAll('input:not([type=submit]), textarea, select, .codemirror, [contenteditable]'); + // ВАЖНО: Используем MutationObserver для динамического отслеживания появления CodeMirror элементов + let watchedInputs = new WeakSet(); - // Функция которая срабатывает при любом изменении - 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); + 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); } }); - form.setAttribute('action', originalAction); - - // Скрываем сообщения об ошибках валидации - let errorNotes = document.querySelectorAll('.errornote, .errorlist'); - errorNotes.forEach(function (errorElement) { - errorElement.style.display = 'none'; - }); } - formInputs.forEach(function (input) { - // Слушаем оба события: 'change' для обычных input/select - // и 'input' для CodeMirror и других редакторов - input.addEventListener('change', handleChange); - input.addEventListener('input', handleChange); - }); + // Первый раз присоединяем слушателей к уже существующим элементам + attachWatchersToInputs(); // Мониторим изменение класса force-ignore-validation на кнопках // Используем MutationObserver для отслеживания добавления/удаления класса - const observer = new MutationObserver(function (mutations) { + const classObserver = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { let btn = mutation.target; @@ -130,8 +147,67 @@ document.addEventListener('DOMContentLoaded', function () { }); }); - // Настраиваем observer для отслеживания изменения класса + // Настраиваем observer для отслеживания изменения класса на кнопках submitButtons.forEach(function (btn) { - observer.observe(btn, {attributes: true, attributeFilter: ['class']}); + 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); + } +}