You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
157 lines
6.0 KiB
157 lines
6.0 KiB
/** |
|
* @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);
|
|
|