minor: сборщик CodeMirror и доки поменяли свои каталоги (подготовка к NPM для TailWindCSS) + в доки и скрипты внесены соответствующие правки.

This commit is contained in:
2026-07-17 19:17:04 +03:00
parent ef7a37fa6a
commit 5b911007de
4 changed files with 42 additions and 49 deletions
@@ -1,152 +0,0 @@
# CodeMirror 6 для Django Admin на LPON.RU
## Обзор
Это руководство описывает, как использовать и обновлять сборку CodeMirror 6, которая используется для редактирования текстовых полей в админке Django.
## Структура проекта
- `frontend-assembly/`: Исходные файлы и скрипты для сборки.
- `package.json`: Список зависимостей CodeMirror 6.
- `package-lock.json`: Файл с зафиксированными версиями зависимостей. **Не редактировать вручную!**
- `build-codemirror6.sh`: Скрипт, который генерирует и собирает финальный JS-бандл.
- `public/static/codemirror/`: Каталог с готовыми файлами для Django.
- `editor.js`: Финальный, минифицированный JS-бандл. **Не редактировать вручную!**
- `codemirror-styles.css`: Кастомные стили для интеграции с админкой.
- `codemirror-patch.js`: JS-патч для управления размерами редактора.
## Как собрать бандл
После клонирования репозитория или обновления зависимостей, выполните:
```bash
# Перейдите в каталог сборки
cd frontend-assembly
# Установите зависимости и соберите бандл
bash ./build-codemirror6.sh
```
## Как использовать в Django Admin
### 1. Подключение в ModelAdmin
В вашем `admin.py` используйте `Media` класс для подключения необходимых файлов.
```python
# frontend/admin.py
class YourModelAdmin(admin.ModelAdmin):
class Media:
css = {
'all': ('codemirror/codemirror-styles.css',)
}
js = (
'codemirror/editor.js',
'codemirror/codemirror-patch.js',
)
```
### 2. Активация для полей
Чтобы превратить стандартный `Textarea` в редактор CodeMirror, используйте кастомную форму или метод `formfield_for_dbfield` и добавьте к виджету специальные `data-` атрибуты.
```python
# frontend/admin.py
class YourModelAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, request, **kwargs):
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
# Активируем CodeMirror для всех текстовых полей
if isinstance(formfield.widget, forms.Textarea):
formfield.widget.attrs['data-codemirror-editor'] = 'true'
# Устанавливаем язык в зависимости от имени поля
if 'j_' in db_field.name:
# Обычно в моделях LPON поля с префиксом 'j_' содержат JSON
formfield.widget.attrs['data-language'] = 'json'
elif 'html' in db_field.name:
formfield.widget.attrs['data-language'] = 'html'
else:
formfield.widget.attrs['data-language'] = 'text' # Обычный текст
# Добавляем утилитарные CSS-классы для стилизации
if db_field.name == 'i_img_sort':
formfield.widget.attrs['class'] = 'codemirror-width-s codemirror-no-lines'
elif db_field.name == 'j_seller_metadata':
formfield.widget.attrs['class'] = 'codemirror-width-xl codemirror-min-height-m'
return formfield
```
### 3. Поддерживаемые языки
- `javascript`
- `css`
- `json`
- `html` (по умолчанию, в него встроенны режимы подсветки синтенза HTML, XML, CSS и JavaScript)
- `text` (обычный текст, без подсветки синтакса, unocode-режим)
## Кастомизация
### Управление размерами и стилями
В файле `public/static/codemirror/codemirror-styles.css` определены утилитарные CSS-классы для управления внешним
видом редакторов:
- **Ширина**: `.codemirror-width-s`, `.codemirror-width-m`, `.codemirror-width-l`, `.codemirror-width-xl`
- **Минимальная высота**: `.codemirror-min-height-5` и `.codemirror-min-height-10` (по 5 и 10 строк соответственно)
- **Отключение номеров строк**: `.codemirror-no-lines`
Просто примените нужный класс к атрибуту `class` виджета в `admin.py`. По аналогии можно создавать свои классы
для своих настройек.
## Как добавить новый язык (например, jinja2 или xml)
1. **Добавить зависимость**: Откройте `frontend-assembly/package.json` и добавьте в `devDependencies` новый языковой пакет. Версию выбирайте близкую к другим пакетам `@codemirror/lang-*`.
```json
// package.json
...
"@codemirror/lang-json": "6.0.1",
"@codemirror/lang-xml": "6.1.0", // <-- Новая строка для добавления xml
"@codemirror/language": "6.12.3",
...
```
2. **Обновить скрипт сборки**: Откройте `frontend-assembly/build-codemirror6.sh` и внесите два изменения в секцию `cat > "$WORK_DIR/src/editor.js" <<'EOF'`:
- Добавьте импорт:
```javascript
import { xml } from '@codemirror/lang-xml';
```
- Добавьте условие для его использования:
```javascript
} else if (language === 'xml') {
extensions.unshift(xml());
}
```
3. **Пересобрать бандл**:
```bash
cd frontend-assembly
npm install # Установит новый пакет и обновит package-lock.json
bash build-codemirror6.sh # Пересоберет editor.js с поддержкой XML
```
4. **Использовать в админке**: Теперь вы можете использовать `data-language="xml"` для любого поля.
## Обновление зависимостей
1. **Проверить устаревшие пакеты**:
```bash
cd frontend-assembly
npm outdated
```
2. **Обновить версии**: Аккуратно обновите номера версий в `package.json`.
3. **Установить обновления и пересобрать**:
```bash
npm install
bash build-codemirror6.sh
```
4. **Закоммитить изменения**: Добавьте в коммит обновленные `package.json`, `package-lock.json` и `public/static/codemirror/editor.js`.
-183
View File
@@ -1,183 +0,0 @@
#!/usr/bin/env bash
# Скрипт создаёт временную рабочую папку, ставит зависимости через `npm ci`, собирает
# минимизированный бандл и затем сам удаляет временные `src/` и `node_modules/`.
# В проекте остаётся только готовая статика:
# * `public/static/codemirror/editor.js`
#
# Запуск:
# bash ./frontend-assembly/build-codemirror6.sh
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUTPUT_DIR="$ROOT_DIR/../public/static/codemirror"
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/codemirror6.XXXXXX")"
log() {
printf '[codemirror6] %s\n' "$*"
}
fail() {
printf '[codemirror6] %s\n' "$*" >&2
exit 1
}
cleanup() {
rm -rf "$WORK_DIR"
rm -rf "$ROOT_DIR/src" "$ROOT_DIR/node_modules"
}
trap cleanup EXIT INT TERM
if ! command -v npm >/dev/null 2>&1; then
fail 'Не найден `npm`. Установи Node.js и повтори сборку.'
fi
if [[ ! -f "$ROOT_DIR/package.json" ]]; then
fail "Не найден package.json: $ROOT_DIR/package.json"
fi
if [[ ! -f "$ROOT_DIR/package-lock.json" ]]; then
fail "Не найден package-lock.json: $ROOT_DIR/package-lock.json"
fi
mkdir -p "$WORK_DIR/src" "$OUTPUT_DIR"
cp "$ROOT_DIR/package.json" "$ROOT_DIR/package-lock.json" "$WORK_DIR/"
cat > "$WORK_DIR/src/editor.js" <<'EOF'
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView, lineNumbers, placeholder } from '@codemirror/view';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { html } from '@codemirror/lang-html';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { css } from '@codemirror/lang-css';
import { solarizedDark, solarizedLight } from '@uiw/codemirror-theme-solarized';
const themeCompartment = new Compartment();
const processedForms = new Set();
function isDarkTheme() {
const rootTheme = document.documentElement.dataset.theme;
if (rootTheme === 'dark') return true;
if (rootTheme === 'light') return false;
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
function reconfigureTheme(view) {
view.dispatch({
effects: themeCompartment.reconfigure(isDarkTheme() ? solarizedDark : solarizedLight),
});
}
function initCodeMirrorEditors() {
document.querySelectorAll('textarea[data-codemirror-editor]').forEach((textarea) => {
const language = textarea.dataset.language || 'text';
const inputMode = textarea.dataset.inputMode || 'default';
let initialDoc = textarea.value ?? '';
const wrapper = document.createElement('div');
wrapper.className = 'cm6-editor-wrapper';
textarea.insertAdjacentElement('beforebegin', wrapper);
if (language === 'json') {
try {
const parsed = JSON.parse(initialDoc);
initialDoc = JSON.stringify(parsed, null, 2);
} catch (e) {
console.warn("CodeMirror: Initial content is not valid JSON, displaying as is.", e);
}
}
const syncTextarea = EditorView.updateListener.of((update) => {
if (update.docChanged) {
textarea.value = update.state.doc.toString();
}
});
// --- Собираем расширения в правильном порядке ---
const extensions = [
syntaxHighlighting(defaultHighlightStyle),
syncTextarea,
themeCompartment.of(isDarkTheme() ? solarizedDark : solarizedLight),
];
// Язык
if (language === 'javascript') extensions.push(javascript());
else if (language === 'css') extensions.push(css());
else if (language === 'json') extensions.push(json());
else if (language === 'html') extensions.push(html());
// Режим ввода
if (inputMode === 'url') {
extensions.push(placeholder('https://example.com/path'));
// Для URL не добавляем lineWrapping и lineNumbers
} else {
extensions.push(EditorView.lineWrapping);
if (!textarea.classList.contains('codemirror-no-lines')) {
extensions.push(lineNumbers());
}
}
const state = EditorState.create({
doc: initialDoc,
extensions,
});
const view = new EditorView({
state,
parent: wrapper,
});
textarea.cmView = view;
reconfigureTheme(view);
const observer = new MutationObserver(() => reconfigureTheme(view));
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme', 'class'],
});
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
colorScheme.addEventListener('change', () => reconfigureTheme(view));
const form = textarea.closest('form');
if (form && !processedForms.has(form)) {
form.addEventListener('submit', () => {
form.querySelectorAll('textarea[data-language="json"]').forEach(jsonTextarea => {
if (jsonTextarea.cmView) {
const prettyJson = jsonTextarea.cmView.state.doc.toString();
try {
const parsed = JSON.parse(prettyJson);
const minifiedJson = JSON.stringify(parsed);
jsonTextarea.value = minifiedJson;
} catch (e) {
console.warn("CodeMirror: Could not minify invalid JSON before submit.", e);
}
}
});
});
processedForms.add(form);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCodeMirrorEditors, { once: true });
} else {
initCodeMirrorEditors();
}
EOF
log "СОБИРАЮ CodeMirror 6 ДЛЯ ФРОНТЕНДА АДМИНКИ ПРОЕКТА"
log "Временная рабочая папка: $WORK_DIR"
cd "$WORK_DIR"
log 'Устанавливаю зависимости через npm ci'
npm ci
log 'Собираю CodeMirror 6'
export CM6_OUTPUT_DIR="$OUTPUT_DIR"
npm run build
log 'ГОТОВО'
@@ -5,7 +5,7 @@
"private": true,
"type": "module",
"scripts": {
"build": "esbuild src/editor.js --bundle --format=iife --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",