292 lines
13 KiB
JavaScript
292 lines
13 KiB
JavaScript
// 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
|
||
}
|
||
}); |