custom_subtabs/custom_subtabs/public/js/custom_subtabs.js

292 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Shared function for setting up subtabs
function setup_custom_subtabs(wrapper, currentDoctype) {
if (!wrapper || !wrapper.length) {
return;
}
const tabs = wrapper.find("ul.form-tabs > li.nav-item");
const hierarchy = frappe.boot.tab_hierarchy?.[currentDoctype];
if (!hierarchy) {
return;
}
const subtabs = hierarchy.subtabs || {};
if (Object.keys(subtabs).length === 0) {
return;
}
// tab_hierarchy хранит СЫРЫЕ (английские) label'ы, а Frappe рисует вкладки
// через __(), т.е. в DOM текст уже переведён (AZ/RU). Поэтому сопоставлять
// вкладку из DOM с ключом иерархии нужно через перевод этого ключа.
// Внутренние структуры (childToParentMap, data-subtab-name) оставляем на
// английском — переводим только на границе сравнения с DOM.
const tr = (s) => (typeof __ === 'function' ? __(s) : s);
// Helper function to find tab link element
function getTabLink(tabElement) {
const $tab = $(tabElement);
let link = $tab.find("button.nav-link");
if (!link.length) {
link = $tab.find("a.nav-link");
}
return link;
}
// Check if already processed
if (wrapper.attr('data-subtabs-processed') === 'true') {
return;
}
// Build parent-child map
const childToParentMap = {};
for (const [parentTab, children] of Object.entries(subtabs)) {
children.forEach(child => {
childToParentMap[child] = parentTab;
});
}
// Process subtabs - move them into containers
tabs.each((index, tab) => {
const tabLabel = $(tab).text().trim();
// tabLabel — переведённый текст из DOM; ключ иерархии — английский.
// Находим английский ключ-родитель, чей перевод совпал с DOM.
const parentKey = Object.keys(subtabs).find((k) => tr(k) === tabLabel);
if (parentKey) {
const tabLink = getTabLink(tab);
const tabContentId = tabLink.attr("aria-controls");
const tabContent = $(`#${tabContentId}`);
if (!tabContent.length) {
return;
}
// Find container
let sectionContainer = tabContent.find(".form-section.card-section");
if (!sectionContainer.length) {
sectionContainer = tabContent.find(".form-section");
}
if (!sectionContainer.length) {
sectionContainer = tabContent.children().first();
}
if (!sectionContainer.length) {
sectionContainer = tabContent;
}
let subtabContainer = sectionContainer.find('.sub-tab-container');
if (!subtabContainer.length) {
subtabContainer = $("<div class='sub-tab-container nav' data-parent-id='" + tabContentId + "'></div>");
sectionContainer.prepend(subtabContainer);
}
subtabs[parentKey].forEach((subtab) => {
// subtab — английский; в DOM ищем по его переводу.
const subtabElement = tabs.filter((_, el) => $(el).text().trim() === tr(subtab));
if (subtabElement.length) {
const subtabClone = subtabElement.clone(true);
subtabClone.addClass("sub-tab");
// data-subtab-name держим английским — childToParentMap/ensureParentChainVisible на нём.
subtabClone.attr("data-subtab-name", subtab);
const clonedLink = getTabLink(subtabClone);
clonedLink.removeAttr("href");
clonedLink.attr("data-target-id", clonedLink.attr("aria-controls"));
// clone(true) копирует прямой обработчик Frappe (tab.js: nav-link.on('click') ->
// set_active()), который деактивирует РОДИТЕЛЬСКУЮ панель — а в ней лежат сами
// кнопки субтабов. Снимаем его: кликами по субтабам управляем своей делегацией.
clonedLink.off("click");
subtabClone.attr("data-parent-id", tabContentId);
subtabContainer.append(subtabClone);
subtabElement.remove();
}
});
}
});
// Function to show parent chain
function ensureParentChainVisible(subtabName) {
let currentChild = subtabName;
const parentsToShow = [];
// Collect parent chain
while (childToParentMap[currentChild]) {
const parentName = childToParentMap[currentChild];
parentsToShow.push(parentName);
// Show all siblings - НЕ используем .show(), используем CSS класс
const siblings = subtabs[parentName] || [];
siblings.forEach(sibling => {
const siblingElement = wrapper.find(`.sub-tab[data-subtab-name="${sibling}"]`);
siblingElement.css('display', ''); // Убираем inline style
});
currentChild = parentName;
}
// Show all parent content
parentsToShow.forEach(parentName => {
const parentTabContentId = `${currentDoctype.toLowerCase().replace(/\s+/g, '-')}-${parentName.toLowerCase().replace(/\s+/g, '_')}_tab`;
const parentContent = $(`#${parentTabContentId}`);
if (parentContent.length && !parentContent.hasClass('active')) {
parentContent.addClass('active show').css('display', 'block');
}
});
}
// Handle subtab clicks
wrapper.on("click.customsubtabs", ".sub-tab button.nav-link, .sub-tab a.nav-link", function(event) {
event.preventDefault();
event.stopPropagation();
const clickedButton = $(this);
const clickedSubtab = clickedButton.closest(".sub-tab");
const subtabName = clickedSubtab.attr("data-subtab-name");
const targetId = clickedButton.attr("data-target-id") || clickedButton.attr("aria-controls");
const parentContainerId = clickedSubtab.attr("data-parent-id");
// Hide all tabs and content
wrapper.find(".nav-item").removeClass("active");
wrapper.find("button.nav-link, a.nav-link").removeClass("active").attr("aria-selected", "false");
wrapper.find(".tab-pane").removeClass("active show");
// Скрыть все контейнеры субтабов, кроме родительского
wrapper.find(".sub-tab-container").each(function() {
const containerParentId = $(this).attr("data-parent-id");
if (containerParentId !== parentContainerId) {
$(this).addClass("subtab-hidden");
} else {
$(this).removeClass("subtab-hidden");
}
});
// Activate subtab
clickedSubtab.addClass("active");
clickedButton.addClass("active").attr("aria-selected", "true");
// Show subtab content
const targetPane = $(`#${targetId}`);
if (targetPane.length) {
targetPane.addClass("active show");
}
// Надёжно вернуть РОДИТЕЛЬСКУЮ панель (в ней живут кнопки субтабов) по её реальному
// id из data-parent-id — НЕ реконструируя id из перевода label'а. Это и есть защита
// от «все субтабы пропали»: панель с кнопками всегда восстанавливается.
const parentPane = $(`#${parentContainerId}`);
if (parentPane.length) {
parentPane.addClass("active show").css("display", "block");
// Панель-контент субтаба может стоять в DOM РАНЬШЕ родительской панели
// (зависит от field_order доктайпа). Тогда бар субтабов визуально уезжает ВНИЗ
// под контент и кажется, что субтабы пропали. Ставим активный контент сразу
// после родительской панели — бар всегда оказывается над ним.
if (targetPane.length) {
parentPane.after(targetPane);
}
}
// Show parent chain (для вложенности глубже одного уровня)
ensureParentChainVisible(subtabName);
});
// Function to setup main tab handler
function setupMainTabHandler() {
// Для Frappe v16 используем MutationObserver вместо событий Bootstrap
// так как shown.bs.tab не генерируется
// Удаляем старый observer если есть
if (wrapper.data('subtabs-observer')) {
wrapper.data('subtabs-observer').disconnect();
}
// Создаем MutationObserver для отслеживания изменений класса active
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const button = mutation.target;
const $button = $(button);
// Проверяем что это кнопка таба и она стала активной
if ($button.hasClass('nav-link') && $button.hasClass('active')) {
const li = $button.closest("li.nav-item");
// Игнорируем субтабы
if (li.hasClass("sub-tab")) {
return;
}
const tabId = button.getAttribute("aria-controls");
// ИСПРАВЛЕНИЕ: Убираем active классы со всех субтабов и их контента
wrapper.find(".sub-tab").removeClass("active");
wrapper.find(".sub-tab button.nav-link, .sub-tab a.nav-link")
.removeClass("active")
.attr("aria-selected", "false");
// Скрываем контент всех субтабов
wrapper.find(".sub-tab").each(function() {
const subtabButton = getTabLink($(this));
const subtabContentId = subtabButton.attr("data-target-id") || subtabButton.attr("aria-controls");
if (subtabContentId) {
$(`#${subtabContentId}`).removeClass("active show");
}
});
// Show/hide subtabs
wrapper.find(".sub-tab-container").each(function() {
const containerParentId = $(this).attr("data-parent-id");
if (containerParentId === tabId) {
$(this).removeClass("subtab-hidden");
} else {
$(this).addClass("subtab-hidden");
}
});
}
}
});
});
// Наблюдаем за всеми кнопками табов (не субтабов)
wrapper.find('ul.form-tabs > li.nav-item:not(.sub-tab) button.nav-link, ul.form-tabs > li.nav-item:not(.sub-tab) a.nav-link').each(function() {
observer.observe(this, { attributes: true, attributeFilter: ['class'] });
});
// Сохраняем observer чтобы можно было удалить позже
wrapper.data('subtabs-observer', observer);
}
// Initialize handler
setupMainTabHandler();
// Инициализация: скрыть все контейнеры, кроме активного таба
const activeMainTab = wrapper.find("ul.form-tabs > li.nav-item.active:not(.sub-tab)").first();
const activeTabLink = getTabLink(activeMainTab);
const activeTabId = activeTabLink.attr("aria-controls");
wrapper.find(".sub-tab-container").each(function() {
const containerParentId = $(this).attr("data-parent-id");
if (containerParentId !== activeTabId) {
$(this).addClass("subtab-hidden");
}
});
// Mark as processed
wrapper.attr('data-subtabs-processed', 'true');
}
// Use form refresh event
frappe.ui.form.on('*', {
refresh: function(frm) {
setTimeout(() => {
const wrapper = frm.layout?.wrapper || frm.$wrapper;
setup_custom_subtabs(wrapper, frm.doctype);
}, 500);
},
onload: function(frm) {
// Form onload hook
}
});