PageSourceSearch

https://www.ausgezeichnet.org/assets/js/staff/mailing_tool_templates-v84wIRs.js

js ausgezeichnet.org collected 2026-10-01 15:10:16 UTC 12,503 bytes, 338 lines download raw bytes

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( /&nbsp;/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.