fix: валидатор форм, парсера и моделей (16) устранены проблемы с ватчером изменений в формах (перекрытие JS и CSS + потенциальные проблемы с порядком загрузки JS)
This commit is contained in:
+23
-17
@@ -468,17 +468,19 @@ class MusicStyleAdmin(RequestInFormMixin, admin.ModelAdmin):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Дополнительный CSS для валидации и вотчер для отслеживания изменений
|
# Обогащение Media из формы (CodeMirrorFormMixin)
|
||||||
class Media:
|
# Используем наследование для расширения, а не перекрытия
|
||||||
css = {'all': ('css/validation-override.css', )} # Стили для обхода валидации
|
# Это гарантирует что все CSS/JS из формы будут загружены
|
||||||
js = ('js/form-field-watcher.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(
|
def save_model(
|
||||||
self,
|
self,
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
obj: TbMusicStyle,
|
obj: TbMusicStyle,
|
||||||
form: forms.ModelForm,
|
form: forms.ModelForm,
|
||||||
change: bool,
|
change: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Переопределяем save_model для добавления информативных сообщений в админку.
|
Переопределяем save_model для добавления информативных сообщений в админку.
|
||||||
@@ -584,10 +586,12 @@ class ArtistAdmin(RequestInFormMixin, admin.ModelAdmin):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Дополнительный CSS для валидации и вотчер для отслеживания изменений
|
# Обогащение Media из формы (CodeMirrorFormMixin)
|
||||||
class Media:
|
# Используем наследование для расширения, а не перекрытия
|
||||||
css = {'all': ('css/validation-override.css', )} # Стили для обхода валидации
|
# Это гарантирует что все CSS/JS из формы будут загружены
|
||||||
js = ('js/form-field-watcher.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(
|
def save_model(
|
||||||
self,
|
self,
|
||||||
@@ -703,10 +707,12 @@ class LabelAdmin(RequestInFormMixin, admin.ModelAdmin):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Дополнительный CSS для валидации и вотчер для отслеживания изменений
|
# Обогащение Media из формы (CodeMirrorFormMixin)
|
||||||
class Media:
|
# Используем наследование для расширения, а не перекрытия
|
||||||
css = {'all': ('css/validation-override.css', )} # Стили для обхода валидации
|
# Это гарантирует что все CSS/JS из формы будут загружены
|
||||||
js = ('js/form-field-watcher.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(
|
def save_model(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
* 4. Визуально показывает пользователю статус обхода валидации цветом кнопок
|
* 4. Визуально показывает пользователю статус обхода валидации цветом кнопок
|
||||||
*
|
*
|
||||||
* Универсальное решение: работает для любых форм в админке, не только для лейблов.
|
* Универсальное решение: работает для любых форм в админке, не только для лейблов.
|
||||||
|
*
|
||||||
|
* ВАЖНО: Этот скрипт работает независимо от порядка загрузки и инициализации CodeMirror.
|
||||||
|
* Используется стратегия проверки readyState и MutationObserver для динамического отслеживания
|
||||||
|
* появления новых элементов, включая CodeMirror div.codemirror.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Функция для добавления GET параметра к form action кнопки
|
// Функция для добавления GET параметра к form action кнопки
|
||||||
@@ -42,7 +46,8 @@ function markSubmitButtonsToIgnoreValidation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
// ===== ОСНОВНАЯ ФУНКЦИЯ ИНИЦИАЛИЗАЦИИ ВОТЧЕРА =====
|
||||||
|
function initFormWatcher() {
|
||||||
// Находим все submit-кнопки администратора
|
// Находим все submit-кнопки администратора
|
||||||
let submitButtons = document.querySelectorAll('input[type=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 attachWatchersToInputs() {
|
||||||
function handleChange() {
|
let formInputs = document.querySelectorAll('input:not([type=submit]), textarea, select, .codemirror, [contenteditable]');
|
||||||
// При изменении любого поля:
|
|
||||||
// 1. Удаляем класс force-ignore-validation (кнопки вернут нормальный цвет)
|
// Функция которая срабатывает при любом изменении
|
||||||
// 2. Удаляем onclick обработчик
|
function handleChange() {
|
||||||
// 3. Восстанавливаем оригинальный action формы
|
// При изменении любого поля:
|
||||||
submitButtons.forEach(function (btn) {
|
// 1. Удаляем класс force-ignore-validation (кнопки вернут нормальный цвет)
|
||||||
if (btn.classList.contains('force-ignore-validation')) {
|
// 2. Удаляем onclick обработчик
|
||||||
btn.classList.remove('force-ignore-validation');
|
// 3. Восстанавливаем оригинальный action формы
|
||||||
removeOnclickHandler(btn);
|
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
|
attachWatchersToInputs();
|
||||||
// и 'input' для CodeMirror и других редакторов
|
|
||||||
input.addEventListener('change', handleChange);
|
|
||||||
input.addEventListener('input', handleChange);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Мониторим изменение класса force-ignore-validation на кнопках
|
// Мониторим изменение класса force-ignore-validation на кнопках
|
||||||
// Используем MutationObserver для отслеживания добавления/удаления класса
|
// Используем MutationObserver для отслеживания добавления/удаления класса
|
||||||
const observer = new MutationObserver(function (mutations) {
|
const classObserver = new MutationObserver(function (mutations) {
|
||||||
mutations.forEach(function (mutation) {
|
mutations.forEach(function (mutation) {
|
||||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||||
let btn = mutation.target;
|
let btn = mutation.target;
|
||||||
@@ -130,8 +147,67 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Настраиваем observer для отслеживания изменения класса
|
// Настраиваем observer для отслеживания изменения класса на кнопках
|
||||||
submitButtons.forEach(function (btn) {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user