Skip to content
Coastal Healthcare Advocates

Source: calendly.js

/**
 * @file Calendly's scheduler, embedded inline instead of linked to.
 *
 * The consultation calendar used to be an outbound link to calendly.com. It is
 * now an **inline embed**: the scheduler sits in the page — the closing
 * `#schedule` band, and a panel in the contact dialog — so a visitor books
 * without leaving the site.
 *
 * ## Why not Calendly's `widget.js`?
 *
 * Calendly's documented inline embed is a `<div class="calendly-inline-widget">`
 * plus their `widget.js`. For an inline embed that script does three things:
 * it builds an `<iframe>` whose URL carries `embed_domain` and
 * `embed_type=Inline`, shows a spinner, and (with `data-resize`) sizes the
 * frame from the `calendly.page_height` messages the framed page posts. This
 * module does the same from first-party code, so the site's CSP only has to
 * allow `https://calendly.com` on `frame-src`, and no third-party script runs
 * in the page that holds the intake form. Calendly documents the plain iframe
 * as a supported alternative.
 *
 * ## Authored markup
 *
 * A mount that shows the scheduler as soon as the visitor scrolls near it:
 *
 * ```html
 * <div class="calendly-embed" data-calendly="https://calendly.com/…"
 *      data-calendly-title="Pick a time (Calendly scheduler)">
 *   <p class="calendly-fallback"><a class="btn" href="https://calendly.com/…">Schedule…</a></p>
 *   <p class="calendly-alt">Calendar not loading? <a href="https://calendly.com/…">Book on Calendly</a></p>
 * </div>
 * ```
 *
 * - `.calendly-fallback` is the **no-JavaScript fallback** (and what prints):
 *   the plain link out. It hides once the frame is in place.
 * - `.calendly-alt` shows only beside a live frame: the way out for a visitor
 *   whose network or content blocker refuses the frame, which a page can't
 *   detect across origins.
 *
 * A mount that waits to be asked for — the dialog's — is authored `hidden`
 * and opened by a disclosure button:
 *
 * ```html
 * <button type="button" data-calendly-toggle aria-expanded="false"
 *         aria-controls="cm-booking">Book time with us</button>
 * <div id="cm-booking" class="calendly-embed calendly-embed--panel"
 *      data-calendly="…" hidden>…</div>
 * ```
 *
 * The style guide borrows the dialog but its stylesheet is built from
 * `style-guide.html` and `src/js/` alone, so the class names in these two
 * examples are also what keeps the embed's rules in `style-guide.css`.
 *
 * Toggle clicks are handled by delegation, so a dialog that `modal.js` imports
 * into a secondary page later needs no setup call.
 *
 * ## When the frame loads
 *
 * Never on page load. A visible mount loads when it scrolls within 600px of
 * the viewport; a hidden one when its toggle opens it. Closing the contact
 * dialog (`contact:close`) collapses its panel and removes the frame, so every
 * open starts fresh and a closed dialog keeps no third-party frame alive.
 *
 * ## Warming, so a click doesn't wait
 *
 * Calendly's scheduler takes a second or two to boot, which the visitor used to
 * spend watching the loading note. Two steps move that off the click. Both wait
 * for the page to have loaded and the browser to be idle, so neither competes
 * with the page itself, and neither reaches Calendly for a visitor who is not
 * being offered a calendar (`data-booked`, below):
 *
 * - a `preconnect` to Calendly, which opens the connection — DNS, TCP, TLS —
 *   and fetches nothing: no page of Calendly's loads, so none of its cookies
 *   are set. Skipped once a mount has loaded, which is a real connection
 *   already.
 * - the contact dialog's booking panel is **loaded while still hidden**, on
 *   `contact:open`. Its toggle lives inside the dialog, so the dialog opening
 *   is the earliest a visitor can be reaching for it. `contact:close` takes the
 *   frame out again, so each open warms afresh — and a dialog closed before the
 *   idle moment arrives is never warmed at all.
 *
 * What the hidden frame buys is the network, not the drawing. It fetches
 * Calendly's page, its stylesheet and script chunks and its `initial_settings`
 * availability call, then stops: Chromium throttles rendering in a frame that
 * isn't visible, so it reports no height while it waits, and the layout its
 * zero width would otherwise imply decides nothing — it lays out at the panel's
 * real width once the panel opens. Measured on the homepage 2026-09-18 — two
 * sets of three runs, clicking "Book time with us" two seconds after opening the
 * dialog — the calendar arrived in a median 1.4s warmed against 4.5s cold. The
 * loading note still shows for a moment; what has gone is the wait for the
 * fetching. Calendly's own boot time is variable, so treat these as a shape
 * rather than a promise.
 *
 * The `#schedule` band needs none of this — it already loads 600px before it
 * reaches the viewport.
 *
 * ## Messages from the frame
 *
 * Only messages whose origin is `https://calendly.com` **and** whose source is
 * one of our frames are read (so two embeds on a page never resize each
 * other, the limit Calendly's own script has). `calendly.page_height` sizes
 * the frame. `calendly.date_and_time_selected` marks a change of step, which
 * replaces the frame's content from the top: the embed is scrolled back into
 * view if its top has left the screen.
 *
 * ## The site's own thanks
 *
 * `calendly.event_scheduled` means the booking is made. If the mount authors a
 * confirmation —
 *
 * ```html
 * <div class="calendly-booked" data-calendly-booked role="status" hidden>…</div>
 * ```
 *
 * — the frame is removed and that panel shown and focused in its place, so the
 * visitor ends on the site's thanks (the beacon's light comes on, as for a
 * sent message) and not on Calendly's confirmation and sign-up prompt. A
 * `calendly:booked` event then bubbles from the mount; `modal.js` uses it to
 * tidy the dialog around the panel. Calendly tells the page *that* a booking
 * was made, never which slot (the payload is two API URIs the browser can't
 * read), so the panel's copy points at Calendly's confirmation email for the
 * date and time. A mount with no such panel keeps Calendly's own confirmation.
 *
 * ## Once booked, not asked again
 *
 * A booking is also remembered: `localStorage` gets {@link BOOKED_KEY} with the
 * time, and for {@link BOOKED_TTL} the site stops inviting that visitor to
 * book. `data-booked` on `<html>` is the switch and `src/input.css` does the
 * hiding: every `[data-calendly]` mount (bar the one showing its thanks), every
 * `[data-calendly-toggle]`, and anything marked `data-calendly-offer` (the
 * success panel's "Book time with us"). Copy marked `data-calendly-after`,
 * authored `hidden`, stands in where a sentence needs rewording. It is a
 * stylesheet rule so that a dialog `modal.js` imports later needs no setup.
 * Only the fact and the time of a booking are stored, never its details.
 *
 * The footer's Calendly icon stays, and it still has to lead somewhere. A link
 * to `#schedule` is an explicit ask for the calendar, so following one brings
 * everything back for that page view: a click on this page, or arriving at
 * `index.html#schedule` from another page (a fresh navigation only, since a
 * reload or the back button lands on that URL without anyone asking). The
 * record is left alone, so the next page view is quiet again.
 *
 * Calendly's cookie notice is deliberately **not** suppressed
 * (`hide_gdpr_banner`): hiding it would make this site responsible for
 * consent to Calendly's cookies.
 *
 * Loaded as an ES module on every page that carries the contact dialog.
 *
 * @module calendly
 */

/** The only origin this module will frame or listen to. @type {string} */
const CALENDLY_ORIGIN = 'https://calendly.com'; // Calendly's scheduling pages
/** Shortest height worth acting on, in px: Calendly reports a few px while it boots, and those are skipped. @type {number} */
const MIN_HEIGHT = 480; // floor for a reported height
/** Tallest the frame may be sized, in px. @type {number} */
const MAX_HEIGHT = 2400; // ceiling for a reported height
/** Campaign parameters passed from the page's URL to the booking, as Calendly's script does. @type {string[]} */
const UTM_KEYS = ['utm_campaign', 'utm_source', 'utm_medium', 'utm_content', 'utm_term']; // the five UTM fields

/** `localStorage` key recording that this visitor booked, and when (`{ at: ms }`). @type {string} */
const BOOKED_KEY = 'cha-booked'; // name of the localStorage entry
/** How long a booking keeps the invitations to book out of the way, in ms (30 days). @type {number} */
const BOOKED_TTL = 30 * 24 * 60 * 60 * 1000; // by then the call has happened, and another may be wanted
/** The fragment naming the booking band: a link to it is an ask for the calendar. @type {string} */
const BAND_HASH = '#schedule'; // the closing band on the homepage
/** How long to wait for an idle moment before warming anyway, in ms. @type {number} */
const IDLE_TIMEOUT = 500; // a page that stays busy shouldn't put the warming off for ever

/** `<html>`, which carries `data-booked` for the stylesheet. @type {HTMLElement} */
const root = document.documentElement; // where the switch lives
/** Every mount with a frame in it, and that frame. @type {Map<HTMLElement, HTMLIFrameElement>} */
const live = new Map(); // nothing loaded yet
/** Whether anything has opened a connection to Calendly yet. @type {boolean} */
let warmed = false; // no hint sent and no frame loaded
/** Whether the contact dialog is open, so warming that arrives after it closes can be dropped. @type {boolean} */
let dialogOpen = false; // it starts closed

/**
 * Whether this visitor booked within the last {@link BOOKED_TTL}. An expired,
 * future-dated or unreadable record is deleted, as `theme.js` does with its own.
 *
 * @returns {boolean} `true` while a booking is remembered.
 */
function hasBooked() { // define hasBooked()
  try { // localStorage can throw when blocked
    const raw = localStorage.getItem(BOOKED_KEY); // read the saved text
    if (!raw) return false; // nothing saved
    let saved = null; // the parsed record, if any
    try { saved = JSON.parse(raw); } catch {} // decode it; bad JSON falls through to removal
    const age = saved ? Date.now() - saved.at : NaN; // NaN fails both comparisons below
    if (age >= 0 && age < BOOKED_TTL) return true; // fresh, and not from the future
    localStorage.removeItem(BOOKED_KEY); // expired or invalid: clean it up
  } catch {} // storage unavailable: nothing is remembered
  return false; // no booking on record
}

/**
 * Records that a booking was just made: the time, and nothing about it.
 *
 * @returns {void}
 */
function rememberBooked() { // define rememberBooked()
  try { localStorage.setItem(BOOKED_KEY, JSON.stringify({ at: Date.now() })); } catch {} // save it; ignore storage errors
}

/**
 * Whether the visitor got here by following a link to the booking band (the
 * footer's Calendly icon on another page). Only a fresh navigation counts: a
 * reload or the back button lands on the same URL without anyone asking.
 *
 * @returns {boolean} `true` if this page view began as an ask for the calendar.
 */
function arrivedAsking() { // define arrivedAsking()
  if (window.location.hash !== BAND_HASH) return false; // some other address: no
  const nav = /** @type {PerformanceNavigationTiming|undefined} */ (performance.getEntriesByType?.('navigation')[0]); // how this page view began
  return !nav || nav.type === 'navigate'; // a followed link or typed address (or a browser that can't say)
}

/**
 * The URL to frame: the mount's Calendly link plus the two parameters that
 * tell Calendly it is an inline embed (which is what makes it post its height
 * and booking events to this page), and any campaign parameters on the page.
 * A query string authored on the mount is kept: the pages pass
 * `hide_event_type_details=1`, which drops Calendly's own event header from
 * the calendar step (it returns on the final step) and with it ~180px.
 *
 * @param {string|undefined} base The mount's `data-calendly` value.
 * @returns {string|null} The frame URL, or null if `base` isn't a Calendly URL.
 */
function embedUrl(base) { // define embedUrl(base)
  let url; // the parsed link
  try { // a malformed value throws
    url = new URL(base); // parse it
  } catch { // not a URL:
    return null; // nothing to frame
  }
  if (url.origin !== CALENDLY_ORIGIN) return null; // only ever frame Calendly
  const here = new URLSearchParams(window.location.search); // this page's query string
  UTM_KEYS.forEach((key) => { // for each campaign field:
    if (here.has(key)) url.searchParams.set(key, here.get(key)); // pass it on to the booking
  });
  url.searchParams.set('embed_domain', window.location.host); // who is embedding
  url.searchParams.set('embed_type', 'Inline'); // and how
  return url.href; // the finished URL
}

/**
 * Runs `fn` once the page has finished loading and the browser has a spare
 * moment — immediately idle if loading is already done. Warming is worth
 * nothing if it slows the page down to do it, so nothing here jumps the queue;
 * {@link IDLE_TIMEOUT} keeps a page that never goes quiet from putting it off
 * for ever. (Safari only got `requestIdleCallback` in 18.2, hence the timer.)
 *
 * @param {function(): void} fn Callback to run.
 * @returns {void}
 */
function whenIdle(fn) { // define whenIdle(fn)
  const soon = () => { // what to do once the page has loaded:
    if (window.requestIdleCallback) window.requestIdleCallback(fn, { timeout: IDLE_TIMEOUT }); // wait for a spare moment
    else setTimeout(fn, 200); // no such thing here: just give the load a moment to settle
  };
  if (document.readyState === 'complete') soon(); // the load event has been and gone
  else window.addEventListener('load', soon, { once: true }); // otherwise wait for it
}

/**
 * Opens the connection to Calendly without fetching anything from it, so the
 * first frame starts at its HTTP request instead of three round trips before
 * it. A `preconnect` is DNS, TCP and TLS only: no page of Calendly's is
 * loaded, so none of its cookies are set, and because nothing is fetched there
 * is no CSP directive for it to pass (the `frame-src` grant still covers the
 * frame itself). No `crossorigin` — the frame's navigation isn't a CORS
 * request, and an anonymous connection wouldn't be the one it reuses.
 *
 * @returns {void}
 */
function warmConnection() { // define warmConnection()
  if (warmed) return; // a hint or a frame has already been sent: stop
  warmed = true; // once per page view
  const link = document.createElement('link'); // the hint
  link.rel = 'preconnect'; // open the connection, fetch nothing
  link.href = CALENDLY_ORIGIN; // to Calendly
  document.head.append(link); // the browser acts on it as it lands
}

/**
 * Puts the scheduler into a mount: a loading note, then the frame. The
 * authored fallback stays in the DOM (hidden by `.is-live`) for print.
 * Does nothing if the mount is already live or names no Calendly URL.
 *
 * @param {HTMLElement} mount A `[data-calendly]` element.
 * @returns {void}
 */
function load(mount) { // define load(mount)
  if (live.has(mount)) return; // already showing: stop
  const src = embedUrl(mount.dataset.calendly); // build the frame URL
  if (!src) return; // no usable URL: leave the fallback link in place
  warmed = true; // this is the real connection: a later preconnect would add nothing

  const loading = document.createElement('p'); // the loading note
  loading.className = 'calendly-loading'; // styled in src/input.css
  loading.setAttribute('role', 'status'); // announced politely
  loading.textContent = 'Loading the booking calendar…'; // what it says

  const frame = document.createElement('iframe'); // the scheduler
  frame.className = 'calendly-frame'; // styled in src/input.css
  frame.title = mount.dataset.calendlyTitle || 'Book a consultation (Calendly scheduler)'; // its accessible name
  frame.addEventListener('load', () => loading.remove(), { once: true }); // Calendly's own page takes over from the note
  frame.src = src; // start loading

  mount.classList.add('calendly-embed', 'is-live'); // hide the fallback, show the way out
  mount.prepend(loading, frame); // above the authored paragraphs
  live.set(mount, frame); // remember it for the message handler
}

/**
 * Takes the scheduler back out of a mount and restores its authored state.
 *
 * @param {HTMLElement} mount A `[data-calendly]` element.
 * @returns {void}
 */
function unload(mount) { // define unload(mount)
  const frame = live.get(mount); // the mount's frame, if any
  if (!frame) return; // nothing loaded: stop
  live.delete(mount); // forget it
  frame.remove(); // remove the frame
  mount.querySelector('.calendly-loading')?.remove(); // and the note, if still there
  mount.classList.remove('is-live'); // show the fallback again
}

/**
 * Swaps a mount's frame for its authored thanks, once Calendly reports the
 * booking. Does nothing on a mount that authors no `[data-calendly-booked]`
 * panel, which leaves Calendly's own confirmation showing in the frame.
 *
 * @param {HTMLElement} mount A `[data-calendly]` element.
 * @returns {boolean} `true` if the thanks was shown, `false` if the mount has none.
 */
function showBooked(mount) { // define showBooked(mount)
  const panel = /** @type {HTMLElement|null} */ (mount.querySelector('[data-calendly-booked]')); // the authored thanks
  if (!panel) return false; // none authored: Calendly's confirmation stays
  unload(mount); // take the frame out
  mount.classList.add('is-booked'); // keeps the fallback link hidden now the frame is gone
  panel.hidden = false; // show the thanks (unhiding is what starts the beacon's light)
  panel.setAttribute('tabindex', '-1'); // make it focusable by script
  panel.focus({ preventScroll: true }); // focus it so it's announced
  panel.scrollIntoView({ block: 'center' }); // the tall frame is gone: bring the thanks into view
  mount.dispatchEvent(new CustomEvent('calendly:booked', { bubbles: true })); // let the dialog tidy up around it
  return true; // shown
}

/**
 * Puts a mount back to its authored state after a booking: thanks hidden, and
 * the fallback link allowed to show again.
 *
 * @param {HTMLElement} mount A `[data-calendly]` element.
 * @returns {void}
 */
function resetBooked(mount) { // define resetBooked(mount)
  mount.classList.remove('is-booked'); // back to the authored state
  const panel = /** @type {HTMLElement|null} */ (mount.querySelector('[data-calendly-booked]')); // the authored thanks
  if (panel) panel.hidden = true; // hide it again
}

/**
 * A booking was just made in `mount`: remember it, and stop inviting this
 * visitor to book. The mount itself stays, because it holds the confirmation.
 *
 * @param {HTMLElement} mount The `[data-calendly]` element the booking came from.
 * @returns {void}
 */
function noteBooked(mount) { // define noteBooked(mount)
  rememberBooked(); // for the page views to come
  mount.classList.add('is-booked'); // the one mount the stylesheet leaves showing
  root.setAttribute('data-booked', ''); // the other invitations step aside now (src/input.css)
  [...live.keys()].forEach((other) => { // a calendar elsewhere on the page goes with them:
    if (other !== mount) unload(other); // no third-party frame left alive out of sight
  });
}

/**
 * Brings the invitations to book back for this page view, because the visitor
 * asked for the calendar. The record stays, so the next page view is quiet again.
 *
 * @returns {void}
 */
function showAgain() { // define showAgain()
  if (!root.hasAttribute('data-booked')) return; // nothing is held back: stop
  root.removeAttribute('data-booked'); // the stylesheet lets everything show
  mounts.forEach((mount) => { // for each mount that shows without being asked (the band's):
    resetBooked(mount); // a thanks from a booking made this page view gives way…
    load(mount); // …to the calendar, straight away: it is what they came for
  });
}

/**
 * Opens or closes a disclosure button's scheduler panel.
 *
 * @param {HTMLElement} btn A `[data-calendly-toggle]` button.
 * @param {boolean} open `true` to show the panel (and load it), `false` to hide it.
 * @returns {HTMLElement|null} The panel, or null if `aria-controls` names nothing.
 */
function setOpen(btn, open) { // define setOpen(btn, open)
  const panel = document.getElementById(btn.getAttribute('aria-controls')); // the panel it controls
  if (!panel) return null; // no such panel: stop
  btn.setAttribute('aria-expanded', String(open)); // tell assistive tech
  panel.hidden = !open; // show or hide it
  if (open) load(panel); // asked for: load now
  return panel; // for the caller
}

/**
 * Loads the contact dialog's booking panel while it is still hidden, so the
 * toggle only has to reveal it. Called when the dialog opens, which is the
 * earliest a visitor can be reaching for a toggle that lives inside it.
 *
 * @returns {void}
 */
function warmDialog() { // define warmDialog()
  if (!dialogOpen) return; // closed again while this waited for an idle moment: a closed dialog keeps no frame alive
  if (root.hasAttribute('data-booked')) return; // no calendar is on offer: don't fetch one nobody is shown
  document.querySelectorAll('#contact-modal [data-calendly-toggle]').forEach((btn) => { // for each toggle in the dialog:
    const panel = document.getElementById(btn.getAttribute('aria-controls')); // the panel it controls
    if (panel) load(panel); // load it where it stands, still hidden (load() ignores one already live)
  });
}

// A visitor who has booked isn't asked again, unless this page view began as
// an ask for the calendar.
if (hasBooked() && !arrivedAsking()) root.setAttribute('data-booked', ''); // hold the invitations back (src/input.css)

// A visible mount loads when the visitor scrolls near it — not on page load, so
// Calendly is fetched only by visitors who reach the calendar. A mount held
// back by `data-booked` has no box, so it never comes into range; showAgain()
// loads it if it is asked for.
const mounts = [...document.querySelectorAll('[data-calendly]')].filter((mount) => !mount.hidden); // hidden ones wait for their toggle
if ('IntersectionObserver' in window) { // if the browser can watch visibility:
  const near = new IntersectionObserver((entries) => { // watch for a mount…
    entries.forEach((entry) => { // for each change:
      if (!entry.isIntersecting) return; // …not yet in range: keep waiting
      near.unobserve(entry.target); // in range: stop watching it…
      load(/** @type {HTMLElement} */ (entry.target)); // …and load
    });
  }, { rootMargin: '600px 0px' }); // count "in range" from 600px out
  mounts.forEach((mount) => near.observe(mount)); // watch each
} else if (!root.hasAttribute('data-booked')) { // otherwise, unless they're held back:
  mounts.forEach(load); // load straight away
}

// With the page loaded and the browser idle, open the connection Calendly will
// need, so whichever calendar the visitor reaches first doesn't start by
// looking up a name. Nothing is fetched, and nothing at all happens for a
// visitor who isn't being offered a calendar, or one whose band has already
// loaded and so has a real connection.
if (!root.hasAttribute('data-booked')) whenIdle(() => { // unless the invitations are held back:
  if (!live.size) warmConnection(); // no frame has beaten us to it: open the connection
});

// Disclosure buttons, by delegation: the contact dialog may be imported into
// the page after this runs (see modal.js).
document.addEventListener('click', (e) => { // on any click:
  const btn = /** @type {Element} */ (e.target).closest?.('[data-calendly-toggle]'); // a toggle, or something inside one?
  if (btn) setOpen(/** @type {HTMLElement} */ (btn), btn.getAttribute('aria-expanded') !== 'true'); // flip it
  if (/** @type {Element} */ (e.target).closest?.(`a[href$="${BAND_HASH}"]`)) showAgain(); // a link to the band (the footer's icon): they asked, so nothing is held back
});

// The dialog opened: with the entrance out of the way, load its booking panel
// behind the scenes, so "Book time with us" opens on a calendar instead of a
// loading note. The click loads it in any case, so a browser that never goes
// idle only ends up where it was before.
document.addEventListener('contact:open', () => { // when modal.js reports an open:
  dialogOpen = true; // it's open now
  whenIdle(warmDialog); // load its calendar once the browser has a moment
});

// The dialog closed: collapse its panel and drop the frame, so the next open
// starts from the top like the form does.
document.addEventListener('contact:close', () => { // when modal.js reports a close:
  dialogOpen = false; // closed: any warming still waiting for an idle moment is dropped
  document.querySelectorAll('#contact-modal [data-calendly-toggle]').forEach((btn) => { // for each toggle in the dialog:
    const panel = setOpen(/** @type {HTMLElement} */ (btn), false); // collapse it
    if (!panel) return; // names no panel: nothing more to do
    unload(panel); // remove its frame
    resetBooked(panel); // and any thanks from a booking made this time
  });
});

// Size and position from the frame's own reports.
window.addEventListener('message', (e) => { // on any cross-document message:
  if (e.origin !== CALENDLY_ORIGIN) return; // not Calendly: ignore
  const name = e.data?.event; // Calendly's event name
  if (typeof name !== 'string') return; // not one of its events: ignore
  for (const [mount, frame] of live) { // find the mount…
    if (frame.contentWindow !== e.source) continue; // …whose frame sent this
    if (name === 'calendly.page_height') { // the framed page's height changed:
      const height = parseFloat(e.data.payload?.height); // e.g. "660px"
      if (height >= MIN_HEIGHT) frame.style.height = `${Math.min(height, MAX_HEIGHT)}px`; // fit the frame to it (a boot-time sliver or junk keeps the current height)
    } else if (name === 'calendly.date_and_time_selected' || name === 'calendly.event_scheduled') { // a new step, or the booking is made:
      if (name === 'calendly.event_scheduled') { // booked:
        noteBooked(mount); // remember it, and stop asking this visitor to book
        if (showBooked(mount)) return; // end on the site's thanks, not Calendly's
      }
      if (mount.getBoundingClientRect().top < 0) mount.scrollIntoView({ block: 'start' }); // else the content was replaced from the top: bring it back if it scrolled away
    }
    return; // handled
  }
});