mod: админка (03) ImageAdmin (03) fin (пропатчено, подключили стили)
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "esbuild src/editor.js --bundle --format=esm --minify --outfile=${CM6_OUTPUT_DIR:-../public/static/codemirror}/editor.js"
|
||||
"build": "esbuild src/editor.js --bundle --format=iife --minify --outfile=${CM6_OUTPUT_DIR:-../public/static/codemirror}/editor.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
@@ -20,4 +20,4 @@
|
||||
"@uiw/codemirror-theme-solarized": "4.25.10",
|
||||
"esbuild": "0.28.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
# Регистрируем модели с удобным интерфейсом.
|
||||
|
||||
from django import forms
|
||||
from django.db import models
|
||||
from django.forms import TextInput, Textarea, URLField
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html, mark_safe
|
||||
from easy_thumbnails.files import get_thumbnailer
|
||||
@@ -25,8 +27,6 @@ class TbImageAdminForm(forms.ModelForm):
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': 'Введите alt-текст для картинки',
|
||||
'class': 'vTextField',
|
||||
'size': 200,
|
||||
}),
|
||||
label='ALT (новый)',
|
||||
help_text='Текст для alt-атрибута картинки <tt><img alt="" .../></tt>.'
|
||||
@@ -37,8 +37,6 @@ class TbImageAdminForm(forms.ModelForm):
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': 'Введите title-описание картинки',
|
||||
'class': 'vTextField',
|
||||
'cols': 120,
|
||||
}),
|
||||
label='TITLE (новый)',
|
||||
help_text='Текст для title-атрибута картинки <tt><img title="" .../></tt>.'
|
||||
@@ -49,8 +47,6 @@ class TbImageAdminForm(forms.ModelForm):
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': 'XXXX, Авторские права на изображение',
|
||||
'class': 'vTextField',
|
||||
'cols': 120,
|
||||
}),
|
||||
label='Copyright',
|
||||
help_text='Авторские права на изображение (например: <tt>2025, Sergei Erjemin</tt>. Будет сохранён в filer_image.author'
|
||||
@@ -64,6 +60,13 @@ class TbImageAdminForm(forms.ModelForm):
|
||||
"""
|
||||
При инициализации формы подгружаем текущие значения alt/caption из filer_image.
|
||||
"""
|
||||
# Атрибуты для активации CodeMirror редактора
|
||||
codemirror_attrs = {
|
||||
'data-codemirror-editor': '1',
|
||||
'data-language': 'text',
|
||||
'data-width': '100%', # Ширина для патча (100% займет полную ширину)
|
||||
}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Если редактируем существующую запись, получаем текущие значения из filer
|
||||
@@ -71,27 +74,70 @@ class TbImageAdminForm(forms.ModelForm):
|
||||
try:
|
||||
filer_image = self.instance.image
|
||||
# Получаем текущие значения из filer и заполняем виртуальные поля
|
||||
# ALT-text
|
||||
self.fields['filer_alt_text'].initial = filer_image.default_alt_text or ''
|
||||
self.fields['filer_alt_text'].widget = Textarea(attrs={
|
||||
'class': 'codemirror-width-m',
|
||||
**codemirror_attrs,
|
||||
})
|
||||
self.fields['filer_caption'].initial = filer_image.default_caption or ''
|
||||
self.fields['filer_caption'].widget = Textarea(attrs={
|
||||
'class': 'codemirror-width-m',
|
||||
**codemirror_attrs,
|
||||
})
|
||||
self.fields['filer_copyright'].initial = filer_image.author or ''
|
||||
self.fields['filer_copyright'].widget = Textarea(attrs={
|
||||
'class': 'codemirror-width-m',
|
||||
**codemirror_attrs,
|
||||
})
|
||||
except Exception:
|
||||
# Если ошибка при получении filer_image, просто оставляем пустые значения
|
||||
pass
|
||||
|
||||
# s_img_src_url - поле URL источника (длинная строка)
|
||||
self.fields['s_img_src_url'].widget = Textarea(attrs={
|
||||
'class': 'codemirror-width-xl',
|
||||
**codemirror_attrs,
|
||||
})
|
||||
|
||||
# i_img_sort - поле сортировки (до четырех цифр)
|
||||
self.fields['i_img_sort'].widget = Textarea(attrs={
|
||||
'class': 'codemirror-width-s codemirror-no-lines',
|
||||
**codemirror_attrs,
|
||||
})
|
||||
|
||||
# f_img_confidence_score - поле confidence score (число с плавающей точкой)
|
||||
self.fields['f_img_confidence_score'].widget = Textarea(attrs={
|
||||
'class': 'codemirror-width-s codemirror-no-lines',
|
||||
**codemirror_attrs,
|
||||
})
|
||||
|
||||
|
||||
class ImageAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Админ для изображений TbImage с поддержкой редактирования метаданных filer_image.
|
||||
|
||||
Позволяет пользователю заполнить default_alt_text и default_caption для картинки в filer
|
||||
прямо в админке TbImage, без необходимости отдельного редактирования фiler.
|
||||
прямо в админке TbImage, без необходимости отдельного редактирования filer.
|
||||
"""
|
||||
form = TbImageAdminForm # Используем кастомную форму с виртуальными полями
|
||||
|
||||
# Подключаем JS через Media (правильный способ!)
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror
|
||||
}
|
||||
js = (
|
||||
'codemirror/editor.js', # Основной CodeMirror
|
||||
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
|
||||
)
|
||||
|
||||
list_display = ('id', 'image_thumbnail', 'image', '_display_filer_alt_text', 'i_img_sort', 't_img_created')
|
||||
list_display_links = ('id', 'image_thumbnail', 'image')
|
||||
list_filter = ('l_img_source', 'l_img_reality', 't_img_created')
|
||||
ordering = ('image', 'i_img_sort')
|
||||
readonly_fields = ('t_img_created', 't_img_updated', '_display_filer_alt_text', '_display_filer_caption')
|
||||
|
||||
fieldsets = (
|
||||
('Изображение', {
|
||||
'fields': ('image', 'l_img_source', 'l_img_reality', 's_img_src_url', 'i_img_sort',
|
||||
@@ -101,8 +147,8 @@ class ImageAdmin(admin.ModelAdmin):
|
||||
('Метаданные filer (SEO для картинок)', {
|
||||
'fields': ('_display_filer_alt_text', '_display_filer_caption', 'filer_alt_text', 'filer_caption',
|
||||
'filer_copyright'),
|
||||
'description': 'Редактируемые поля для заполнения Alt текста и описания в filer. Если не заполнить,'
|
||||
' текущие значения останутся без изменений (или не будут заполнены при создании).',
|
||||
'description': 'Редактируемые поля для заполнения ALT-, TITLE- и ©-текста в filer. Если не заполнить,'
|
||||
' текущие значения останутся без изменений (и не будут заполнены при создании).',
|
||||
# 'classes': ('collapse',),
|
||||
}),
|
||||
('Служебная информация', {
|
||||
|
||||
@@ -314,9 +314,9 @@ class TbImage(models.Model):
|
||||
# Доверие данным (для парсеров и API)
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
default=10.0,
|
||||
verbose_name='Достоверность',
|
||||
help_text='Уверенность (для автоматических данных) 0.0 - 1.0, насколько уверены, что это правильное изображение',
|
||||
help_text='Уверенность (для автоматических данных) 0.0 - 10.0, насколько уверены, что это правильное изображение',
|
||||
)
|
||||
s_img_copyright = models.CharField(
|
||||
# Авторские права и лицензия (по идее -- ненужное поле. Можно в filer использовать `obj.image.author`.
|
||||
|
||||
78
public/static/codemirror/codemirror-patch.js
Normal file
78
public/static/codemirror/codemirror-patch.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* CodeMirror 6 - Height Control Patch
|
||||
*
|
||||
* Этот патч перехватывает создание CodeMirror редакторов и копирует
|
||||
* атрибут data-height с textarea на создаваемый wrapper div.
|
||||
*
|
||||
* Использование в admin.py:
|
||||
* textarea widget: attrs={'data-height': '50px', 'data-codemirror-editor': '1'}
|
||||
*
|
||||
* Загружается ПОСЛЕ editor.js через Media в Admin класс.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Применяем высоту к уже созданным wrapper'ам
|
||||
* (CodeMirror уже загрузился и создал обёртки)
|
||||
*/
|
||||
function applyHeightsToExistingWrappers() {
|
||||
// Находим все textarea'ы с data-codemirror-editor
|
||||
document.querySelectorAll('textarea[data-codemirror-editor]').forEach((textarea) => {
|
||||
// Соседний div (wrapper, созданный CodeMirror)
|
||||
const wrapper = textarea.previousElementSibling;
|
||||
|
||||
// Проверяем что это действительно wrapper CodeMirror
|
||||
if (wrapper && wrapper.classList && wrapper.classList.contains('cm6-editor-wrapper')) {
|
||||
// Копируем data-height на style если указано
|
||||
if (textarea.dataset.height) {
|
||||
wrapper.style.height = textarea.dataset.height;
|
||||
}
|
||||
|
||||
// Копируем data-width на style если указано (для ширины)
|
||||
if (textarea.dataset.width) {
|
||||
wrapper.style.width = textarea.dataset.width;
|
||||
wrapper.style.minWidth = textarea.dataset.width;
|
||||
|
||||
// Также устанавливаем ширину на сам .cm-editor внутри wrapper
|
||||
// Используем !important чтобы переопределить flex: 1 1 auto из CSS
|
||||
const cmEditor = wrapper.querySelector('.cm-editor');
|
||||
// if (cmEditor) {
|
||||
// cmEditor.style.cssText = `width: ${textarea.dataset.width} !important; flex: 0 0 auto;`;
|
||||
// }
|
||||
}
|
||||
|
||||
// Копируем классы с textarea если они есть
|
||||
// (например cm6-editor-wrapper-xs, cm6-editor-wrapper-sm и т.д.)
|
||||
if (textarea.className) {
|
||||
// Добавляем классы из textarea (сохраняя cm6-editor-wrapper)
|
||||
textarea.className.split(' ').forEach((cls) => {
|
||||
if (cls && cls !== 'cm6-editor-wrapper') {
|
||||
wrapper.classList.add(cls);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Применяем высоты когда DOM готов
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', applyHeightsToExistingWrappers);
|
||||
} else {
|
||||
// DOM уже загружен
|
||||
applyHeightsToExistingWrappers();
|
||||
}
|
||||
|
||||
// Также применяем для динамически добавленных элементов (если загружают CodeMirror после инициализации)
|
||||
const observer = new MutationObserver(() => {
|
||||
applyHeightsToExistingWrappers();
|
||||
});
|
||||
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
})();
|
||||
|
||||
58
public/static/codemirror/codemirror-styles.css
Normal file
58
public/static/codemirror/codemirror-styles.css
Normal file
@@ -0,0 +1,58 @@
|
||||
/* CodeMirror 6 - Стили для правильного отображения */
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--brdr: light-dark(#333, #ccc);
|
||||
--accent-mode-color: light-dark(#222, #eee);
|
||||
}
|
||||
|
||||
.cm6-editor-wrapper {
|
||||
/* Основной wrapper, созданный CodeMirror */
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
/* Сам редактор внутри wrapper - растягиваем на всю доступную высоту */
|
||||
flex: 1 1 auto;
|
||||
font-family: monospace;
|
||||
line-height: 1;
|
||||
border: 1px solid var(--brdr);;
|
||||
}
|
||||
|
||||
/*
|
||||
Утилитарные классы для управления шириной.
|
||||
Мы ищем редактор .cm-editor ВНУТРИ элемента с нашим классом.
|
||||
*/
|
||||
|
||||
.codemirror-width-s > .cm-editor {
|
||||
/* Маленький (для чисел, коротких ID) */
|
||||
max-width: 8em !important;
|
||||
}
|
||||
|
||||
.codemirror-width-m .cm-editor {
|
||||
/* Средний (для URL, коротких строк) */
|
||||
max-width: calc(50% - 13em) !important;
|
||||
}
|
||||
|
||||
.codemirror-width-l > .cm-editor {
|
||||
/* Большой (для текста, JSON, HTML) */
|
||||
max-width: calc(75% - 13em) !important;
|
||||
}
|
||||
|
||||
.codemirror-width-xl > .cm-editor {
|
||||
/* Во всю ширину контейнера */
|
||||
max-width: calc(100% - 13em) !important;
|
||||
}
|
||||
|
||||
/* --- Новое правило для скрытия номеров строк --- */
|
||||
/* Если у обертки есть наш класс, находим внутри панель с номерами и скрываем ее */
|
||||
.codemirror-no-lines .cm-gutters {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* Скрыть оригинальный textarea */
|
||||
textarea[data-codemirror-editor] {
|
||||
display: none !important;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user