add: CodeMirror 6 в админку и сборка бандла
This commit is contained in:
@@ -79,7 +79,7 @@ class AdminContentForm(forms.ModelForm):
|
||||
typograph_strip_soft_hyphens = forms.BooleanField(
|
||||
label='Удалять переносы',
|
||||
required=False,
|
||||
initial=False,
|
||||
initial=True,
|
||||
help_text='Убирает `&shy;`, `&#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'),
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -53,11 +53,13 @@
|
||||
временной директории только на время сборки.
|
||||
|
||||
Внутри этого файла живёт минимальная инициализация CodeMirror 6:
|
||||
- монтирование на `textarea[data-codemirror-editor]`;
|
||||
- HTML-режим;
|
||||
- JavaScript-режим;
|
||||
- CSS-режим;
|
||||
- тема `oneDark`;
|
||||
- перенос строк.
|
||||
- перенос строк;
|
||||
- синхронизация значения редактора обратно в скрытую textarea.
|
||||
|
||||
Если позже захочется менять поведение редактора, логика правится либо в шаблоне,
|
||||
который выдаёт этот исходник, либо прямо в сборочном скрипте.
|
||||
|
||||
@@ -45,38 +45,98 @@ 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 (language === 'javascript') {
|
||||
extensions.unshift(javascript());
|
||||
} else if (language === 'css') {
|
||||
extensions.unshift(css());
|
||||
} else {
|
||||
extensions.unshift(html());
|
||||
if (rootTheme === 'dark') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const state = EditorState.create({
|
||||
doc,
|
||||
extensions,
|
||||
});
|
||||
if (rootTheme === 'light') {
|
||||
return false;
|
||||
}
|
||||
|
||||
new EditorView({
|
||||
state,
|
||||
parent: editorHost,
|
||||
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());
|
||||
} else if (language === 'css') {
|
||||
extensions.unshift(css());
|
||||
} else {
|
||||
extensions.unshift(html());
|
||||
}
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: initialDoc,
|
||||
extensions,
|
||||
});
|
||||
|
||||
const view = new EditorView({
|
||||
state,
|
||||
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
|
||||
|
||||
log "СОБИРАЮ CodeMirror 6 ДЛЯ ФРОНТЕНДА АДМИНКИ ПРОЕКТА"
|
||||
|
||||
61
frontend-assembly/package-lock.json
generated
61
frontend-assembly/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user