PageSourceSearch

https://www.champlain.edu/app/themes/champlain-core/src-core/js/co…/gf-compound-entry-term.js?ver=1790335777

js champlain.edu collected 2026-10-01 08:53:35 UTC 12,822 bytes, 302 lines download raw bytes

1/**
2 * Front-end behavior for CC_GF_CompoundEntryTermField.
3 *
4 * Two dependent questions in one Gravity Forms field:
5 *
6 *   1. Student Type - a radio group (or a hidden input, when the field is
7 *      locked to one type via the ccFixedStudentType setting).
8 *   2. A second question whose visible label, placeholder and option list are
9 *      all derived from the answer to question 1. It stays hidden until a
10 *      Student Type has been chosen.
11 *
12 * All date math runs against the visitor's local clock (new Date()) rather
13 * than server time, so a page served from cache still produces a correct,
14 * current option list.
15 *
16 * Cutoff dates, year-counts AND the label/placeholder strings all come from
17 * the per-field config baked into data-gf-compound-entry-term-config by the
18 * PHP field class. Nothing user-visible is hard-coded here - keeping the
19 * strings server-side means they stay translatable and live in one place.
20 */
21( function ( $ ) {
22    'use strict';
23
24    // First-Year: plain Fall terms, values "{year}FA". Starts at the
25    // current calendar year, unless today has already passed this year's
26    // cutoff (default 7/1) - in which case the list starts at next year
27    // instead. cfg: { yearsCount, cutoffMonth, cutoffDay }
28    function buildFirstYearOptions( today, cfg ) {
29        var y = today.getFullYear();
30        var cutoff = new Date( y, cfg.cutoffMonth - 1, cfg.cutoffDay );
31        var baseYear = today >= cutoff ? y + 1 : y;
32
33        var options = [];
34        for ( var i = 0; i < cfg.yearsCount; i++ ) {
35            var year = baseYear + i;
36            options.push( { label: String( year ), value: year + 'FA' } );
37        }
38        return options;
39    }
40
41    // Transfer: a Spring + Fall pair for each year starting at the current
42    // calendar year. Each year's Spring/Fall is checked against its own
43    // cutoff independently - only the current year can realistically be
44    // affected, since future years' cutoff dates haven't happened yet.
45    // cfg: { yearsCount, fallCutoffMonth, fallCutoffDay,
46    //        springCutoffMonth, springCutoffDay }
47    function buildTransferOptions( today, cfg ) {
48        var y = today.getFullYear();
49        var terms = [];
50
51        for ( var i = 0; i < cfg.yearsCount; i++ ) {
52            var year = y + i;
53
54            var springCutoff = new Date( year, cfg.springCutoffMonth - 1, cfg.springCutoffDay );
55            if ( today < springCutoff ) {
56                terms.push( { year: year, term: 'SP' } );
57            }
58
59            var fallCutoff = new Date( year, cfg.fallCutoffMonth - 1, cfg.fallCutoffDay );
60            if ( today < fallCutoff ) {
61                terms.push( { year: year, term: 'FA' } );
62            }
63        }
64
65        // Chronological order: Spring before Fall within the same year.
66        terms.sort( function ( a, b ) {
67            var av = a.year + ( a.term === 'FA' ? 0.5 : 0 );
68            var bv = b.year + ( b.term === 'FA' ? 0.5 : 0 );
69            return av - bv;
70        } );
71
72        return terms.map( function ( t ) {
73            return {
74                label: ( t.term === 'FA' ? 'Fall ' : 'Spring ' ) + t.year,
75                value: t.year + t.term
76            };
77        } );
78    }
79
80    /**
81     * Resolve the currently selected Student Type for a field instance.
82     *
83     * Handles both render modes: a radio group (visitor chooses) and a single
84     * hidden input (ccFixedStudentType locks the field to one type).
85     *
86     * Calling .val() directly on a radio set is unsafe - jQuery returns the
87     * FIRST radio's value whether or not anything is checked, which would
88     * make the field behave as though "first_year" were always selected.
89     *
90     * @param {jQuery} $wrap The .cc-compound-entry-term-field container.
91     * @return {string} 'first_year', 'transfer', or '' if nothing selected.
92     */
93    function getStudentType( $wrap ) {
94        var $inputs = $wrap.find( '.cc-student-type-select' );
95        var $radios = $inputs.filter( '[type="radio"]' );
96
97        if ( $radios.length ) {
98            return $radios.filter( ':checked' ).val() || '';
99        }
100
101        return $inputs.val() || '';
102    }
103
104    /**
105     * Show or hide the second question.
106     *
107     * Uses the `hidden` attribute rather than a CSS class so the subtree is
108     * removed from the accessibility tree and the tab order, not merely
109     * painted out (WCAG 2.4.3 Focus Order). The select is disabled 
109while
110     * hidden, which also keeps it out of the POST - so a required field
111     * correctly fails validation when no Student Type was chosen.
112     *
113     * Focus is deliberately NOT moved into the revealed control: the new
114     * question appears immediately after the radio the visitor just
115     * activated, so natural tab order reaches it, and stealing focus would
116     * be an unexpected change of context (WCAG 3.2.2 On Input).
117     *
118     * @param {jQuery}  $wrapper  The .cc-entry-term-wrap element.
119     * @param {jQuery}  $select   The .cc-entry-term-select element.
120     * @param {boolean} reveal    Whether to show the question.
121     */
122    function setEntryTermVisibility( $wrapper, $select, reveal ) {
123        $wrapper.prop( 'hidden', ! reveal );
124        $select.prop( 'disabled', ! reveal );
125    }
126
127    /**
128     * Rebuild the second question for the current Student Type.
129     *
130     * Sets the visible label text, the placeholder option and the option list
131     * from the per-field config, then reveals the question. Hides and clears
132     * it when no Student Type is selected.
133     *
134     * @param {jQuery}  $wrap     The .cc-compound-entry-term-field container.
135     * @param {boolean} isInitial True on first population (page load or
136     *                            re-render), false when the visitor changed
137     *                            Student Type themselves.
138     */
139    function populateEntryTerm( $wrap, isInitial ) {
140        var config      = $wrap.data( 'gfCompoundEntryTermConfig' );
141        var $wrapper    = $wrap.find( '.cc-entry-term-wrap' );
142        var $label      = $wrap.find( '.cc-entry-term-label' );
143        var $entryTerm  = $wrap.find( '.cc-entry-term-select' );
144        var studentType = getStudentType( $wrap );
145        var today       = new Date();
146
147        if ( ! config || ! $entryTerm.length ) {
148            return;
149        }
150
151        // The server-provided saved value (data-current-value) only makes
152        // sense to restore on the very first populate call - e.g. editing
153        // an existing entry, or redisplaying the form after a validation
154        // error. Once the visitor changes Student Type themselves, any
155        // prior Entry Term selection belongs to the old option set and
156        // won't exist in the new one - trying to reapply it just leaves
157        // the select blank instead of showing the placeholder.
158        var currentValue = isInitial ? $entryTerm.data( 'currentValue' ) : null;
159
160        $entryTerm.empty();
161
162        if ( ! studentType ) {
163            // Nothing chosen yet: keep the question out of the document
164            // flow entirely rather than showing a disabled placeholder.
165            setEntryTermVisibility( $wrapper, $entryTerm, false );
166            return;
167        }
168
169        var strings = studentType === 'first_year' ? config.firstYear : config.transfer;
170        var options = studentType === 'first_year'
171            ? buildFirstYearOptions( today, config.firstYear )
172            : buildTransferOptions( today, config.transfer );
173
174        // Swap the VISIBLE label. The select carries no aria-label - an
175        // aria-label would override this text for screen reader users and
176        // they would hear something different from what is on screen.
177        // .text() (not .html()) so the string is treated as text, never markup.
178        $label.contents().filter( function () {
179            return this.nodeType === 3; // text nodes only - preserve the
180                                        // required asterisk <span> sibling.
181        } ).remove();
182        $label.prepend( document.createTextNode( strings.label ) );
183
184        $entryTerm.append( $( '<option>', { value: '', text: strings.placeholder } ) );
185        $.each( options, function ( i, opt ) {
186            $entryTerm.append( $( '<option>', { value: opt.value, text: opt.label } ) );
187        } );
188
189        if ( currentValue ) {
190            $entryTerm.val( currentValue );
191            // If the saved value isn't in the rebuilt list (e.g. it rolled
192            // off since the entry was saved), fall back to the placeholder
193            // instead of leaving the select with nothing visibly selected.
194            if ( $entryTerm.val() === null ) {
195                $entryTerm.val( '' );
196            }
197        }
198
199        setEntryTermVisibility( $wrapper, $entryTerm, true );
200    }
201
202    /**
203     * Populate every instance of the field on the page.
204     *
205     * Runs as the initial pass, so any Student Type already selected
206     * server-side (a locked field, or a value carried through a failed
207     * validation) is honoured and its saved Entry Term restored.
208     */
209    function initAll() {
210        $( '.cc-compound-entry-term-field' ).each( function () {
211            populateEntryTerm( $( this ), true );
212        } );
213    }
214
215    // Delegated so it survives Gravity Forms re-rendering the markup. Works
216    // for radios as well as selects - `change` fires when a radio is
217    // activated by click, tap, arrow key or spacebar.
218    $( document ).on( 'change', '.cc-student-type-select', function () {
219        populateEntryTerm( $( this ).closest( '.cc-compound-entry-term-field' ), false );
220    } );
221
222    /**
223     * Teach Gravity Forms how to read this field's value for conditional logic.
224     *
225     * GF's client-side rule evaluation looks for a single element carrying the
226     * canonical input id. This field renders Student Type as a radio group
227     * (ids suffixed with the choice value), so that lookup finds nothing and
228     * every rule silently evaluates against an empty value - dependent fields
229     * then never show. gform_is_value_match is GF's documented extension point
230     * for exactly this case: a custom field type that stores its value in a
231     * non-standard way.
232     *
233     * Note rule.fieldId is a sub-input id such as "43.1", which cannot be
234     * interpolated into a selector directly - "#field_1_43.1" parses as id
235     * field_1_43 with class "1". The integer part is extracted for the
236     * container lookup and the decimal part identifies which input is targeted.
237     *
238     * Returns isMatch untouched for every rule that is not ours, so this cannot
239     * affect conditional logic elsewhere on the form.
240     *
241     * @param {boolean} isMatch GF's own comparison result.
242     * @param {number}  formId  Form being evaluated.
243     * @param {Object}  rule    { fieldId, operator, value }.
244     * @return {boolean}
245     */
246    function matchCompoundEntryTermRule( isMatch, formId, rule ) {
247        var fieldId = parseInt( rule.fieldId, 10 );
248
249        if ( isNaN( fieldId ) ) {
250            return isMatch;
251        }
252
253        var $wrap = jQuery( '#field_' + formId + '_' + fieldId )
254            .find( '.cc-compound-entry-term-field' );
255
256        if ( ! $wrap.length ) {
257            return isMatch;
258        }
259
260        var inputIndex = String( rule.fieldId ).split( '.' )[ 1 ];
261        var value;
262
263        if ( '1' === inputIndex ) {
264            value = getStudentType( $wrap );
265        } else if ( '2' === inputIndex ) {
266            value = $wrap.find( '.cc-entry-term-select' ).val() || '';
267        } else {
268            // A rule against the parent field id has no single value to compare.
269            return isMatch;
270        }
271
272        return gf_matches_operation( value, rule.value, rule.operator );
273    }
274
275    /**
276     * Register the filter once the gform object exists.
277     *
278     * This script declares no dependency on GF's own bundle, so gform may not be
279     * defined at parse time. Registration is attempted immediately and again on
280     * the later hooks, guarded by a flag so it only ever takes effect once.
281     */
282    var clFilterRegistered = false;
283
284    function registerConditionalLogicFilter() {
285        if ( clFilterRegistered || typeof gform === 'undefined' || ! gform.addFilter ) {
286            return;
287        }
288
289        gform.addFilter( 'gform_is_value_match', matchCompoundEntryTermRule );
290        clFilterRegistered = true;
291    }
292
293    registerConditionalLogicFilter();
294    $( document ).ready( registerConditionalLogicFilter );
295    $( document ).ready( initAll );
296
297    // Gravity Forms re-renders the field markup after AJAX page changes
298    // and after conditional-logic-driven redraws - re-init each time.
299    $( document ).on( 'gform_post_render', initAll );
300    $( document ).on( 'gform_post_render', registerConditionalLogicFilter );
301
302} )( jQuery );

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.