minor: сборщик CodeMirror и доки поменяли свои каталоги (подготовка к NPM для TailWindCSS) + в доки и скрипты внесены соответствующие правки.
This commit is contained in:
@@ -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`.
|
||||
@@ -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",
|
||||
Reference in New Issue
Block a user