add: CodeMirror 6 в админку и сборка бандла

This commit is contained in:
2026-04-11 21:15:08 +03:00
parent b4b0fe5ea6
commit a301e75cdd
7 changed files with 178 additions and 50 deletions

View File

@@ -79,7 +79,7 @@ class AdminContentForm(forms.ModelForm):
typograph_strip_soft_hyphens = forms.BooleanField(
label='Удалять переносы',
required=False,
initial=False,
initial=True,
help_text='Убирает `&amp;shy;`, `&amp;#173;` и Unicode-символ мягкого переноса<br />'
'перед типографом.',
)
@@ -104,6 +104,7 @@ class AdminContentForm(forms.ModelForm):
fields = '__all__'
class Media:
js = ('codemirror/editor.js',)
css = {
'all': ('css/admin-select2-theme.css',),
}
@@ -128,6 +129,20 @@ class AdminContentForm(forms.ModelForm):
else:
tag_choices = []
codemirror_attrs = {
'data-codemirror-editor': '1',
'data-language': 'html',
}
self.fields['szContentHead'].widget = Textarea(attrs={
'rows': 4,
'cols': 120,
**codemirror_attrs,
})
for field_name in ('szContentHead', 'szContentIntro', 'szContentBody'):
self.fields[field_name].widget.attrs.update(codemirror_attrs)
self.fields['tags'].widget = AjaxCommaSeparatedSelect2TagWidget(
attrs={
'data-ajax--url': reverse('web_tag_autocomplete'),

View File

@@ -1,6 +1,7 @@
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.forms import Textarea
from django.test import SimpleTestCase, TestCase
from django.urls import reverse
from etpgrf.config import MODE_UNICODE, SANITIZE_ETPGRF
@@ -67,6 +68,22 @@ class AdminTypographFormTests(SimpleTestCase):
self.assertTrue(form.fields['typograph_hyphenation'].initial)
self.assertEqual(form.fields['typograph_sanitizer'].initial, 'None')
def test_admin_form_adds_codemirror_attrs_and_media(self):
form = AdminContentForm()
for field_name in ('szContentHead', 'szContentIntro', 'szContentBody'):
self.assertIsInstance(form.fields[field_name].widget, Textarea)
self.assertEqual(
form.fields[field_name].widget.attrs.get('data-codemirror-editor'),
'1',
)
self.assertEqual(
form.fields[field_name].widget.attrs.get('data-language'),
'html',
)
self.assertIn('codemirror/editor.js', str(form.media))
def test_tbcontent_model_has_no_btypograf_field(self):
self.assertNotIn('bTypograf', [field.name for field in TbContent._meta.fields])

View File

@@ -53,11 +53,13 @@
временной директории только на время сборки.
Внутри этого файла живёт минимальная инициализация CodeMirror 6:
- монтирование на `textarea[data-codemirror-editor]`;
- HTML-режим;
- JavaScript-режим;
- CSS-режим;
- тема `oneDark`;
- перенос строк.
- перенос строк;
- синхронизация значения редактора обратно в скрытую textarea.
Если позже захочется менять поведение редактора, логика правится либо в шаблоне,
который выдаёт этот исходник, либо прямо в сборочном скрипте.

View File

@@ -45,19 +45,59 @@ 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 { EditorState } from '@codemirror/state';
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { html } from '@codemirror/lang-html';
import { javascript } from '@codemirror/lang-javascript';
import { css } from '@codemirror/lang-css';
import { oneDark } from '@codemirror/theme-one-dark';
import { solarizedDark, solarizedLight } from '@uiw/codemirror-theme-solarized';
import { lineNumbers } from '@codemirror/view';
const editorHost = document.querySelector('[data-codemirror-editor]');
const themeCompartment = new Compartment();
if (editorHost) {
const language = editorHost.dataset.language || 'html';
const doc = editorHost.textContent ?? '';
const extensions = [EditorView.lineWrapping, oneDark];
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 || 'html';
const initialDoc = textarea.value ?? '';
const wrapper = document.createElement('div');
wrapper.className = 'cm6-editor-wrapper';
textarea.insertAdjacentElement('beforebegin', wrapper);
textarea.hidden = true;
const syncTextarea = EditorView.updateListener.of((update) => {
if (update.docChanged) {
textarea.value = update.state.doc.toString();
}
});
const extensions = [
lineNumbers(),
EditorView.lineWrapping,
syntaxHighlighting(defaultHighlightStyle),
syncTextarea,
themeCompartment.of(isDarkTheme() ? solarizedDark : solarizedLight),
];
if (language === 'javascript') {
extensions.unshift(javascript());
@@ -68,14 +108,34 @@ if (editorHost) {
}
const state = EditorState.create({
doc,
doc: initialDoc,
extensions,
});
new EditorView({
const view = new EditorView({
state,
parent: editorHost,
parent: wrapper,
});
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));
textarea.value = view.state.doc.toString();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCodeMirrorEditors, { once: true });
} else {
initCodeMirrorEditors();
}
EOF

View File

@@ -1,11 +1,14 @@
{
"name": "cadpoint-codemirror6",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cadpoint-codemirror6",
"version": "0.1.0",
"devDependencies": {
"@babel/runtime": "7.29.2",
"@codemirror/autocomplete": "6.20.1",
"@codemirror/commands": "6.10.3",
"@codemirror/lang-css": "6.3.1",
@@ -13,11 +16,21 @@
"@codemirror/lang-javascript": "6.2.5",
"@codemirror/language": "6.12.3",
"@codemirror/state": "6.6.0",
"@codemirror/theme-one-dark": "6.1.3",
"@codemirror/view": "6.41.0",
"@uiw/codemirror-theme-solarized": "4.25.9",
"esbuild": "0.28.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.20.1",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz",
@@ -129,19 +142,6 @@
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.41.0",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.0.tgz",
@@ -667,6 +667,39 @@
"dev": true,
"license": "MIT"
},
"node_modules/@uiw/codemirror-theme-solarized": {
"version": "4.25.9",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-solarized/-/codemirror-theme-solarized-4.25.9.tgz",
"integrity": "sha512-axUgU9+3JKXW83F+te454qcyTmQAm0+2Fxv0yoegiH6bdl7DjFq/lNVGGZtLwN47AQCj2Qwrheeet2t3GbY9VQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@uiw/codemirror-themes": "4.25.9"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
}
},
"node_modules/@uiw/codemirror-themes": {
"version": "4.25.9",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.9.tgz",
"integrity": "sha512-DAHKb/L9ELwjY4nCf/MP/mIllHOn4GQe7RR4x8AMJuNeh9nGRRoo1uPxrxMmUL/bKqe6kDmDbIZ2AlhlqyIJuw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
},
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
},
"peerDependencies": {
"@codemirror/language": ">=6.0.0",
"@codemirror/state": ">=6.0.0",
"@codemirror/view": ">=6.0.0"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",

View File

@@ -8,6 +8,7 @@
"build": "esbuild src/editor.js --bundle --format=esm --minify --outfile=${CM6_OUTPUT_DIR:-../public/static/codemirror}/editor.js"
},
"devDependencies": {
"@babel/runtime": "7.29.2",
"@codemirror/autocomplete": "6.20.1",
"@codemirror/commands": "6.10.3",
"@codemirror/lang-css": "6.3.1",
@@ -15,8 +16,8 @@
"@codemirror/lang-javascript": "6.2.5",
"@codemirror/language": "6.12.3",
"@codemirror/state": "6.6.0",
"@codemirror/theme-one-dark": "6.1.3",
"@codemirror/view": "6.41.0",
"@uiw/codemirror-theme-solarized": "4.25.9",
"esbuild": "0.28.0"
}
}

File diff suppressed because one or more lines are too long