add: CodeMirror 6 в админку и сборка бандла
This commit is contained in:
@@ -79,7 +79,7 @@ class AdminContentForm(forms.ModelForm):
|
|||||||
typograph_strip_soft_hyphens = forms.BooleanField(
|
typograph_strip_soft_hyphens = forms.BooleanField(
|
||||||
label='Удалять переносы',
|
label='Удалять переносы',
|
||||||
required=False,
|
required=False,
|
||||||
initial=False,
|
initial=True,
|
||||||
help_text='Убирает `&shy;`, `&#173;` и Unicode-символ мягкого переноса<br />'
|
help_text='Убирает `&shy;`, `&#173;` и Unicode-символ мягкого переноса<br />'
|
||||||
'перед типографом.',
|
'перед типографом.',
|
||||||
)
|
)
|
||||||
@@ -104,6 +104,7 @@ class AdminContentForm(forms.ModelForm):
|
|||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
|
js = ('codemirror/editor.js',)
|
||||||
css = {
|
css = {
|
||||||
'all': ('css/admin-select2-theme.css',),
|
'all': ('css/admin-select2-theme.css',),
|
||||||
}
|
}
|
||||||
@@ -128,6 +129,20 @@ class AdminContentForm(forms.ModelForm):
|
|||||||
else:
|
else:
|
||||||
tag_choices = []
|
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(
|
self.fields['tags'].widget = AjaxCommaSeparatedSelect2TagWidget(
|
||||||
attrs={
|
attrs={
|
||||||
'data-ajax--url': reverse('web_tag_autocomplete'),
|
'data-ajax--url': reverse('web_tag_autocomplete'),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.forms import Textarea
|
||||||
from django.test import SimpleTestCase, TestCase
|
from django.test import SimpleTestCase, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from etpgrf.config import MODE_UNICODE, SANITIZE_ETPGRF
|
from etpgrf.config import MODE_UNICODE, SANITIZE_ETPGRF
|
||||||
@@ -67,6 +68,22 @@ class AdminTypographFormTests(SimpleTestCase):
|
|||||||
self.assertTrue(form.fields['typograph_hyphenation'].initial)
|
self.assertTrue(form.fields['typograph_hyphenation'].initial)
|
||||||
self.assertEqual(form.fields['typograph_sanitizer'].initial, 'None')
|
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):
|
def test_tbcontent_model_has_no_btypograf_field(self):
|
||||||
self.assertNotIn('bTypograf', [field.name for field in TbContent._meta.fields])
|
self.assertNotIn('bTypograf', [field.name for field in TbContent._meta.fields])
|
||||||
|
|
||||||
|
|||||||
@@ -53,11 +53,13 @@
|
|||||||
временной директории только на время сборки.
|
временной директории только на время сборки.
|
||||||
|
|
||||||
Внутри этого файла живёт минимальная инициализация CodeMirror 6:
|
Внутри этого файла живёт минимальная инициализация CodeMirror 6:
|
||||||
|
- монтирование на `textarea[data-codemirror-editor]`;
|
||||||
- HTML-режим;
|
- HTML-режим;
|
||||||
- JavaScript-режим;
|
- JavaScript-режим;
|
||||||
- CSS-режим;
|
- CSS-режим;
|
||||||
- тема `oneDark`;
|
- тема `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/"
|
cp "$ROOT_DIR/package.json" "$ROOT_DIR/package-lock.json" "$WORK_DIR/"
|
||||||
|
|
||||||
cat > "$WORK_DIR/src/editor.js" <<'EOF'
|
cat > "$WORK_DIR/src/editor.js" <<'EOF'
|
||||||
import { EditorState } from '@codemirror/state';
|
import { Compartment, EditorState } from '@codemirror/state';
|
||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||||
import { html } from '@codemirror/lang-html';
|
import { html } from '@codemirror/lang-html';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { css } from '@codemirror/lang-css';
|
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) {
|
function isDarkTheme() {
|
||||||
const language = editorHost.dataset.language || 'html';
|
const rootTheme = document.documentElement.dataset.theme;
|
||||||
const doc = editorHost.textContent ?? '';
|
|
||||||
const extensions = [EditorView.lineWrapping, oneDark];
|
|
||||||
|
|
||||||
if (language === 'javascript') {
|
if (rootTheme === 'dark') {
|
||||||
extensions.unshift(javascript());
|
return true;
|
||||||
} else if (language === 'css') {
|
|
||||||
extensions.unshift(css());
|
|
||||||
} else {
|
|
||||||
extensions.unshift(html());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = EditorState.create({
|
if (rootTheme === 'light') {
|
||||||
doc,
|
return false;
|
||||||
extensions,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
new EditorView({
|
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
state,
|
}
|
||||||
parent: editorHost,
|
|
||||||
|
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
|
EOF
|
||||||
|
|
||||||
log "СОБИРАЮ CodeMirror 6 ДЛЯ ФРОНТЕНДА АДМИНКИ ПРОЕКТА"
|
log "СОБИРАЮ CodeMirror 6 ДЛЯ ФРОНТЕНДА АДМИНКИ ПРОЕКТА"
|
||||||
|
|||||||
61
frontend-assembly/package-lock.json
generated
61
frontend-assembly/package-lock.json
generated
@@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "cadpoint-codemirror6",
|
"name": "cadpoint-codemirror6",
|
||||||
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cadpoint-codemirror6",
|
"name": "cadpoint-codemirror6",
|
||||||
|
"version": "0.1.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/runtime": "7.29.2",
|
||||||
"@codemirror/autocomplete": "6.20.1",
|
"@codemirror/autocomplete": "6.20.1",
|
||||||
"@codemirror/commands": "6.10.3",
|
"@codemirror/commands": "6.10.3",
|
||||||
"@codemirror/lang-css": "6.3.1",
|
"@codemirror/lang-css": "6.3.1",
|
||||||
@@ -13,11 +16,21 @@
|
|||||||
"@codemirror/lang-javascript": "6.2.5",
|
"@codemirror/lang-javascript": "6.2.5",
|
||||||
"@codemirror/language": "6.12.3",
|
"@codemirror/language": "6.12.3",
|
||||||
"@codemirror/state": "6.6.0",
|
"@codemirror/state": "6.6.0",
|
||||||
"@codemirror/theme-one-dark": "6.1.3",
|
|
||||||
"@codemirror/view": "6.41.0",
|
"@codemirror/view": "6.41.0",
|
||||||
|
"@uiw/codemirror-theme-solarized": "4.25.9",
|
||||||
"esbuild": "0.28.0"
|
"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": {
|
"node_modules/@codemirror/autocomplete": {
|
||||||
"version": "6.20.1",
|
"version": "6.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz",
|
||||||
@@ -129,19 +142,6 @@
|
|||||||
"@marijn/find-cluster-break": "^1.0.0"
|
"@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": {
|
"node_modules/@codemirror/view": {
|
||||||
"version": "6.41.0",
|
"version": "6.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.0.tgz",
|
||||||
@@ -667,6 +667,39 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/crelt": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
"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"
|
"build": "esbuild src/editor.js --bundle --format=esm --minify --outfile=${CM6_OUTPUT_DIR:-../public/static/codemirror}/editor.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/runtime": "7.29.2",
|
||||||
"@codemirror/autocomplete": "6.20.1",
|
"@codemirror/autocomplete": "6.20.1",
|
||||||
"@codemirror/commands": "6.10.3",
|
"@codemirror/commands": "6.10.3",
|
||||||
"@codemirror/lang-css": "6.3.1",
|
"@codemirror/lang-css": "6.3.1",
|
||||||
@@ -15,8 +16,8 @@
|
|||||||
"@codemirror/lang-javascript": "6.2.5",
|
"@codemirror/lang-javascript": "6.2.5",
|
||||||
"@codemirror/language": "6.12.3",
|
"@codemirror/language": "6.12.3",
|
||||||
"@codemirror/state": "6.6.0",
|
"@codemirror/state": "6.6.0",
|
||||||
"@codemirror/theme-one-dark": "6.1.3",
|
|
||||||
"@codemirror/view": "6.41.0",
|
"@codemirror/view": "6.41.0",
|
||||||
|
"@uiw/codemirror-theme-solarized": "4.25.9",
|
||||||
"esbuild": "0.28.0"
|
"esbuild": "0.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user