Skip to content

Commit 398723b

Browse files
committed
Update JavaScript for MarkdownEditor to support multiple instances
1 parent 406ee6b commit 398723b

File tree

2 files changed

+273
-149
lines changed

2 files changed

+273
-149
lines changed
Lines changed: 150 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -1,153 +1,171 @@
11
import debounce from '../utils/debounce'
22

3-
const store = {
4-
target: null,
5-
source: null,
6-
authenticityToken: null,
7-
csrfToken: null,
8-
liveRegion: null,
9-
i18n: null,
10-
errorArea: null
11-
}
3+
class AjaxMarkdownPreview {
4+
constructor (target, source, endpoint, i18n) {
5+
this.target = target
6+
this.source = source
7+
this.endpoint = endpoint
8+
this.i18n = i18n
9+
this.liveRegion = null
10+
this.errorArea = null
11+
this.authenticityToken = document.querySelector(
12+
'input[name="authenticity_token"]'
13+
)?.value
14+
this.csrfToken = document
15+
.querySelector('meta[name="csrf-token"]')
16+
?.getAttribute('content')
17+
18+
// Create a debounced version of triggerAjaxMarkdownPreview bound to this instance
19+
this.debouncedAjaxMarkdownPreview = debounce(() => {
20+
this.triggerAjaxMarkdownPreview()
21+
}, 1000)
22+
23+
this.init()
24+
}
1225

13-
const setLoadingStatus = () => {
14-
store.liveRegion.setAttribute('aria-busy', 'true')
15-
store.target.innerHTML = `<p>${store.i18n.preview_loading}</p>`
16-
}
26+
init () {
27+
this.addLiveRegion()
28+
this.createErrorArea()
1729

18-
const setFailureStatus = () => {
19-
store.target.innerHTML = `<p>${store.i18n.preview_error}</p>`
20-
const retryButton = document.createElement('button')
21-
retryButton.classList.add('govuk-button', 'govuk-button--secondary')
22-
retryButton.innerHTML = 'Retry preview'
23-
addEventListeners(retryButton, manuallyTriggerMarkdownPreview)
24-
store.target.appendChild(retryButton)
25-
}
30+
// run on page load
31+
this.setLoadingStatus()
32+
this.triggerAjaxMarkdownPreview()
2633

27-
const triggerAjaxMarkdownPreview = async () => {
28-
try {
29-
if (store.endpoint) {
30-
const response = await window.fetch(store.endpoint, {
31-
method: 'POST',
32-
mode: 'same-origin',
33-
cache: 'no-cache',
34-
credentials: 'same-origin',
35-
headers: {
36-
'Content-Type': 'application/json',
37-
'X-CSRF-Token': store.csrfToken
38-
},
39-
redirect: 'follow',
40-
referrerPolicy: 'same-origin',
41-
body: JSON.stringify({
42-
markdown: store.source.value,
43-
authenticity_token: store.authenticityToken
44-
})
45-
})
46-
47-
// insert the preview into the DOM
48-
const json = await response.json()
49-
store.target.innerHTML = json.preview_html
50-
if (json.errors.length > 0) {
51-
addErrorToField(json.errors[0])
52-
addErrorClass()
53-
} else {
54-
clearErrorsFromField()
55-
removeErrorClass()
56-
}
57-
addNotification('Preview updated.')
58-
} else {
59-
throw new Error('No endpoint set')
60-
}
61-
} catch {
62-
setFailureStatus()
63-
addNotification(store.i18n.preview_error)
34+
// run when the user types
35+
this.source.addEventListener('input', () => this.inputEventListener())
6436
}
65-
}
6637

67-
const manuallyTriggerMarkdownPreview = event => {
68-
event?.preventDefault()
38+
setLoadingStatus () {
39+
this.liveRegion.setAttribute('aria-busy', 'true')
40+
this.target.innerHTML = `<p>${this.i18n.preview_loading}</p>`
41+
}
6942

70-
triggerAjaxMarkdownPreview()
71-
}
43+
setFailureStatus () {
44+
this.target.innerHTML = `<p>${this.i18n.preview_error}</p>`
45+
const retryButton = document.createElement('button')
46+
retryButton.classList.add('govuk-button', 'govuk-button--secondary')
47+
retryButton.innerHTML = 'Retry preview'
48+
retryButton.addEventListener('click', event => {
49+
this.manuallyTriggerMarkdownPreview(event)
50+
})
51+
this.target.appendChild(retryButton)
52+
}
7253

73-
const addEventListeners = (trigger, callback) => {
74-
trigger.addEventListener('click', callback)
75-
}
54+
async triggerAjaxMarkdownPreview () {
55+
try {
56+
if (this.endpoint) {
57+
const response = await window.fetch(this.endpoint, {
58+
method: 'POST',
59+
mode: 'same-origin',
60+
cache: 'no-cache',
61+
credentials: 'same-origin',
62+
headers: {
63+
'Content-Type': 'application/json',
64+
'X-CSRF-Token': this.csrfToken
65+
},
66+
redirect: 'follow',
67+
referrerPolicy: 'same-origin',
68+
body: JSON.stringify({
69+
markdown: this.source.value,
70+
authenticity_token: this.authenticityToken
71+
})
72+
})
7673

77-
// debounce the AJAX request so we don't hammer the server with one request per keystroke
78-
const debouncedAjaxMarkdownPreview = debounce(() => {
79-
triggerAjaxMarkdownPreview()
80-
}, 1000)
74+
// insert the preview into the DOM
75+
const json = await response.json()
76+
this.target.innerHTML = json.preview_html
77+
if (json.errors.length > 0) {
78+
this.addErrorToField(json.errors[0])
79+
this.addErrorClass()
80+
} else {
81+
this.clearErrorsFromField()
82+
this.removeErrorClass()
83+
}
84+
this.addNotification('Preview updated.')
85+
} else {
86+
throw new Error('No endpoint set')
87+
}
88+
} catch {
89+
this.setFailureStatus()
90+
this.addNotification(this.i18n.preview_error)
91+
}
92+
}
8193

82-
const inputEventListener = () => {
83-
setLoadingStatus()
84-
return debouncedAjaxMarkdownPreview()
85-
}
94+
manuallyTriggerMarkdownPreview (event) {
95+
event?.preventDefault()
96+
this.triggerAjaxMarkdownPreview()
97+
}
8698

87-
const addLiveRegion = () => {
88-
const liveRegion = document.createElement('div')
89-
liveRegion.setAttribute('role', 'status')
90-
liveRegion.classList.add('app-markdown-editor__notification-area')
91-
store.liveRegion = liveRegion
92-
store.source.after(liveRegion)
93-
}
99+
inputEventListener () {
100+
this.setLoadingStatus()
101+
return this.debouncedAjaxMarkdownPreview()
102+
}
94103

95-
const addNotification = text => {
96-
store.liveRegion.setAttribute('aria-busy', 'false')
97-
store.liveRegion.innerHTML = text
98-
setTimeout(() => {
99-
store.liveRegion.innerHTML = ''
100-
}, 5000)
101-
}
104+
addLiveRegion () {
105+
const liveRegion = document.createElement('div')
106+
liveRegion.setAttribute('role', 'status')
107+
liveRegion.classList.add('app-markdown-editor__notification-area')
108+
this.liveRegion = liveRegion
109+
this.source.after(liveRegion)
110+
}
102111

103-
const createErrorArea = () => {
104-
// Use existing error area if there's a server side error present on the field
105-
store.errorArea =
106-
store.source
107-
.closest('.govuk-form-group')
108-
?.querySelector('.govuk-error-message') ?? document.createElement('p')
109-
store.errorArea.classList.add(
110-
'govuk-error-message',
111-
'app-markdown-editor__error-message'
112-
)
113-
store.source.closest('.govuk-form-group').prepend(store.errorArea)
114-
setAriaAttributesForError()
115-
}
112+
addNotification (text) {
113+
this.liveRegion.setAttribute('aria-busy', 'false')
114+
this.liveRegion.innerHTML = text
115+
setTimeout(() => {
116+
this.liveRegion.innerHTML = ''
117+
}, 5000)
118+
}
116119

117-
const setAriaAttributesForError = () => {
118-
if (!store.errorArea.getAttribute('id')) {
119-
const id = `${store.source.getAttribute('id')}-error`
120-
store.errorArea.setAttribute('id', id)
121-
store.source.setAttribute(
122-
'aria-describedby',
123-
`${id} ${store.source.getAttribute('aria-describedby')}`
120+
createErrorArea () {
121+
// Use existing error area if there's a server side error present on the field
122+
this.errorArea =
123+
this.source
124+
.closest('.govuk-form-group')
125+
?.querySelector('.govuk-error-message') ?? document.createElement('p')
126+
this.errorArea.classList.add(
127+
'govuk-error-message',
128+
'app-markdown-editor__error-message'
124129
)
130+
this.source.closest('.govuk-form-group').prepend(this.errorArea)
131+
this.setAriaAttributesForError()
125132
}
126-
store.errorArea.setAttribute('aria-live', 'polite')
127-
}
128133

129-
const addErrorToField = error => {
130-
if (!store.errorArea) createErrorArea()
131-
store.errorArea.innerHTML = `<span class="govuk-visually-hidden">Error:</span> ${error}`
132-
}
134+
setAriaAttributesForError () {
135+
if (!this.errorArea.getAttribute('id')) {
136+
const id = `${this.source.getAttribute('id')}-error`
137+
this.errorArea.setAttribute('id', id)
138+
this.source.setAttribute(
139+
'aria-describedby',
140+
`${id} ${this.source.getAttribute('aria-describedby')}`
141+
)
142+
}
143+
this.errorArea.setAttribute('aria-live', 'polite')
144+
}
133145

134-
const clearErrorsFromField = () => {
135-
if (!store.errorArea) createErrorArea()
136-
store.errorArea.innerHTML = ''
137-
}
146+
addErrorToField (error) {
147+
if (!this.errorArea) this.createErrorArea()
148+
this.errorArea.innerHTML = `<span class="govuk-visually-hidden">Error:</span> ${error}`
149+
}
138150

139-
const addErrorClass = () => {
140-
store.source
141-
.closest('.govuk-form-group')
142-
?.classList.add('govuk-form-group--error')
143-
store.source.classList.add('govuk-textarea--error')
144-
}
151+
clearErrorsFromField () {
152+
if (!this.errorArea) this.createErrorArea()
153+
this.errorArea.innerHTML = ''
154+
}
145155

146-
const removeErrorClass = () => {
147-
store.source
148-
.closest('.govuk-form-group--error')
149-
?.classList.remove('govuk-form-group--error')
150-
store.source.classList.remove('govuk-textarea--error')
156+
addErrorClass () {
157+
this.source
158+
.closest('.govuk-form-group')
159+
?.classList.add('govuk-form-group--error')
160+
this.source.classList.add('govuk-textarea--error')
161+
}
162+
163+
removeErrorClass () {
164+
this.source
165+
.closest('.govuk-form-group--error')
166+
?.classList.remove('govuk-form-group--error')
167+
this.source.classList.remove('govuk-textarea--error')
168+
}
151169
}
152170

153171
/**
@@ -156,28 +174,11 @@ const removeErrorClass = () => {
156174
* @param {HTMLElement} source - The element which contains the raw markdown for conversion.
157175
* @param {string} endpoint - The URL for the endpoint that renders the markdown.
158176
* @param {Object} i18n - An object containing translations for the component.
177+
* @returns {AjaxMarkdownPreview} The instance of the AjaxMarkdownPreview class.
159178
*/
160179
const ajaxMarkdownPreview = (target, source, endpoint, i18n) => {
161-
store.target = target
162-
store.source = source
163-
store.endpoint = endpoint
164-
store.i18n = i18n
165-
store.authenticityToken = document.querySelector(
166-
'input[name="authenticity_token"]'
167-
)?.value
168-
store.csrfToken = document
169-
.querySelector('meta[name="csrf-token"]')
170-
?.getAttribute('content')
171-
172-
addLiveRegion()
173-
createErrorArea()
174-
175-
// run on page load
176-
setLoadingStatus()
177-
triggerAjaxMarkdownPreview()
178-
179-
// run when the user types
180-
source.addEventListener('input', inputEventListener)
180+
return new AjaxMarkdownPreview(target, source, endpoint, i18n)
181181
}
182182

183183
export default ajaxMarkdownPreview
184+
export { AjaxMarkdownPreview }

0 commit comments

Comments
 (0)