From 1d5306a48ecf4f547e8bf59e51a82c24f9290420 Mon Sep 17 00:00:00 2001 From: erjemin Date: Mon, 24 Aug 2026 17:51:36 +0300 Subject: [PATCH] =?UTF-8?q?add:=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B0=20(12)=20?= =?UTF-8?q?=D0=BC=D0=B8=D0=BA=D1=81=D0=B8=D0=BD=D1=8B,=20=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BB=D0=B8=20=D0=B8=20=D0=BF=D0=B0=D1=82=D1=87=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20CodeMirror=20=D0=B2=20=D0=B0=D0=B4=D0=BC=D0=B8?= =?UTF-8?q?=D0=BD=D0=BA=D0=B5...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hypn0/hypn0_site/admin.py | 108 +++++++++++++++++- .../static/codemirror/codemirror-styles.css | 78 +++++++++++++ public/static/codemirror/odemirror-patch.js | 77 +++++++++++++ 3 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 public/static/codemirror/codemirror-styles.css create mode 100644 public/static/codemirror/odemirror-patch.js diff --git a/hypn0/hypn0_site/admin.py b/hypn0/hypn0_site/admin.py index 3450d5a..df21cdf 100644 --- a/hypn0/hypn0_site/admin.py +++ b/hypn0/hypn0_site/admin.py @@ -1,6 +1,112 @@ +# Кастомная конфигурация Django Admin для Hypn0. +# Регистрируем модели с удобным интерфейсом. + +from django import forms from django.contrib import admin +from django.forms import Textarea from django.utils.html import format_html -from .models import TbHypn0Item, TbVote, TbBlogPost +from .models import ( + TbHypn0Item, + TbVote, + TbBlogPost +) + +# ============================================================================ +# МИКСИНЫ ДЛЯ АДМИНКИ +# ============================================================================ +class RequestInFormMixin(admin.ModelAdmin): + """ + Миксин для передачи request объекта в форму. + + Используется когда форма нуждается в доступе к request для проверки POST параметров + или другой информации о текущем HTTP-запросе. + + Переопределяет get_form() и передает request в __init__ формы через kwargs. + """ + + def get_form(self, request, obj=None, **kwargs): + """ + Переопределяем get_form чтобы передать request в форму. + Создаем оборачивающий класс который передаст request в __init__. + """ + FormClass = super().get_form(request, obj, **kwargs) + + # Сохраняем request в замыкании для доступа в классе + request_ref = request + + class FormWithRequest(FormClass): + """Оборачивающий класс, который передает request при инстанцировании""" + def __init__(form_instance, *args, **init_kwargs): + # Добавляем request в kwargs перед вызовом __init__ родителя + init_kwargs['request'] = request_ref + super().__init__(*args, **init_kwargs) + + return FormWithRequest + + +class CodeMirrorFormMixin(forms.ModelForm): + """ + Миксин для форм с поддержкой CodeMirror редактора. + + Предоставляет: + - Готовый Media класс с CSS и JS для CodeMirror + - Helper метод setup_codemirror_field() для конфигурации полей + - Базовые атрибуты для активации CodeMirror + + Использование: + class MyForm(CodeMirrorFormMixin): + class Meta: + model = MyModel + fields = (...) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setup_codemirror_field('field_name', 'json', 'codemirror-width-l') + """ + + # ===== MEDIA КЛАСС ДЛЯ CODEMIRROR ===== + class Media: + """Подключаем CSS и JS для CodeMirror редактора""" + css = { + 'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror + } + js = ( + 'codemirror/editor.js', # Основной CodeMirror + 'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной + ) + + # ===== БАЗОВЫЕ АТРИБУТЫ CODEMIRROR ===== + CODEMIRROR_ATTRS_BASE = { + 'data-codemirror-editor': '1', + 'data-width': '100%', # Ширина для патча (100% займет полную ширину) + } + + def setup_codemirror_field(self, field_name: str, language: str = 'text', css_class: str = 'codemirror-width-l'): + """ + Конфигурирует поле для использования CodeMirror редактором. + + Применяет Textarea виджет с нужными атрибутами и CSS классами для CodeMirror. + + Args: + field_name: Имя поля в форме (например, 's_label') + language: Язык для подсветки синтаксиса (text, json, html, url и т.д.) + css_class: CSS класс для управления размерами (codemirror-width-s, codemirror-width-l и т.д.) + + Пример: + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setup_codemirror_field('s_label', 'text', 'codemirror-width-xl codemirror-no-lines') + self.setup_codemirror_field('j_metadata', 'json', 'codemirror-width-l') + """ + # Собираем атрибуты для поля + attrs = { + **self.CODEMIRROR_ATTRS_BASE, + 'data-language': language, + 'class': css_class, + } + + # Применяем Textarea виджет с атрибутами + self.fields[field_name].widget = Textarea(attrs=attrs) @admin.register(TbHypn0Item) diff --git a/public/static/codemirror/codemirror-styles.css b/public/static/codemirror/codemirror-styles.css new file mode 100644 index 0000000..4cdd3fc --- /dev/null +++ b/public/static/codemirror/codemirror-styles.css @@ -0,0 +1,78 @@ +/* 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; /* Вычитание 13em подобрано. Это ширина бокового меню слева */ +} + +.codemirror-width-l > .cm-editor { + /* Большой (для текста, JSON, HTML) */ + max-width: calc(75% - 13em) !important; +} + +.codemirror-width-xl > .cm-editor { + /* Во всю ширину контейнера */ + max-width: calc(100% - 13em) !important; +} + +/* Ширина редактора зависит от видимости бокового меню Django Admin. + На экранах < 767px меню прячется, поэтому вычитание 13em неактуально. + Подобрано значение 3ex. */ +@media (max-width: 767px) { + .codemirror-width-m > .cm-editor, + .codemirror-width-l > .cm-editor, + .codemirror-width-xl > .cm-editor { + max-width: calc(100% - 3ex) !important; + } +} + +/* --- Новый класс для минимальной высоты --- */ +.codemirror-min-height-2 .cm-editor { + min-height: 3.5em; /* 5 строк */ +} +.codemirror-min-height-5 .cm-editor { + min-height: 8em; /* 5 строк */ +} +.codemirror-min-height-10 .cm-editor { + min-height: 16em; /* 10 строк */ +} + +.codemirror-no-lines .cm-gutters { + /* Скрываем нумерацию строк */ + display: none !important; +} + +/* Скрыть оригинальный textarea */ +textarea[data-codemirror-editor] { + display: none !important; +} diff --git a/public/static/codemirror/odemirror-patch.js b/public/static/codemirror/odemirror-patch.js new file mode 100644 index 0000000..ebca6f5 --- /dev/null +++ b/public/static/codemirror/odemirror-patch.js @@ -0,0 +1,77 @@ +/** + * 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, + }); +})();