11import 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 */
160179const 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
183183export default ajaxMarkdownPreview
184+ export { AjaxMarkdownPreview }
0 commit comments