PageSourceSearch

https://campus.gov.il/wp-content/themes/campusnew/js/contact-form.js?var=190

js campus.gov.il collected 2026-10-02 06:35:06 UTC 10,181 bytes, 229 lines download raw bytes

1/* Contact form behaviour that CSS cannot express.
2 *
3 * Two jobs:
4 *   1. Format the phone field as it is typed, to the Israeli 05X-XXX-XXXX shape.
5 *   2. Mark the subject field as filled, so its floating label behaves like the text fields.
6 *      A <select> has no :placeholder-shown, so the "has a value" half of that rule needs a class.
7 *
8 * Everything else — revealing the label on focus, hiding the placeholder — is CSS.
9 */
10(function () {
11  'use strict';
12
13  var PREFIX = '0';         // every Israeli number starts here, so the visitor need not type it
14
15  /**
16   * Grouping follows the prefix: mobile and VoIP (05X/07X) are 10 digits shaped 3-3-4,
17   * landline area codes (02/03/04/08/09) are 9 digits shaped 2-3-4.
18   */
19  function groupsFor(digits) {
20    if (/^0[57]/.test(digits)) return {groups: [3, 3, 4], max: 10};
21    if (/^0[23489]/.test(digits)) return {groups: [2, 3, 4], max: 9};
22    return {groups: [3, 3, 4], max: 10};   // undecided or invalid prefix — mobile shape
23  }
24
25  /** Digits only, grouped with dashes. Anything the visitor pastes is normalised the same way. */
26  function formatPhone(raw) {
27    var all = String(raw).replace(/\D/g, '');
28    // The focus handler seeds a leading 0; visitors who type the full number anyway would
29    // produce 00… — no Israeli number starts that way, so duplicates collapse to one.
30    all = all.replace(/^0+/, '0');
31    var spec = groupsFor(all);
32    var digits = all.slice(0, spec.max);
33    var out = '', at = 0;
34    for (var i = 0; i < spec.groups.length && at < digits.length; i++) {
35      if (out) out += '-';
36      out += digits.substr(at, spec.groups[i]);
37      at += spec.groups[i];
38    }
39    return out;
40  }
41
42  /** Digits before the caret — the stable reference point when dashes shift positions. */
43  function digitsBefore(value, caret) {
44    return value.slice(0, caret).replace(/\D/g, '').length;
45  }
46
47  /** Caret position holding that many digits to its left. */
48  function caretAfterDigits(value, count) {
49    if (count <= 0) return 0;
50    var seen = 0;
51    for (var i = 0; i < value.length; i++) {
52      if (/\d/.test(value[i])) seen++;
53      if (seen === count) return i + 1;
54    }
55    return value.length;
56  }
57
58  /**
59   * Keeps the caret to the right of the leading 0.
60   *
61   * Clicking into the field places the caret wherever the pointer landed, and the browser does
62   * that after the focus handler has seeded the prefix — so a click on the left edge left the
63   * caret at position 0 and the next digit landed before the zero, turning 05… into 50…. Every
64   * Israeli number starts with that 0, so no caret position in front of it is ever meaningful.
65   * Keyboard movement (Home, arrow-left) is clamped for the same reason.
66   */
67  function keepCaretAfterPrefix(input) {
68    if (input.value.charAt(0) !== PREFIX) return;
69    // A deliberate selection that spans the prefix is left alone — the visitor is replacing it.
70    if (input.selectionStart !== input.selectionEnd) return;
71    if (input.selectionStart >= PREFIX.length) return;
72    try { input.setSelectionRange(PREFIX.length, PREFIX.length); } catch (e) {}
73  }
74
75  function bindPhone(input) {
76    // Seed the prefix on focus so typing starts at the third digit.
77    input.addEventListener('focus', function () {
78      if (this.value !== '') return;
79      this.value = PREFIX;
80      // Tell the floating label and the validator the field changed.
81      this.dispatchEvent(new Event('input', { bubbles: true }));
82      var end = this.value.length;
83      try { this.setSelectionRange(end, end); } catch (e) {}
84    });
85
86    // Nothing beyond the seeded prefix means the visitor never really filled this in — clear it so
87    // the field reads as untouched: placeholder returns, label drops, no "invalid" complaint.
88    //
89    // The validator has already run by this point: its focusout handler is delegated on the form
90    // and fires before this, so it has judged the bare "05" and rendered a message. Clearing the
91    // value alone would leave that message stranded, so it is removed here too — unless the visitor
92    // has already tried to send, in which case an empty required field genuinely should complain,
93    // and the field is simply re-validated to get the right message.
94    input.addEventListener('blur', function () {
95      if (this.value.replace(/\D/g, '').length > PREFIX.length) return;
96      this.value = '';
97      this.dispatchEvent(new Event('input', { bubbles: true }));
98
99      // Set by the form's validator on the first send attempt. validator.submitted is not a
100      // substitute: it fills up on any element check, including the one that just ran.
101      if (this.form && this.form.dataset.attempted === '1') { jQuery(this).val
101id(); return; }
102
103      var message = document.getElementById(this.id + '-error');
104      if (message) message.parentNode.removeChild(message);
105      this.classList.remove('error');
106      this.removeAttribute('aria-describedby');
107      this.removeAttribute('aria-invalid');
108    });
109
110    // The click runs after the browser has placed the caret, which is the only point late enough
111    // to correct it. keyup covers Home and arrow-left reaching the same position.
112    input.addEventListener('click', function () { keepCaretAfterPrefix(this); });
113    input.addEventListener('keyup', function () { keepCaretAfterPrefix(this); });
114
115    input.addEventListener('input', function () {
116      // Re-anchor on digit count rather than raw offset, so inserted dashes do not drag the caret.
117      var wanted = digitsBefore(this.value, this.selectionStart);
118      var formatted = formatPhone(this.value);
119      if (formatted === this.value) {
120        keepCaretAfterPrefix(this);
121        return;
122      }
123      this.value = formatted;
124      var pos = caretAfterDigits(formatted, wanted);
125      try { this.setSelectionRange(Math.max(pos, PREFIX.length), Math.max(pos, PREFIX.length)); } catch (e) {}
126    });
127  }
128
129  function bindSelect(select) {
130    var field = select.closest('.cf-field');
131    if (!field) return;
132    var sync = function () {
133      // "0" is the prompt option, not a real choice.
134      field.classList.toggle('cf-field--filled', select.value !== '' && select.value !== '0');
135    };
136    select.addEventListener('change', sync);
137    sync();
138  }
139
140  /* ----- Confirmation modal ------------------------------------------------
141   * Opened by the form's submit handler with the result kind and message.
142   * The close button is the CPT popup modal's, so it gets the same device-pixel
143   * nudge measurement: the surface centre can fall on a half pixel, and a 36px
144   * circle drawn across it stops reading as round.
145   */
146  window.cilContactModal = (function () {
147    var opener = null;
148
149    function el()      { return document.getElementById('contactFormModal'); }
150    function surface() { return el() && el().querySelector('.cf-modal__surface'); }
151    function closeBtn(){ return el() && el().querySelector('.cf-modal__close a'); }
152
153    function nudgeClose() {
154      var modal = el();
155      var wrap = modal.querySelector('.cf-modal__close');
156      var box = surface();
157      if (!wrap || !box) return;
158      var rect = box.getBoundingClientRect();
159      var left = rect.left + rect.width / 2 - wrap.offsetWidth / 2;
160      var nudge = Math.round(left) - left;
161      wrap.style.setProperty('--cil-close-nudge', nudge.toFixed(3) + 'px');
162    }
163
164    function onKeydown(e) {
165      if (e.key === 'Escape') close();
166    }
167
168    function open(kind, title) {
169      var modal = el();
170      if (!modal) return;
171      // document.activeElement is <body> by this point: submitting disables the send button, and
172      // disabling a focused element blurs it. Returning focus to <body> on close drops a keyboard
173      // or screen-reader visitor at the top of the document instead of back where they were, so
174      // the send button is named explicitly and only then falls back to whatever holds focus.
175      opener = document.getElementById('contactFormSubmit') || document.activeElement;
176      modal.querySelector('.cf-modal__title').textContent = title || '';
177      modal.classList.remove('is-success', 'is-error');
178      modal.classList.add(kind === 'success' ? 'is-success' : 'is-error');
179      modal.hidden = false;
180      // Two frames so the browser commits display:flex before the transitions start.
181      requestAnimationFrame(function () { requestAnimationFrame(function () {
182        modal.classList.add('is-open');
183        nudgeClose();
184        var btn = closeBtn();
185        if (btn) btn.focus();
186      }); });
187      document.addEventListener('keydown', onKeydown);
188    }
189
190    function close() {
191      var modal = el();
192      if (!modal || modal.hidden) return;
193      modal.classList.remove('is-open');
194      document.removeEventListener('keydown', onKeydown);
195      // Let the backdrop/box fade run before the element leaves the tree.
196      setTimeout(function () {
197        modal.hidden = true;
198        // A disabled control cannot take focus, and the send button is still disabled 
198while the
199        // request settles — re-enabling is the submit path's job, so only focus what can accept it.
200        if (opener && document.contains(opener) && !opener.disabled) opener.focus();
201        opener = null;
202      }, 320);
203    }
204
205    function bind() {
206      var modal = el();
207      if (!modal) return;
208      [].forEach.call(modal.querySelectorAll('[data-cf-modal-close]'), function (t) {
209        t.addEventListener('click', function (e) { e.preventDefault(); close(); });
210      });
211    }
212
213    return { open: open, close: close, bind: bind };
214  })();
215
216  function init() {
217    var phone = document.getElementById('contact_form_tel');
218    if (phone) bindPhone(phone);
219    var subject = document.getElementById('contact_subject');
220    if (subject) bindSelect(subject);
221    window.cilContactModal.bind();
222  }
223
224  if (document.readyState === 'loading') {
225    document.addEventListener('DOMContentLoaded', init);
226  } else {
227    init();
228  }
229})();

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.