1import { createEmailPreview } from "../util/email_preview.js"; 2 3// Admin form page: TinyMCE init on every locale-specific content textarea. 4// The form has one textarea per locale, each inside a Bootstrap tab-pane. 5// TinyMCE inits fine while hidden but sometimes miscalculates its container 6// dimensions â nudge a re-render when the user reveals a tab. 7 8function initTinyMceOn( textarea ) { 9 if ( !window.tinymce || typeof window.tinymce.init !== "function" ) { 10 return false; 11 } 12 13 window.tinymce.init( { 14 target: textarea, 15 license_key: "gpl", 16 language: window.adminMailingTemplateTinyMceLang || "en", 17 skin: false, 18 content_css: false, 19 branding: false, 20 plugins: "code lists", 21 toolbar: "fontfamily | fontsize | forecolor | bold italic | alignleft aligncenter alignright alignjustify | lineheight | outdent indent | numlist bullist | code", 22 font_size_formats: "8pt 9pt 10pt 11pt 12pt 14pt 18pt 24pt 30pt 36pt 48pt 60pt 72pt 96pt", 23 statusbar: false, 24 resize: false, 25 promotion: false 26 } ); 27 28 return true; 29} 30 31function initAllEditors( attempt ) { 32 const textareas = document.querySelectorAll( "textarea.admin-mt-tinymce" ); 33 if ( !textareas.length ) { 34 return; 35 } 36 37 if ( !window.tinymce || typeof window.tinymce.init !== "function" ) { 38 if ( attempt < 60 ) { 39 window.setTimeout( function() { 40 initAllEditors( attempt + 1 ); 41 }, 150 ); 42 } 43 return; 44 } 45 46 textareas.forEach( function( textarea ) { 47 initTinyMceOn( textarea ); 48 } ); 49} 50 51function bindTabResizeHook() { 52 const tabButtons = document.querySelectorAll( "#adminMailingTemplateLocaleTabs button[data-bs-toggle=\"tab\"]" ); 53 tabButtons.forEach( function( button ) { 54 button.addEventListener( "shown.bs.tab", function() { 55 if ( !window.tinymce || typeof window.tinymce.get !== "function" ) { 56 return; 57 } 58 const target = button.getAttribute( "data-bs-target" ); 59 if ( !target ) { 60 return; 61 } 62 const pane = document.querySelector( target ); 63 if ( !pane ) { 64 return; 65 } 66 pane.querySelectorAll( "textarea.admin-mt-tinymce" ).forEach( function( textarea ) { 67 const editor = window.tinymce.get( textarea.id ); 68 if ( editor && !editor.removed && editor.editorContainer ) { 69 editor.editorContainer.style.display = ""; 70 } 71 } ); 72 } ); 73 } ); 74} 75 76// Reads the subject + content from the locale tab that's currently visible. 77// TinyMCE.save() flushes editor state into the underlying textarea first. 78function readActiveLocaleSlot() { 79 const activeButton = document.querySelector( 80 "#adminMailingTemplateLocaleTabs button.nav-link.active[data-bs-target]" 81 ); 82 if ( !activeButton ) { 83 return null; 84 } 85 86 const paneSelector = activeButton.getAttribute( "data-bs-target" ); 87 const pane = paneSelector ? document.querySelector( paneSelector ) : null; 88 if ( !pane ) { 89 return null; 90 } 91 92 const localeMatch = paneSelector.match( /locale-pane-(.+)$/ ); 93 const locale = localeMatch ? localeMatch[ 1 ] : "en"; 94 const subjectEl = pane.querySelector( "input[name^=\"translations[\"][name$=\"[subject]\"]" ); 95 const contentEl = pane.querySelector( "textarea.admin-mt-tinymce" ); 96 97 let content = ""; 98 if ( contentEl ) { 99 if ( window.tinymce && typeof window.tinymce.get === "function" ) { 100 const editor = window.tinymce.get( contentEl.id ); 101 if ( editor && !editor.removed ) { 102 if ( typeof editor.save === "function" ) { 103 editor.save(); 104 } 105 content = editor.getContent( { format: "html" } ) || ""; 106 } else { 107 content = contentEl.value || ""; 108 } 109 } else { 110 content = contentEl.value || ""; 111 } 112 } 113 114 return { 115 locale: locale, 116 subject: subjectEl ? subjectEl.value : "", 117 content: content 118 }; 119} 120 121const preview = createEmailPreview( { 122 url: window.adminMailingTemplatePreviewUrl, 123 iframeId: "admin-mt-preview-iframe", 124 subjectId: "admin-mt-preview-subject", 125 loadingId: "admin-mt-preview-loading", 126 errorId: "admin-mt-preview-error", 127 errorMessage: window.adminMailingTemplatePreviewErrorMsg, 128 tokenGetter: function() { 129 return window.adminMailingTemplatePreviewToken; 130 }, 131 payload: function() { 132 const slot = readActiveLocaleSlot(); 133 const typeEl = document.getElementById( "admin-mt-type" ); 134 return { 135 locale: slot ? slot.locale : "en", 136 type: typeEl ? typeEl.value : "", 137 subject: slot ? slot.subject : "", 138 content: slot ? slot.content : "" 139 }; 140 } 141} ); 142
143function bindPreviewControls() { 144 const typeEl = document.getElementById( "admin-mt-type" ); 145 if ( typeEl ) { 146 typeEl.addEventListener( "change", preview.refresh ); 147 } 148 149 document.querySelectorAll( "#adminMailingTemplateLocaleTabs button[data-bs-toggle=\"tab\"]" ) 150 .forEach( function( tab ) { 151 tab.addEventListener( "shown.bs.tab", preview.refresh ); 152 } ); 153} 154 155// TinyMCE is initialised asynchronously. Wait for editors to be ready before 156// firing the first preview so the request body includes the loaded content. 157function schedulePreviewAutoLoad() { 158 const textareas = document.querySelectorAll( "textarea.admin-mt-tinymce" ); 159 if ( !textareas.length ) { 160 preview.refresh(); 161 return; 162 } 163 164 let attempts = 0; 165 const timer = window.setInterval( function() { 166 attempts += 1; 167 const allReady = Array.prototype.every.call( textareas, function( textarea ) { 168 if ( !window.tinymce || typeof window.tinymce.get !== "function" ) { 169 return false; 170 } 171 const editor = window.tinymce.get( textarea.id ); 172 return Boolean( editor && editor.initialized && !editor.removed ); 173 } ); 174 175 if ( allReady || attempts >= 60 ) { 176 window.clearInterval( timer ); 177 preview.refresh(); 178 } 179 }, 200 ); 180} 181 182// Validates the top-level `name` and `type` fields plus the per-locale 183// `subject` / `content` pair. Content is stored in a hidden textarea backed by 184// TinyMCE, so we sync editor state into the textarea before reading it and 185// treat the rendered HTML as empty when only whitespace/markup remains. 186// Returns the list of failed fields; callers use the first entry to focus. 187function collectValidationErrors( form ) { 188 const messages = window.adminMailingTemplateValidationMessages || {}; 189 const errors = []; 190 191 const nameEl = form.querySelector( "#admin-mt-name" ); 192 if ( nameEl && nameEl.value.trim() === "" ) { 193 errors.push( { field: nameEl, message: messages.name || "This field is required.", locale: null } ); 194 } 195 196 const typeEl = form.querySelector( "#admin-mt-type" ); 197 if ( typeEl && typeEl.value === "" ) { 198 errors.push( { field: typeEl, message: messages.type || "This field is required.", locale: null } ); 199 } 200 201 const tabButtons = form.querySelectorAll( "#adminMailingTemplateLocaleTabs button[data-bs-target]" ); 202 tabButtons.forEach( function( button ) { 203 const paneSelector = button.getAttribute( "data-bs-target" ); 204 const pane = paneSelector ? form.querySelector( paneSelector ) : null; 205 if ( !pane ) { 206 return; 207 } 208 const localeMatch = paneSelector.match( /locale-pane-(.+)$/ ); 209 const locale = localeMatch ? localeMatch[ 1 ] : ""; 210 211 const subjectEl = pane.querySelector( "input[name^=\"translations[\"][name$=\"[subject]\"]" ); 212 if ( subjectEl && subjectEl.value.trim() === "" ) { 213 errors.push( { field: subjectEl, message: messages.subject || "This field is required.", locale: locale } ); 214 } 215 216 const contentEl = pane.querySelector( "textarea.admin-mt-tinymce" ); 217 if ( contentEl ) { 218 if ( window.tinymce && typeof window.tinymce.get === "function" ) { 219 const editor = window.tinymce.get( contentEl.id ); 220 if ( editor && !editor.removed && typeof editor.save === "function" ) { 221 editor.save(); 222 } 223 } 224 const plain = contentEl.value.replace( /<[^>]*>/g, "" ).replace( / /gi, " " ).trim(); 225 if ( plain === "" ) { 226 errors.push( { field: contentEl, message: messages.content || "This field is required.", locale: locale } ); 227 } 228 } 229 } ); 230 231 return errors; 232} 233 234function clearValidationState( form ) { 235 form.querySelectorAll( ".is-invalid" ).forEach( function( el ) { 236 el.classList.remove( "is-invalid" ); 237 } ); 238 form.querySelectorAll( ".admin-mt-js-feedback" ).forEach( function( el ) { 239 el.remove(); 240 } ); 241 form.querySelectorAll( "#adminMailingTemplateLocaleTabs .admin-mt-js-tab-error" ).forEach( function( icon ) { 242 icon.remove(); 243 } ); 244} 245 246function renderValidationErrors( form, errors ) {
247 errors.forEach( function( err ) { 248 err.field.classList.add( "is-invalid" ); 249 250 const feedback = document.createElement( "div" ); 251 feedback.className = "invalid-feedback d-block admin-mt-js-feedback"; 252 feedback.textContent = err.message; 253 254 // For TinyMCE-backed textareas the visible element is the iframe wrapper 255 // that follows the textarea in the DOM; append after that wrapper so the 256 // message is visible. 257 const parent = err.field.parentNode; 258 if ( parent ) { 259 parent.appendChild( feedback ); 260 } 261 262 if ( err.locale ) { 263 const tabButton = form.querySelector( "#locale-btn-" + err.locale ); 264 if ( tabButton && !tabButton.querySelector( ".admin-mt-js-tab-error" ) ) { 265 const icon = document.createElement( "i" ); 266 icon.className = "fa-solid fa-triangle-exclamation text-danger ms-1 admin-mt-js-tab-error"; 267 tabButton.appendChild( icon ); 268 } 269 } 270 } ); 271} 272 273function focusFirstError( form, first ) { 274 if ( !first ) { 275 return; 276 } 277 278 if ( first.locale ) { 279 const tabButton = form.querySelector( "#locale-btn-" + first.locale ); 280 if ( tabButton && window.bootstrap && window.bootstrap.Tab ) { 281 const tab = window.bootstrap.Tab.getOrCreateInstance( tabButton ); 282 tab.show(); 283 } 284 } 285 286 // Defer focus so the tab switch has completed and TinyMCE can accept focus. 287 window.setTimeout( function() { 288 if ( first.field.classList.contains( "admin-mt-tinymce" ) && 289 window.tinymce && typeof window.tinymce.get === "function" ) { 290 const editor = window.tinymce.get( first.field.id ); 291 if ( editor && !editor.removed && typeof editor.focus === "function" ) { 292 editor.focus(); 293 return; 294 } 295 } 296 if ( typeof first.field.focus === "function" ) { 297 first.field.focus(); 298 } 299 }, 50 ); 300} 301 302function bindFormValidation() { 303 const form = document.getElementById( "admin-mt-form" ); 304 if ( !form ) { 305 return; 306 } 307 308 form.addEventListener( "submit", function( event ) { 309 clearValidationState( form ); 310 const errors = collectValidationErrors( form ); 311 if ( errors.length === 0 ) { 312 return; 313 } 314 event.preventDefault(); 315 renderValidationErrors( form, errors ); 316 focusFirstError( form, errors[ 0 ] ); 317 } ); 318 319 // Clear a field's error state as the user corrects it. 320 form.addEventListener( "input", function( event ) { 321 const target = event.target; 322 if ( target && target.classList && target.classList.contains( "is-invalid" ) ) { 323 target.classList.remove( "is-invalid" ); 324 const sibling = target.parentNode ? target.parentNode.querySelector( ".admin-mt-js-feedback" ) : null; 325 if ( sibling ) { 326 sibling.remove(); 327 } 328 } 329 } ); 330} 331 332document.addEventListener( "DOMContentLoaded", function() { 333 initAllEditors( 0 ); 334 bindTabResizeHook(); 335 bindPreviewControls(); 336 schedulePreviewAutoLoad(); 337 bindFormValidation(); 338} );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.