Skip to content

Commit b67fc5e

Browse files
committed
Replace CodeFlask code editor with CodeMirror on the UI.
Unfortunately, the tiny CodeFlask lib stopped getting updates 5+ years ago and is buggy. The attempt to replace it with CodeInput (which itself seems to have several open bugs) failed. So, biting the bullet with CodeMirror, which is a robust, battle tested, albeit large (~300KB) code editor library. This patch replaces all code editor UIs (Campaign, Templates, Settings) with CodeMirror.
1 parent 53d7929 commit b67fc5e

File tree

14 files changed

+836
-202
lines changed

14 files changed

+836
-202
lines changed

frontend/cypress/e2e/campaigns.cy.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,11 +271,19 @@ describe('Campaigns', () => {
271271
});
272272
cy.wait(500);
273273
} else if (c === 'html') {
274-
cy.get('code-flask').shadow().find('.codeflask textarea').invoke('val', htmlBody)
275-
.trigger('input');
274+
cy.get('[contenteditable="true"]').then(($el) => {
275+
cy.window().then((win) => {
276+
$el.focus();
277+
win.document.execCommand('insertText', false, htmlBody);
278+
});
279+
});
276280
} else if (c === 'markdown') {
277-
cy.get('code-flask').shadow().find('.codeflask textarea').invoke('val', markdownBody)
278-
.trigger('input');
281+
cy.get('[contenteditable="true"]').then(($el) => {
282+
cy.window().then((win) => {
283+
$el.focus();
284+
win.document.execCommand('insertText', false, markdownBody);
285+
});
286+
});
279287
} else if (c === 'plain') {
280288
cy.get('textarea[name=content]').invoke('val', plainBody).trigger('input');
281289
} else if (c === 'visual') {

frontend/package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,23 @@
1010
"prebuild": "eslint --ext .js,.vue --ignore-path .gitignore src"
1111
},
1212
"dependencies": {
13+
"@codemirror/commands": "^6.8.1",
14+
"@codemirror/lang-css": "^6.3.1",
15+
"@codemirror/lang-html": "^6.4.9",
16+
"@codemirror/lang-javascript": "^6.2.3",
17+
"@codemirror/lang-markdown": "^6.3.2",
18+
"@codemirror/language": "^6.11.0",
19+
"@codemirror/language-data": "^6.5.1",
20+
"@codemirror/search": "^6.5.10",
21+
"@codemirror/state": "^6.5.2",
22+
"@codemirror/view": "^6.36.5",
23+
"@lezer/highlight": "^1.2.1",
1324
"@tinymce/tinymce-vue": "^3",
1425
"axios": "^1.8.2",
1526
"buefy": "^0.9.25",
1627
"bulma": "^0.9.4",
1728
"chart.js": "^4.4.1",
18-
"codeflask": "^1.4.1",
29+
"codemirror": "^6.0.0",
1930
"dayjs": "^1.11.10",
2031
"indent.js": "^0.3.5",
2132
"js-beautify": "^1.15.1",

frontend/src/assets/style.scss

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -296,13 +296,28 @@ body.is-noscroll {
296296
}
297297

298298
/* WYSIWYG / HTML code editor */
299-
.html-editor, .plain-editor textarea, .markdown-editor {
299+
.plain-editor textarea, .code-editor {
300300
position: relative;
301301
width: 100%;
302302
min-height: 250px;
303303
height: 65vh;
304304
border: 1px solid $grey-lighter;
305305
border-radius: 2px;
306+
307+
.cm-editor {
308+
height: 100%;
309+
}
310+
}
311+
312+
.code-editor {
313+
.cm-lineNumbers .cm-gutterElement{
314+
color: #aaa;
315+
font-size: 0.875rem;
316+
padding-right: 10px;
317+
}
318+
.cm-gutters {
319+
margin-right: 10px;
320+
}
306321
}
307322

308323
.alt-body textarea {
@@ -977,8 +992,8 @@ section.analytics {
977992
.box {
978993
margin-bottom: 30px;
979994
}
980-
.html-editor {
981-
height: auto;
995+
.code-editor {
996+
height: 20vh;
982997
min-height: 350px;
983998
}
984999
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<template>
2+
<div ref="editor" class="code-editor" />
3+
</template>
4+
5+
<script>
6+
import { EditorState } from '@codemirror/state';
7+
import {
8+
EditorView, keymap, highlightActiveLine, lineNumbers, highlightActiveLineGutter,
9+
} from '@codemirror/view';
10+
import { markdown } from '@codemirror/lang-markdown';
11+
import { javascript } from '@codemirror/lang-javascript';
12+
import { css } from '@codemirror/lang-css';
13+
import { html } from '@codemirror/lang-html';
14+
import {
15+
defaultKeymap, history, historyKeymap, indentWithTab,
16+
} from '@codemirror/commands';
17+
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
18+
import { search, searchKeymap, highlightSelectionMatches } from '@codemirror/search';
19+
import { vsCodeLight } from './editor-theme';
20+
21+
export default {
22+
props: {
23+
value: { type: String, default: '' },
24+
lang: { type: String, default: 'html' },
25+
disabled: Boolean,
26+
},
27+
28+
data() {
29+
return {
30+
data: '',
31+
editor: null,
32+
internalUpdate: false,
33+
};
34+
},
35+
36+
methods: {
37+
},
38+
39+
mounted() {
40+
const onUpdate = EditorView.updateListener.of((update) => {
41+
if (update.docChanged) {
42+
this.internalUpdate = true;
43+
this.$emit('input', update.state.doc.toString());
44+
}
45+
});
46+
47+
// Set the chosen language.
48+
let langs = [];
49+
switch (this.lang) {
50+
case 'html':
51+
langs = [html()];
52+
break;
53+
case 'css':
54+
langs = [css()];
55+
break;
56+
case 'javascript':
57+
langs = [javascript()];
58+
break;
59+
case 'markdown':
60+
langs = [markdown()];
61+
break;
62+
default:
63+
langs = [html()];
64+
}
65+
66+
// Prepare the full config.
67+
const stateCfg = EditorState.create({
68+
// Initial value.
69+
doc: this.value,
70+
71+
extensions: [
72+
EditorView.baseTheme({}),
73+
...langs,
74+
history(),
75+
highlightActiveLine(),
76+
bracketMatching(),
77+
highlightSelectionMatches(),
78+
lineNumbers(),
79+
highlightActiveLineGutter(),
80+
keymap.of([...defaultKeymap, ...historyKeymap, ...searchKeymap, indentWithTab]),
81+
82+
// Readonly?
83+
EditorState.readOnly.of(this.disabled),
84+
EditorView.editable.of(!this.disabled),
85+
86+
// Syntax highlighting and theme.
87+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
88+
EditorView.lineWrapping,
89+
90+
vsCodeLight,
91+
92+
search({
93+
top: true, // Places the search panel at the top of the editor
94+
}),
95+
96+
// On content change.
97+
onUpdate,
98+
],
99+
});
100+
101+
// Create the editor.
102+
this.editor = new EditorView({
103+
state: stateCfg,
104+
parent: this.$refs.editor,
105+
});
106+
107+
this.$nextTick(() => {
108+
window.setTimeout(() => {
109+
this.editor.focus();
110+
}, 100);
111+
});
112+
},
113+
114+
beforeDestroy() {
115+
if (this.editor) {
116+
this.editor.destroy();
117+
}
118+
},
119+
120+
watch: {
121+
value(val) {
122+
if (!this.internalUpdate) {
123+
this.editor.dispatch({
124+
changes: { from: 0, to: this.editor.state.doc.length, insert: val },
125+
});
126+
this.internalUpdate = false;
127+
}
128+
},
129+
},
130+
};
131+
</script>

frontend/src/components/Editor.vue

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@
6565
height="65vh" ref="visualEditor" />
6666

6767
<!-- raw html editor //-->
68-
<html-editor v-if="self.contentType === 'html'" v-model="self.body" />
68+
<code-editor lang="html" v-if="self.contentType === 'html'" v-model="self.body" key="editor-html" />
6969

7070
<!-- markdown editor //-->
71-
<markdown-editor v-if="self.contentType === 'markdown'" v-model="self.body" />
71+
<code-editor lang="markdown" v-if="self.contentType === 'markdown'" v-model="self.body" key="editor-markdown" />
7272

7373
<!-- plain text //-->
7474
<b-input v-if="self.contentType === 'plain'" v-model="self.body" type="textarea" name="content" ref="plainEditor"
@@ -86,19 +86,17 @@ import TurndownService from 'turndown';
8686
import { mapState } from 'vuex';
8787
8888
import CampaignPreview from './CampaignPreview.vue';
89-
import HTMLEditor from './HTMLEditor.vue';
90-
import MarkdownEditor from './MarkdownEditor.vue';
9189
import VisualEditor from './VisualEditor.vue';
9290
import RichtextEditor from './RichtextEditor.vue';
9391
import markdownToVisualBlock from './editor';
92+
import CodeEditor from './CodeEditor.vue';
9493
9594
const turndown = new TurndownService();
9695
9796
export default {
9897
components: {
9998
CampaignPreview,
100-
'html-editor': HTMLEditor,
101-
'markdown-editor': MarkdownEditor,
99+
'code-editor': CodeEditor,
102100
'visual-editor': VisualEditor,
103101
'richtext-editor': RichtextEditor,
104102
},

frontend/src/components/HTMLEditor.vue

Lines changed: 0 additions & 78 deletions
This file was deleted.

0 commit comments

Comments
 (0)