PageSourceSearch

https://www.parker.edu/wp-content/plugins/parker-salesforce-integr…/appconfig.js?ver=1790564791

js parker.edu collected 2026-10-01 12:06:03 UTC 29,298 bytes, 587 lines download raw bytes

1/**
2 * App Modal — step 1 (program/term picker) logic.
3 *
4 * Hoisted out of a DOMContentLoaded handler so it can be invoked on demand. Step 1's DOM is
5 * NOT torn down when swapping to step 2 — its nodes are detached into an in-memory
6 * DocumentFragment (parkedStep1Nodes) and reattached on Back. This preserves all listeners,
7 * dropdown selections, and DOM state natively. The earlier approach of caching innerHTML and
8 * re-binding from scratch had too many timing-sensitive edge cases (when does `.loaded`
9 * class get added, when do listeners attach vs. when do we re-dispatch change, etc.).
10 */
11
12// DocumentFragment holding step 1's detached children while step 2 is shown. Null when step 1
13// is currently in the DOM. Re-creating per round-trip is fine — the actual node references
14// (with their listeners and state) are what we care about preserving.
15var parkedStep1Nodes = null;
16
17function initParkerAppmodalStep1() {
18    parkerDebugLog("App Modal - AppConfig loaded");
19
20    // CRITICAL: all queries below MUST be scoped to inside the modal element. Otherwise on
21    // the /application page (which has [parker_application] in page content AND
22    // [parker_appmodal] in the footer via wp_footer site-wide hook), document.querySelector
23    // returns the standalone page's form elements (which appear first in DOM order). That
24    // makes the modal's submit handler hijack the page's submit button — with preventDefault
25    // and stopImmediatePropagation — and the standalone form never gets to call
26    // handleAppConfigSection() to redirect to /application?record_id=…&program_id=…&
27    // program_term=…. Step 2 never loads. User sees "still on step 1".
28    const modalEl = document.getElementById('parker-appmodal-modal');
29    if (!modalEl) {
30        parkerDebugLog("App Modal: no modal element on page, skipping appmodal init");
31        return;
32    }
33
34    const urlParams = new URLSearchParams(window.location.search);
35    const useUnmanagedData = urlParams.get('use_unmanaged_data') === '1';
36    parkerDebugLog('🔍 DATA SOURCE:', useUnmanagedData ? 'SALESFORCE (Direct API)' : 'DATABASE (Managed)');
37
38    const programsDropdown = modalEl.querySelector('#TargetX_SRMb__Program__c');
39    const programTermsDropdown = modalEl.querySelector('#custom_program_term');
40    const submitButton = modalEl.querySelector('#submit-btn');
41
42    if (!programsDropdown || !programTermsDropdown || !submitButton) {
43        parkerDebugWarn("App modal form elements not found");
44        return;
45    }
46
47    submitButton.textContent = 'Next';
48    // Start disabled. The updateStep1ButtonState() helper below enables it only when
49    // BOTH a program AND a term are picked. All queries here are already scoped to
50    // modalEl (the #parker-appmodal-modal root) so this never touches anything outside
51    // the modal — important because the modal lives in the footer on every site page
52    // and we previously had bugs from broad submit-button selectors.
53    submitButton.disabled = true;
54
55    // Form reveal (clear FOUC, dismiss the #section-loader overlay, activate the first section)
56    // is handled by window.initParkerApplicationForm(), called from the modal's openModal()
57    // orchestration right after this form is injected. Kept out of here so reveal lives in one
58    // place for every lazy-load path. Defensive: if that helper wasn't available for some
59    // reason, fall back to revealing step 1 here so the modal never shows a dead spinner.
60    if (typeof window.initParkerApplicationForm !== 'function') {
61        const firstSection = modalEl.querySelector('.form-section');
62        if (firstSection) firstSection.classList.add('active');
63        const sectionLoaderEl = modalEl.querySelector('#section-loader');
64        if (sectionLoaderEl) sectionLoaderEl.classList.add('hidden');
65    }
66
67    const programTermsFormGroup = programTermsDropdown.closest('.form-group');
68
69    if (programTermsFormGroup) {
70        programTermsFormGroup.style.display = 'none';
71    }
72
73    // Capture step-1's program data NOW, while window.* still holds it. When the user
74    // advances to step 2, ApplicationShortcode's embedded render emits inline scripts that
75    // OVERWRITE window.programLegend / window.availableProgramTerms /
76    // window.jsonDecodedAvailableProgramTerms with just the ONE chosen program's data.
77    // After clicking "Back to program selection" the step-1 DOM is restored (parked nodes)
78    // but these globals are gone — so the submit handler's label->SF-id lookup and the
79    // term-repopulation must use these captured copies, not the live (clobbered) globals.
80    // Captured here because the injected step-1 inline script (which sets these) runs just
81    // before this init function is called.
82    const step1ProgramLegend = window.programLegend || window.jsonDecodedprogramLegend || {};
83    const step1ProgramTerms  = window.jsonDecodedAvailableProgramTerms || window.availableProgramTerms || {};
84
85    // Toggle submit button based on whether both selects have values. Called from the
86    // change listeners on both dropdowns AND from inside the term-population branch
87    // below (after we fill the term dropdown, the selected value defaults to empty,
88    // so the button correctly stays disabled until the user picks one).
89    const updateStep1ButtonState = function () {
90        const hasProgram = programsDropdown.value && programsDropdown.value.trim() !== '';
91        const hasTerm    = programTermsDropdown.value && programTermsDropdown.value.trim() !== '';
92        submitButton.disabled = !(hasProgram && hasTerm);
93    };
94    programTermsDropdown.addEventListener('change', updateStep1ButtonState);
95
96    let debounceTimer;
97    programsDropdown.addEventListener("change", function () {
98        clearTimeout(debounceTimer);
99        debounceTimer = setTimeout(() => {
100            const selectedProgramProcess = programsDropdown.value;
101
102            parkerDebugLog("=== PROGRAM SELECTED ===");
103            parkerDebugLog("Selected Program Value:", selectedProgramProcess);
104
105            if (programTermsFormGroup) {
106                programTermsFormGroup.style.display = selectedProgramProcess ? 'block' : 'none';
107            }
108
109            if (!selectedProgramProcess) {
110                return;
111            }
112
113            // Use the captured step-1 terms (see step1ProgramTerms above) — window.* may have
114            // been clobbered by a step-2 render the user has since backed out of.
115            const terms = step1ProgramTerms?.[selectedProgramProcess];
116            parkerDebugLog("Terms for this program:", terms);
117
118            if (!terms || terms.length === 0) {
119                parkerDebugWarn("No terms available for this program.");
120                programTermsDropdown.innerHTML = '<option value="">No terms available</option>';
121                return;
122            }
123
124            const activeTerms = getActiveTerms(terms);
125            if (!activeTerms || activeTerms.length === 0) {
126                parkerDebugWarn("No active terms available.");
127                programTermsDropdown.innerHTML = '<option value="">No active terms available</option>';
128                return;
129            }
130
131            const sortedTerms = sortTermsChronologically(activeTerms);
132
133            if (typeof FieldHandler !== 'undefined' && FieldHandler.populateDropdown) {
134                FieldHandler.populateDropdown(programTermsDropdown, sortedTerms, 'Select a term');
135            } else {
136                programTermsDropdown.innerHTML = '<option value="">Select a term</option>';
137                sortedTerms.forEach(term => {
138                    const option = document.createElement('option');
139                    option.value = term.name;
140                    option.textContent = term.name;
141                    programTermsDropdown.appendChild(option);
142                });
143            }
144
145            // Term dropdown was just repopulated; its value is the empty placeholder.
146            // Force the button to reflect that — without this, the button could remain
147            // enabled from a previous program's term selection that no longer applies.
148            updateStep1ButtonState();
149        }, 300);
150    });
151
152    submitButton.addEventListener("click", function (e) {
153        e.preventDefault();
154        e.stopPropagation();
155        e.stopImmediatePropagation();
156
157        const selectedProgramProcess = programsDropdown.value;
158        const selectedProgramTerm = programTermsDropdown.value;
159
160        parkerDebugLog('=== APP MODAL SUBMIT CLICKED ===');
161        parkerDebugLog('Selected Program Process:', selectedProgramProcess);
162        parkerDebugLog('Selected Term:', selectedProgramTerm);
163
164        if (!selectedProgramProcess || !selectedProgramTerm) {
165            showInlineError('Please select both a program and a term.');
166            return;
167        }
168
169        // Resolve the program's Salesforce Id by matching the dropdown's selected label
170        // against programLegend (same lookup the old redirect flow used). Use the captured
171        // step-1 legend — window.programLegend is overwritten once step 2 renders, which
172        // broke this lookup after "Back to program selection".
173        //
174        // Normalize whitespace on BOTH sides before comparing. HTMLOptionElement.text
175        // collapses internal runs of whitespace per spec, but the programLegend JSON label
176        // does not — so a program whose source label has a double space (e.g.
177        // "Certificate -  Computed Tomography") would never match (option.text = single space,
178        // legend = double). .trim() only strips the ends, not the middle. normalizeLabel
179        // collapses internal whitespace too so these line up regardless of data-entry quirks.
180        const normalizeLabel = function (s) {
181            return String(s == null ? '' : s).replace(/\s+/g, ' ').trim().toLowerCase();
182        };
183        const programLegend = step1ProgramLegend;
184        const selectedProgramLabel = normalizeLabel(programsDropdown.options[programsDropdown.selectedIndex].text);
185        let programId = '';
186        const matchedProgram = Object.values(programLegend).find(p =>
187            normalizeLabel(p.label) === selectedProgramLabel
188        );
189        if (matchedProgram && matchedProgram.value) {
190            programId = matchedProgram.value;
191        }
192
193        // Hard-stop: refuse to advance without a real SF program id. Previously a label-match
194        // failure silently set programId='' and proceeded, which corrupted lead rows
195        // (program_id column stored the label instead of an SF id, breaking
196        // resolve_program_label and causing some submissions to fail at /registeruser).
197        if (!programId) {
198            parkerDebugWarn('Program label did not match any programLegend entry:', selectedProgramLabel);
199            showInlineError("We couldn't match the selected program to its Salesforce record. Please pick a different program or refresh the page and try again.");
200            return;
201        }
202
203        const waiverCode = urlParams.get('waiver') || urlParams.get('waiver_code') || '';
204
205        submitButton.disabled = true;
206        clearInlineError();
207
208        // Scope to modal — #section-loader exists in both the standalone /application form
209        // and the modal's form, so document.getElementById would return the wrong one.
210        const sectionLoader = modalEl.querySelector('#section-loader');
211        if (sectionLoader) sectionLoader.classList.remove('hidden');
212        startLoadingSequence([
213            "Loading your selected program...",
214            "Setting your selected term...",
215            "Preparing application setup...",
216        ]);
217
218        // Kick off the intro-panel-slides-out animation BEFORE the AJAX completes so the
219        // motion plays in parallel with the network round-trip. But we ALSO need to ensure
220        // the swap waits for the animation to finish, so a fast AJAX doesn't interrupt the
221        // panel mid-slide. animationStartTime is referenced later in the .then() to compute
222        // any remaining delay before swapInApplicationForm runs.
223        const stage = document.getElementById('parker-appmodal-stage');
224        if (stage) stage.classList.add('is-step2');
225        const animationStartTime = Date.now();
226        // Must match the longer of (panel transition 0.4s + 0s delay) and (grid transition
227        // 0.4s + 0.4s delay) in styles.css. 800ms covers both phases of the Forward
228        // sequence: panel slides out (0–400ms), then grid collapses (400–800ms).
229        const STEP2_ANIMATION_MS = 800;
230
231        // TODO: the appconfig.js getActiveTerms() filter (date-range) and the server-side
232        // is_active column on wp_parker_terms can disagree about whether a term is "active".
233        // Today that mismatch surfaces as a friendly inline error instead of the old
234        // "bounce back to step 1" trap — but the underlying semantic mismatch should still
235        // be unified eventually.
236        const ajaxUrl = window.parkerAppmodalAjaxUrl || '/wp-admin/admin-ajax.php';
237        const payload = new URLSearchParams();
238        payload.set('action', 'render_parker_application');
239        payload.set('record_id', selectedProgramProcess);
240        payload.set('program_id', programId);
241        payload.set('program_term', selectedProgramTerm);
242        payload.set('waiver', waiverCode);
243
244        fetch(ajaxUrl, {
245            method: 'POST',
246            credentials: 'same-origin',
247            headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
248            body: payload.toString(),
249        })
250            .then(function (res) { return res.json().catch(function () { return { success: false, message: 'Unexpected server response.' }; }); })
251            .then(function (data) {
252                if (!data || !data.success) {
253                    var msg = (data && data.message) ? data.message : 'Unable to load the application form. Please try again.';
254                    showInlineError(msg);
255                    submitButton.disabled = false;
256                    if (sectionLoader) sectionLoader.classList.add('hidden');
257                    var shimmer = modalEl.querySelector('#shimmer-wrapper');
258                    if (shimmer) shimmer.remove();
259                    // Reverse the intro-out animation so the user isn't left staring at a
260                    // collapsed-intro step 1 after a server error.
261                    if (stage) stage.classList.remove('is-step2');
262                    return;
263                }
264
265                // Wait for the slide-out / grid-collapse animation to finish before swapping
266                // in step 2. Without this, a fast AJAX (~200ms) would interrupt the panel
267                // mid-flight, making the motion look broken.
268                var elapsed = Date.now() - animationStartTime;
269                var remaining = Math.max(0, STEP2_ANIMATION_MS - elapsed);
270                setTimeout(function () { swapInApplicationForm(data.html); }, remaining);
271            })
272            .catch(function (err) {
273                console.error('AppModal AJAX render failed:', err);
274                showInlineError('Network error while loading the application. Please try again.');
275                submitButton.disabled = false;
276                if (sectionLoader) sectionLoader.classList.add('hidden');
277                var shimmer = modalEl.querySelector('#shimmer-wrapper');
278                if (shimmer) shimmer.remove();
279                if (stage) stage.classList.remove('is-step2');
280            });
281    }, true); // capture phase
282
283    /**
284     * Stash step 1's DOM nodes into an in-memory fragment (preserving listeners, dropdown
285     * selections, and all state) and inject step 2 + a back link in their place. On Back the
286     * fragment is reattached to formContainer — step 1 picks up exactly where the user left
287     * off with no re-init needed.
288     */
289    function swapInApplicationForm(html) {
290        var formContainer = document.getElementById('parker-appmodal-form-container');
291        if (!formContainer) {
292            console.error('App modal form container not found, cannot swap in step 2.');
293            return;
294        }
295
296        // Detach step 1 into an in-memory DocumentFragment. The DOM nodes themselves are
297        // moved (not cloned), so all attached event listeners and form state ride along.
298        parkedStep1Nodes = document.createDocumentFragment();
299        while (formContainer.firstChild) {
300            parkedStep1Nodes.appendChild(formContainer.firstChild);
301        }
302
303        var step2Wrapper = document.createElement('div');
304        step2Wrapper.id = 'parker-appmodal-step2';
305        step2Wrapper.innerHTML = html;
306        formContainer.appendChild(step2Wrapper);
307
308        // Inject the back link INSIDE FormGenerator's .form-container (the white card) so it
309        // (a) shares the same opacity/visibility transition as the form — no more "back link
310        // sits alone then jumps once the form loads" jarring effect — and (b) aligns with the
311        // form fields' content start instead of overlapping the stage background image that
312        // bleeds through col-form at narrow widths.
313        var backLink = document.createElement('a');
314        backLink.href = '#';
315        backLink.className = 'parker-appmodal-back-link';
316        backLink.textContent = '← Back to program selection';
317        var formGenContainer = step2Wrapper.querySelector('.form-container');
318        if (formGenContainer) {
319            formGenContainer.insertBefore(backLink, formGenContainer.firstChild);
320        } else {
321            // Fallback if FormGenerator's wrapper isn't found for some reason — better to show
322            // the link in a slightly off position than to lose the Back UX entirely.
323            formContainer.insertBefore(backLink, formContainer.firstChild);
324        }
325
326        // innerHTML does not execute embedded <script> tags. Clone/replace each script node
327        // so the inline window.parkerEmbeddedContext block (and the inlined token_manager.js)
328        // actually runs before initParkerApplication() reads window globals.
329        var scripts = step2Wrapper.querySelectorAll('script');
330        scripts.forEach(function (oldScript) {
331            var newScript = document.createElement('script');
332            if (oldScript.src) {
333                newScript.src = oldScript.src;
334            } else {
335                newScript.textContent = oldScript.textContent;
336            }
337            for (var i = 0; i < oldScript.attributes.length; i++) {
338                var attr = oldScript.attributes[i];
339                if (attr.name !== 'src') newScript.setAttribute(attr.name, attr.value);
340            }
341            oldScript.parentNode.replaceChild(newScript, oldScript);
342        });
343
344        // Init functions are run in separate try/catch blocks so a failure in one doesn't
345        // prevent the other from running. We surface the actual error.message in the inline
346        // banner so we can see what's failing — generic "please refresh" hides real bugs.
347        try {
348            if (typeof window.initParkerApplication === 'function') {
349                window.initParkerApplication();
350            } else {
351                parkerDebugWarn('initParkerApplication is not available. Did script.js fail to load?');
352            }
353        } catch (e) {
354            console.error('initParkerApplication threw:', e);
355            console.error('  message:', e && e.message);
356            console.error('  stack:', e && e.stack);
357            showInlineError('Application init error: ' + (e && e.message ? e.message : 
357String(e)));
358        }
359
360        try {
361            if (typeof window.initParkerApplicationUi === 'function') {
362                window.initParkerApplicationUi();
363            }
364        } catch (e) {
365            console.error('initParkerApplicationUi threw:', e);
366            console.error('  message:', e && e.message);
367            console.error('  stack:', e && e.stack);
368            showInlineError('Application UI init error: ' + (e && e.message ? e.message : String(e)));
369        }
370
371        // validation.js's DOMContentLoaded ran at initial page load — when the password modal
372        // (#pwSubmitButton / #Password / #pwLetter etc.) wasn't in the DOM yet — and bailed
373        // out at its early-return guard. Re-run it now so the keyup → validatePassword
374        // listener gets attached and the password-strength hints actually turn green.
375        try {
376            if (typeof window.initParkerApplicationValidation === 'function') {
377                window.initParkerApplicationValidation();
378            }
379        } catch (e) {
380            console.error('initParkerApplicationValidation threw:', e);
381            console.error('  message:', e && e.message);
382            console.error('  stack:', e && e.stack);
383            showInlineError('Application validation init error: ' + (e && e.message ? e.message : String(e)));
384        }
385    }
386
387    /**
388     * Inline error helper — keeps the user in the modal with their selections intact rather
389     * than ever bouncing them back to a previous step or navigating away.
390     */
391    function showInlineError(message) {
392        // Scope to modal so we never target the standalone /application page's .form-container.
393        var formContainer = modalEl.querySelector('.form-container')
394            || modalEl.querySelector('#parker-appmodal-form-container');
395        if (!formContainer) {
396            alert(message);
397            return;
398        }
399        clearInlineError();
400        var banner = document.createElement('div');
401        banner.id = 'parker-appmodal-inline-error';
402        banner.setAttribute('role', 'alert');
403        banner.style.cssText = 'background:#fdecea;color:#a00;border:1px solid #e0b4b4;border-radius:4px;padding:10px 14px;margin:12px 0;font-size:14px;';
404        banner.textContent = message;
405        formContainer.insertBefore(banner, formContainer.firstChild);
406    }
407
408    function clearInlineError() {
409        var existing = modalEl.querySelector('#parker-appmodal-inline-error');
410        if (existing) existing.remove();
411    }
412
413    function getActiveTerms(terms) {
414        const today = new Date();
415        const activeTerms = terms.filter(term => {
416            const startDate = new Date(term.start_date);
417            const endDate = new Date(term.end_date);
418            const isActive = startDate <= today && endDate >= today;
419            return isActive;
420        });
421        parkerDebugLog("Filtered Active Terms:", activeTerms);
422        return activeTerms;
423    }
424
425    function sortTermsChronologically(terms) {
426        const monthOrder = {
427            'january': 0, 'february': 1, 'march': 2, 'april': 3,
428            'may': 4, 'june': 5, 'july': 6, 'august': 7,
429            'september': 8, 'october': 9, 'november': 10, 'december': 11
430        };
431
432        const seasonOrder = {
433            'winter': 0,
434            'spring': 3,
435            'summer': 4,
436            'fall': 8
437        };
438
439        function getTermSortKey(termName) {
440            const lower = termName.toLowerCase();
441            const yearMatch = termName.match(/\b(\d{4})\b/);
442            const year = yearMatch ? parseInt(yearMatch[1], 10) : 9999;
443
444            let month = -1;
445            for (const [name, idx] of Object.entries(monthOrder)) {
446                if (lower.includes(name)) {
447                    month = idx;
448                    break;
449                }
450            }
451
452            if (month === -1) {
453                for (const [name, idx] of Object.entries(seasonOrder)) {
454                    if (lower.includes(name)) {
455                        month = idx;
456                        break;
457                    }
458                }
459            }
460
461            if (month === -1) month = 99;
462            return year * 100 + month;
463        }
464
465        return [...terms].sort((a, b) => getTermSortKey(a.name) - getTermSortKey(b.name));
466    }
467
468    function startLoadingSequence(lines = []) {
469        const visibleLines = 3;
470        // Scope to modal — .spinner and .form-container exist in both the standalone
471        // /application page form and the modal's form. Without scoping, the shimmer would
472        // be appended to the wrong element on the /application page.
473        const spinner = modalEl.querySelector(".spinner");
474        const formContainer = modalEl.querySelector(".form-container");
475
476        if (!formContainer) return;
477
478        const existingWrapper = modalEl.querySelector("#shimmer-wrapper");
479        if (existingWrapper) existingWrapper.remove();
480
481        const shimmerWrapper = document.createElement("div");
482        shimmerWrapper.id = "shimmer-wrapper";
483        shimmerWrapper.style.height = "4.8em";
484        shimmerWrapper.style.overflow = "hidden";
485        shimmerWrapper.style.position = "relative";
486
487        const shimmerContainer = document.createElement("div");
488        shimmerContainer.id = "loading-lines";
489        shimmerContainer.style.transition = "transform 0.4s ease-in-out";
490
491        shimmerWrapper.appendChild(shimmerContainer);
492        formContainer.appendChild(shimmerWrapper);
493
494        if (spinner) {
495            spinner.insertAdjacentElement('afterend', shimmerWrapper);
496            spinner.classList.add("hidden");
497        }
498
499        const tempLine = document.createElement("div");
500        tempLine.className = "loading-line";
501        tempLine.textContent = lines[0];
502        shimmerContainer.appendChild(tempLine);
503
504        requestAnimationFrame(() => {
505            const lineHeight = tempLine.getBoundingClientRect().height;
506            const containerWidth = shimmerWrapper.getBoundingClientRect().width;
507            shimmerContainer.style.minWidth = `${tempLine.getBoundingClientRect().width}px`;
508            shimmerWrapper.style.height = `${lineHeight * visibleLines}px`;
509            shimmerWrapper.style.width = `${containerWidth}px`;
510            shimmerContainer.removeChild(tempLine);
511
512            showNextLine(0, lineHeight);
513        });
514
515        function showNextLine(index, lineHeight) {
516            if (index < lines.length) {
517                const line = document.createElement("div");
518                line.className = "loading-line";
519                line.textContent = lines[index];
520                shimmerContainer.appendChild(line);
521                setTimeout(() => line.classList.add("visible"), 10);
522
523                if (index >= visibleLines) {
524                    shimmerContainer.style.transform = `translateY(-${(index - visibleLines + 1) * lineHeight}px)`;
525                }
526
527                setTimeout(() => showNextLine(index + 1, lineHeight), 2100);
528            }
529        }
530    }
531}
532
533window.initParkerAppmodalStep1 = initParkerAppmodalStep1;
534
535document.addEventListener("DOMContentLoaded", initParkerAppmodalStep1);
536
537/**
538 * Document-level click delegation for the "← Back to program selection" link injected by
539 * swapInApplicationForm. Lives outside initParkerAppmodalStep1 so it's only registered once
540 * per page — registering it inside the init function would stack up duplicate handlers each
541 * time step 1 is re-initialized.
542 */
543document.addEventListener('click', function (e) {
544    var link = e.target.closest('.parker-appmodal-back-link');
545    if (!link) return;
546
547    e.preventDefault();
548
549    var formContainer = document.getElementById('parker-appmodal-form-container');
550    if (!formContainer || !parkedStep1Nodes) {
551        parkerDebugWarn('Cannot go back: step 1 nodes not parked.');
552        return;
553    }
554
555    // Tear down step 2. Its listeners die with its DOM — fine, they were specific to that
556    // step's form fields and IDs.
557    formContainer.innerHTML = '';
558
559    // Reattach the parked step 1 nodes. Because we're moving the SAME node references back
560    // (not cloning), every event listener attached during the first init is still wired up,
561    // and the dropdowns retain their .value selections natively. No re-init needed.
562    formContainer.appendChild(parkedStep1Nodes);
563    parkedStep1Nodes = null;
564
565    // Reverse the intro-panel-slides-out animation. Removing the class triggers the CSS
566    // transition the other direction: grid template expands back to 3 columns and the
567    // intro panel slides in from the left.
568    var stage = document.getElementById('parker-appmodal-stage');
569    if (stage) stage.classList.remove('is-step2');
570
571    // Step 1 was parked mid-submit — the click handler had already kicked off the loading
572    // shimmer, disabled the submit button, swapped the spinner, and shown the section loader
573    // before the AJAX response came back. All that transient state was inside the parked DOM
574    // and rode along into the fragment. Now that step 1 is visible again we reset those four
575    // pieces so the form is interactive and visually idle, ready for another Next click.
576    var leftoverShimmer = formContainer.querySelector('#shimmer-wrapper');
577    if (leftoverShimmer) leftoverShimmer.remove();
578
579    var step1SubmitBtn = formContainer.querySelector('#submit-btn');
580    if (step1SubmitBtn) step1SubmitBtn.disabled = false;
581
582    var step1SectionLoader = formContainer.querySelector('#section-loader');
583    if (step1SectionLoader) step1SectionLoader.classList.add('hidden');
584
585    var step1Spinner = formContainer.querySelector('.spinner');
586    if (step1Spinner) step1Spinner.classList.remove('hidden');
587});

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.