/** * @file * JavaScript behaviors for the druid theme. * * ───────────────────────────────────────────────────────────────────────── * What Drupal.behaviors solves * ───────────────────────────────────────────────────────────────────────── * * Drupal swaps DOM fragments at runtime — form errors, AJAX-loaded views, * contextual links, modal dialogs, and inline edits all inject new HTML * into the page after the initial load. A plain `DOMContentLoaded` handler * runs once and misses everything that arrives later. * * The behaviors system runs your `attach()` on every fragment Drupal * inserts (including the initial document), so the same code initializes * new content automatically. Use `once()` to make sure an element is only * initialized one time even if `attach()` is invoked repeatedly. * * ───────────────────────────────────────────────────────────────────────── * Pattern * ───────────────────────────────────────────────────────────────────────── * * Drupal.behaviors.druidSomething = { * attach(context, settings) { * once('druid-something', '.selector', context).forEach((el) => { * // initialize el — add listeners, hydrate state, etc. * }); * }, * detach(context, settings, trigger) { * // OPTIONAL — clean up listeners/observers when Drupal removes the * // fragment. Skip if you don't allocate anything that leaks. * }, * }; * * - `context` is the DOM subtree being attached. On the initial page load * it's `document`; on AJAX updates it's just the newly inserted fragment. * Always scope queries to `context`, never to `document`, or you'll * re-initialize the whole page. * - The first arg to `once()` is a unique string ID for this behavior. The * second is a CSS selector. `once()` returns an array of elements that * haven't been initialized yet (it marks them with a data attribute so * subsequent calls skip them). * - `settings` is the JS object Drupal exposes via `drupalSettings` — use * it to read PHP-side configuration (`drupalSettings.druid.someValue`). * * Naming: prefix behavior keys with `druid` so they don't collide with * core/contrib behaviors. The `once()` ID should match the behavior name * minus the prefix, lowercased and kebab-case. * * Library wiring: this file is registered in druid.libraries.yml under the * `behaviors` library, with `core/drupal` and `core/once` as dependencies. * If you split JS into multiple files, either add them under the same * library or define new libraries and attach them via `libraries-extend` * (for conditional loading) or the always-on list in druid.info.yml. * * Reference: https://www.drupal.org/docs/develop/standards/javascript */ ((Drupal, once) => { // Behaviors go here. Example skeleton — uncomment and adapt: // // Drupal.behaviors.druidExample = { // attach(context) { // once('druid-example', '[data-druid-example]', context).forEach((el) => { // el.addEventListener('click', () => { // el.classList.toggle('is-active'); // }); // }); // }, // }; Drupal.behaviors.mainMenuToggle = { attach(context) { once( "dropdown-menu-toggle", "nav .menu li.menu-item--expanded > a", context ).forEach((link) => { let touched = false; const handler = function (e) { // Prevent duplicate firing (touch + click) if (e.type === "click" && touched) { touched = false; return; } if (e.type === "touchstart") { touched = true; } const parent = this.parentElement; const submenu = parent.querySelector("ul"); // Only toggle if submenu exists if (submenu) { e.preventDefault(); parent.classList.toggle("open"); } }; // Mobile-first interaction link.addEventListener("touchstart", handler, { passive: true }); // Desktop + accessibility fallback link.addEventListener("click", handler); }); }, }; })(Drupal, once); ((Drupal, once) => { Drupal.behaviors.sidebarMenuDropdown = { attach(context) { once( "sidebar-menu-dropdown", ".block-menu .menu-item--expanded", context ).forEach((item) => { const link = item.querySelector("a"); const submenu = item.querySelector("ul"); if (!submenu || !link) return; // ✅ Create caret element const caret = document.createElement("span"); caret.className = "menu-caret"; caret.setAttribute("aria-expanded", "false"); // Insert caret after link link.insertAdjacentElement("afterend", caret); let touched = false; const toggleMenu = (e) => { // Prevent touch + click double fire if (e.type === "click" && touched) { touched = false; return; } if (e.type === "touchstart") { touched = true; } e.preventDefault(); e.stopPropagation(); const isOpen = item.classList.toggle("open"); caret.setAttribute("aria-expanded", isOpen); }; // ✅ Only caret toggles menu (link still navigates) caret.addEventListener("touchstart", toggleMenu, { passive: false }); caret.addEventListener("click", toggleMenu); }); }, }; })(Drupal, once);