assets_js_controllers_detail-tabs-controller.js
import { Controller } from "@hotwired/stimulus";
/**
* DetailTabs Stimulus Controller
*
* Manages tabbed interfaces with URL state management and browser history integration.
* Automatically handles tab activation, URL updates, and Turbo frame reloading for
* dynamic content management.
*
* Features:
* - URL-based tab selection and state persistence
* - Browser history integration with pushState
* - Automatic first tab activation
* - Turbo frame reloading on tab change
* - Configurable URL update behavior
* - Scroll management for better UX
*
* Values:
* - updateUrl: Boolean (default: true) - Whether to update URL on tab change
*
* Targets:
* - tabBtn: Tab button elements for navigation
* - tabContent: Tab content panels (optional)
*
* Usage:
* <div data-controller="detail-tabs" data-detail-tabs-update-url-value="true">
* <nav>
* <button data-detail-tabs-target="tabBtn" id="nav-info-tab">Info</button>
* <button data-detail-tabs-target="tabBtn" id="nav-history-tab">History</button>
* </nav>
* <turbo-frame id="info-frame">...</turbo-frame>
* <turbo-frame id="history-frame">...</turbo-frame>
* </div>
*/
class DetailTabsController extends Controller {
static targets = ["tabBtn", "tabContent"]
static values = { updateUrl: { type: Boolean, default: true } }
foundFirst = false;
/**
* Handle tab button connection to DOM
* Sets up tab activation based on URL parameters or defaults to first tab
*
* @param {HTMLElement} event - Connected tab button element
*/
tabBtnTargetConnected(event) {
var tab = event.id.replace('nav-', '').replace('-tab', '');
var urlTab = KMP_utils.urlParam('tab');
if (urlTab) {
if (tab == urlTab) {
event.click();
this.foundFirst = true;
window.scrollTo(0, 0);
}
} else {
if (!this.foundFirst) {
// Get the first tab based on CSS order, not DOM order
const firstTab = this.getFirstTabByOrder();
if (firstTab) {
firstTab.click();
this.foundFirst = true;
}
window.scrollTo(0, 0);
}
}
event.addEventListener('click', this.tabBtnClicked.bind(this));
}
/**
* Get the first tab button based on CSS order attribute
* Respects the data-tab-order attribute for mixed plugin/template tabs
*
* @returns {HTMLElement|null} First tab button by order, or null if none found
*/
getFirstTabByOrder() {
if (this.tabBtnTargets.length === 0) {
return null;
}
// Sort tabs by their order attribute (lower number = first)
const sortedTabs = [...this.tabBtnTargets].sort((a, b) => {
const orderA = parseInt(a.dataset.tabOrder || '999', 10);
const orderB = parseInt(b.dataset.tabOrder || '999', 10);
return orderA - orderB;
});
return sortedTabs[0];
}
/**
* Handle tab button clicks
* Updates URL history and triggers frame reloading for dynamic content
*
* @param {Event} event - Click event from tab button
*/
tabBtnClicked(event) {
// Get first tab based on order, not DOM position
const firstTab = this.getFirstTabByOrder();
const firstTabId = firstTab?.id || this.tabBtnTargets[0]?.id;
var eventTabId = event.target.id;
var tab = event.target.id.replace('nav-', '').replace('-tab', '');
if (this.updateUrlValue) {
if (firstTabId != eventTabId) {
window.history.pushState({}, '', '?tab=' + tab);
} else {
//only push state if there is a tab in the querystring
var urlTab = KMP_utils.urlParam('tab');
if (urlTab) {
window.history.pushState({}, '', window.location.pathname);
}
}
}
var frame = document.getElementById(tab + '-frame');
if (frame) {
// Check if frame has been loaded before - if it has a src and is complete, reload it
// Otherwise, let the lazy loading handle the initial load
if (frame.loaded || (frame.complete && !frame.hasAttribute('loading'))) {
frame.reload();
}
}
}
/**
* Handle tab button disconnection from DOM
* Cleans up event listeners to prevent memory leaks
*
* @param {HTMLElement} event - Disconnected tab button element
*/
tabBtnTargetDisconnected(event) {
event.removeEventListener('click', this.tabBtnClicked.bind(this));
}
}
if (!window.Controllers) {
window.Controllers = {}
}
window.Controllers["detail-tabs"] = DetailTabsController;