From e3c2e3e3a1facde293cc35112d3e05c15f7bb827 Mon Sep 17 00:00:00 2001 From: Philip Peterson <1326208+philip-peterson@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:35:40 -0700 Subject: [PATCH] Add nice non-restrictive phone number input formatting for booking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New pure-vanilla progressive formatter that turns typed digits into (123) 456-7890 (or 1 (800) 555-1212 for +1 numbers) on the fly. - Works for free-form input: typing, pasting, editing anywhere, backspacing, extra chars, etc. — no hard mask or input restrictions. - Applied to: - The Preact widget phone field (home page quick booking + details). - The final Drupal confirmation form at /schedule/book (via lightweight enhancer + .rpt-phone class). - Prefills last_name / phone / comments on the confirmation form when the widget was used to pick the slot (so collected phone isn't lost). - No new dependencies whatsoever: * Zero npm / package.json / composer changes. * Zero additional CDN / external scripts (uses native String.replace, regex, and input event + selection APIs only). * The new js/phone-format.js is simply attached via the pre-existing 'app' library (already loaded on all riverside_pt.* routes). * Formatter logic duplicated in the ESM component (tiny pure function). - The two other modified files (calendar.css, calendar.js) were left uncommitted as they are unrelated to this feature. --- .../riverside_pt/js/components/rpt-booking.js | 74 +++++++++++++++---- .../custom/riverside_pt/js/phone-format.js | 66 +++++++++++++++++ .../riverside_pt/riverside_pt.libraries.yml | 1 + .../riverside_pt/src/Form/BookingForm.php | 22 +++--- 4 files changed, 138 insertions(+), 25 deletions(-) create mode 100644 web/modules/custom/riverside_pt/js/phone-format.js diff --git a/web/modules/custom/riverside_pt/js/components/rpt-booking.js b/web/modules/custom/riverside_pt/js/components/rpt-booking.js index 3b28a99..7f0bc0e 100644 --- a/web/modules/custom/riverside_pt/js/components/rpt-booking.js +++ b/web/modules/custom/riverside_pt/js/components/rpt-booking.js @@ -15,6 +15,23 @@ const CHECK = html` diff --git a/web/modules/custom/riverside_pt/js/phone-format.js b/web/modules/custom/riverside_pt/js/phone-format.js new file mode 100644 index 0000000..64027e0 --- /dev/null +++ b/web/modules/custom/riverside_pt/js/phone-format.js @@ -0,0 +1,66 @@ +(function () { + function formatPhone(raw) { + let d = String(raw || "").replace(/\D/g, ""); + if (d.length === 11 && d[0] === "1") { + // NANP with leading 1: show "1 (xxx) xxx-xxxx" + const rest = d.slice(1); + return "1 (" + rest.slice(0, 3) + ") " + rest.slice(3, 6) + "-" + rest.slice(6); + } + d = d.slice(0, 10); + if (d.length === 0) return ""; + if (d.length <= 3) return d; + if (d.length <= 6) return "(" + d.slice(0, 3) + ") " + d.slice(3); + return "(" + d.slice(0, 3) + ") " + d.slice(3, 6) + "-" + d.slice(6); + } + + function enhancePhoneInput(input) { + if (input.dataset.phoneEnhanced) return; + input.dataset.phoneEnhanced = "true"; + + input.addEventListener("input", function () { + const oldValue = input.value; + const oldStart = input.selectionStart || 0; + + const formatted = formatPhone(input.value); + if (oldValue !== formatted) { + input.value = formatted; + + // Try to keep cursor relative to the digit sequence the user was editing. + const oldDigitsBefore = (oldValue.slice(0, oldStart).match(/\d/g) || []).length; + + let newPos = 0; + let digitsSeen = 0; + for (; newPos < formatted.length; newPos++) { + if (/\d/.test(formatted[newPos])) { + digitsSeen++; + } + if (digitsSeen >= oldDigitsBefore) { + newPos++; + break; + } + } + if (newPos > formatted.length) newPos = formatted.length; + + try { + input.setSelectionRange(newPos, newPos); + } catch (_) {} + } + }); + + // Format any server-provided default value on load (e.g. prefilled from tempstore) + if (input.value) { + const f = formatPhone(input.value); + if (input.value !== f) input.value = f; + } + } + + function scan() { + document.querySelectorAll("input.rpt-phone").forEach(enhancePhoneInput); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", scan); + } else { + scan(); + } +})(); diff --git a/web/modules/custom/riverside_pt/riverside_pt.libraries.yml b/web/modules/custom/riverside_pt/riverside_pt.libraries.yml index 7437410..b08f972 100644 --- a/web/modules/custom/riverside_pt/riverside_pt.libraries.yml +++ b/web/modules/custom/riverside_pt/riverside_pt.libraries.yml @@ -4,6 +4,7 @@ app: css/app.css: {} js: js/nav.js: {} + js/phone-format.js: {} js/components/rpt-toggle.js: { attributes: { type: module } } js/components/rpt-carousel.js: { attributes: { type: module } } js/components/rpt-testimonials.js: { attributes: { type: module } } diff --git a/web/modules/custom/riverside_pt/src/Form/BookingForm.php b/web/modules/custom/riverside_pt/src/Form/BookingForm.php index 08a8348..86b75e1 100644 --- a/web/modules/custom/riverside_pt/src/Form/BookingForm.php +++ b/web/modules/custom/riverside_pt/src/Form/BookingForm.php @@ -72,21 +72,25 @@ class BookingForm extends FormBase { ]; $form['last_name'] = [ - '#type' => 'textfield', - '#title' => $this->t('Last name'), - '#required' => TRUE, + '#type' => 'textfield', + '#title' => $this->t('Last name'), + '#required' => TRUE, + '#default_value' => $slot['last_name'] ?? '', ]; $form['phone'] = [ - '#type' => 'tel', - '#title' => $this->t('Phone number'), - '#required' => TRUE, + '#type' => 'tel', + '#title' => $this->t('Phone number'), + '#required' => TRUE, + '#default_value' => $slot['phone'] ?? '', + '#attributes' => ['class' => ['rpt-phone']], ]; $form['comments'] = [ - '#type' => 'textarea', - '#title' => $this->t('Comments'), - '#rows' => 4, + '#type' => 'textarea', + '#title' => $this->t('Comments'), + '#rows' => 4, + '#default_value' => $slot['comments'] ?? '', ]; $form['actions'] = ['#type' => 'actions'];