Skip to content

Commit bba5626

Browse files
AlexBTurchynomohokcoj
authored andcommitted
add ability to edit html email
1 parent bdf5e58 commit bba5626

14 files changed

Lines changed: 814 additions & 88 deletions

app/javascript/application.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import RequiredCheckboxGroup from './elements/required_checkbox_group'
4242
import PageContainer from './elements/page_container'
4343
import EmailEditor from './elements/email_editor'
4444
import MarkdownEditor from './elements/markdown_editor'
45+
import HtmlEditor from './elements/html_editor'
4546
import MountOnClick from './elements/mount_on_click'
4647
import RemoveOnEvent from './elements/remove_on_event'
4748
import ScrollTo from './elements/scroll_to'
@@ -135,6 +136,7 @@ safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
135136
safeRegisterElement('page-container', PageContainer)
136137
safeRegisterElement('email-editor', EmailEditor)
137138
safeRegisterElement('markdown-editor', MarkdownEditor)
139+
safeRegisterElement('html-editor', HtmlEditor)
138140
safeRegisterElement('mount-on-click', MountOnClick)
139141
safeRegisterElement('remove-on-event', RemoveOnEvent)
140142
safeRegisterElement('scroll-to', ScrollTo)

app/javascript/elements/email_editor.js

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ function loadCodeMirror () {
99
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
1010
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
1111
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
12+
import(/* webpackChunkName: "email-editor" */ '@codemirror/lint'),
1213
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
13-
]).then(([view, commands, language, html, htmlflow]) => {
14+
]).then(([view, commands, language, html, lint, htmlflow]) => {
1415
return {
1516
minimalSetup: [
1617
commands.history(),
@@ -19,6 +20,8 @@ function loadCodeMirror () {
1920
],
2021
EditorView: view.EditorView,
2122
html: html.html,
23+
htmlLanguage: html.htmlLanguage,
24+
linter: lint.linter,
2225
htmlflow: htmlflow.default || htmlflow
2326
}
2427
})
@@ -46,6 +49,70 @@ export default targetable(class extends HTMLElement {
4649

4750
this.previewViewTab.addEventListener('click', this.showPreviewView)
4851
this.codeViewTab.addEventListener('click', this.showCodeView)
52+
53+
this.form = this.closest('form')
54+
this.form?.addEventListener('submit', this.validateOnSubmit)
55+
}
56+
57+
disconnectedCallback () {
58+
this.form?.removeEventListener('submit', this.validateOnSubmit)
59+
}
60+
61+
validateOnSubmit = (e) => {
62+
if (!this.htmlLanguage) return
63+
64+
const bodyType = this.form.querySelector('input[name$="[body_type]"]:checked')?.value
65+
66+
if (bodyType && bodyType !== 'html') return
67+
68+
const diagnostics = this.buildDiagnostics(this.input.value)
69+
70+
if (diagnostics.length === 0) return
71+
72+
e.preventDefault()
73+
74+
this.showCodeView()
75+
76+
const pos = Math.min(diagnostics[0].from, this.editorView.state.doc.length)
77+
78+
this.editorView.dispatch({ selection: { anchor: pos }, scrollIntoView: true })
79+
this.editorView.focus()
80+
81+
alert(diagnostics[0].message)
82+
}
83+
84+
buildDiagnostics (value) {
85+
const diagnostics = []
86+
87+
if (!value.trim()) return diagnostics
88+
89+
if (!/^\s*(<!doctype[^>]*>\s*)?<html/i.test(value)) {
90+
diagnostics.push({
91+
from: 0,
92+
to: Math.min(5, value.length),
93+
severity: 'error',
94+
message: 'The email template must start with the <html> tag'
95+
})
96+
}
97+
98+
const seen = new Set()
99+
100+
this.htmlLanguage.parser.parse(value).iterate({
101+
enter: (node) => {
102+
if (!node.type.isError || seen.has(node.from) || seen.size >= 20) return
103+
104+
seen.add(node.from)
105+
106+
diagnostics.push({
107+
from: node.from,
108+
to: Math.min(node.to + 1, value.length),
109+
severity: 'error',
110+
message: 'The email template contains invalid HTML'
111+
})
112+
}
113+
})
114+
115+
return diagnostics
49116
}
50117

51118
showCodeView = () => {
@@ -76,7 +143,9 @@ export default targetable(class extends HTMLElement {
76143
this.input = this.querySelector('input[type="hidden"]')
77144
this.input.style.display = 'none'
78145

79-
const { EditorView, minimalSetup, html, htmlflow } = await loadCodeMirror()
146+
const { EditorView, minimalSetup, html, htmlLanguage, linter, htmlflow } = await loadCodeMirror()
147+
148+
this.htmlLanguage = htmlLanguage
80149

81150
this.editorView = new EditorView({
82151
doc: this.input.value,
@@ -85,8 +154,11 @@ export default targetable(class extends HTMLElement {
85154
html(),
86155
minimalSetup,
87156
EditorView.lineWrapping,
157+
linter((view) => this.buildDiagnostics(view.state.doc.toString()), { delay: 600 }),
88158
EditorView.updateListener.of(update => {
89-
if (update.docChanged) this.input.value = update.state.doc.toString()
159+
if (update.docChanged) {
160+
this.input.value = update.state.doc.toString()
161+
}
90162
}),
91163
EditorView.theme({
92164
'&': {

0 commit comments

Comments
 (0)