fix: keep subtabs open when Frappe re-activates a tab
Frappe's tab model does not know subtabs exist. Tab.set_active() strips `show active` off every pane except the one it activates, and Frappe calls it on every refresh_sections() — which runs on every refresh_field() and every value change. So editing any field wiped the open subtab: its content appeared for a fraction of a second and vanished. Clicking a subtab now registers its top-level ancestor as the active tab, so Frappe's own bookkeeping stays coherent and keeps re-activating the right tab, and Tab.set_active is wrapped to re-assert the rest of the chain once Frappe is done. The whole ancestor chain has to be restored, not just the leaf: subtabs nest two deep in several declarations, where the top pane holds the first subtab bar, the middle pane holds the second and the leaf holds the content — three panes active at once, which Frappe's one-tab-at-a-time model will never do on its own. This replaces the MutationObserver, which was fighting the same battle from the wrong end: it could not tell a genuine tab switch from a refresh re-asserting the tab already open. Verified: 70 subtabs across 11 doctypes stay visible through refresh_field() and refresh_sections(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7d9fb69f3b
commit
63476b39aa
|
|
@ -0,0 +1,2 @@
|
|||
.sub-tab-container{margin:0!important;padding:0!important;display:flex!important;justify-content:flex-start!important;border-bottom:1px solid #ddd!important}.sub-tab-container .sub-tab{margin:0!important;background:none!important;border:none!important;padding:0!important}.sub-tab-container .sub-tab a,.sub-tab-container .sub-tab button{text-decoration:none!important;display:block!important;padding:6px 15px!important;color:#555!important;border:none!important;font-size:14px!important;font-weight:400!important;margin:0!important;background:transparent!important;box-shadow:none!important}.sub-tab-container .sub-tab a:hover,.sub-tab-container .sub-tab button:hover{color:#000!important}.sub-tab-container .sub-tab.active a,.sub-tab-container .sub-tab.active button{color:#000!important;font-weight:600!important;border-bottom:2px solid #000!important;background-color:transparent!important;border-radius:0!important}.form-section:has(.sub-tab-container){padding-top:0!important;padding-bottom:0!important;margin-bottom:0!important}.form-section .section-body:empty,.form-section .section-body:has(> .form-column > form > .frappe-control[data-fieldname="html_vohk"] > span.tooltip-content:only-child){display:none!important;padding:0!important;margin:0!important;min-height:0!important}.sub-tab-container.nav{display:flex;list-style:none;padding:0;margin:0}.sub-tab-container.nav>li[style*="display: none"]{display:none!important}.sub-tab-container.subtab-hidden{display:none!important;visibility:hidden!important;pointer-events:none!important;height:0!important;overflow:hidden!important}
|
||||
/*# sourceMappingURL=custom_subtabs.bundle.DYDKGLKL.css.map */
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"version": 3,
|
||||
"sources": ["../../../../../../../../tmp/tmp-18495-vJNc0fl1RN65/custom_subtabs/custom_subtabs/public/css/custom_subtabs.bundle.css"],
|
||||
"sources": ["../../../../../../../../tmp/tmp-15812-S7Giyd0UpxCO/custom_subtabs/custom_subtabs/public/css/custom_subtabs.bundle.css"],
|
||||
"sourcesContent": [".sub-tab-container {\n margin: 0 !important;\n padding: 0 !important;\n display: flex !important;\n justify-content: flex-start !important;\n border-bottom: 1px solid #ddd !important;\n}\n\n.sub-tab-container .sub-tab {\n margin: 0 !important;\n background: none !important;\n border: none !important;\n padding: 0 !important;\n}\n\n/* Support both <a> and <button> for v15 and v16 compatibility */\n.sub-tab-container .sub-tab a,\n.sub-tab-container .sub-tab button {\n text-decoration: none !important;\n display: block !important;\n padding: 6px 15px !important;\n color: #555 !important;\n border: none !important;\n font-size: 14px !important;\n font-weight: normal !important;\n margin: 0 !important;\n background: transparent !important;\n box-shadow: none !important;\n}\n\n.sub-tab-container .sub-tab a:hover,\n.sub-tab-container .sub-tab button:hover {\n color: #000 !important;\n}\n\n.sub-tab-container .sub-tab.active a,\n.sub-tab-container .sub-tab.active button {\n color: #000 !important;\n font-weight: 600 !important;\n border-bottom: 2px solid #000 !important;\n background-color: transparent !important;\n border-radius: 0 !important;\n}\n\n/* \u041A\u041B\u042E\u0427\u0415\u0412\u041E\u0415 \u0418\u0421\u041F\u0420\u0410\u0412\u041B\u0415\u041D\u0418\u0415: \u0443\u0431\u0440\u0430\u0442\u044C padding \u0443 form-section \u0441 \u0441\u0443\u0431\u0442\u0430\u0431\u0430\u043C\u0438 */\n.form-section:has(.sub-tab-container) {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n margin-bottom: 0 !important;\n}\n\n/* \u0421\u043A\u0440\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u0435 section-body */\n.form-section .section-body:empty,\n.form-section .section-body:has(> .form-column > form > .frappe-control[data-fieldname=\"html_vohk\"] > span.tooltip-content:only-child) {\n display: none !important;\n padding: 0 !important;\n margin: 0 !important;\n min-height: 0 !important;\n}\n\n/* \u0418\u0441\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 sub-tab-container */\n.sub-tab-container.nav {\n display: flex;\n list-style: none;\n padding: 0;\n margin: 0;\n}\n\n.sub-tab-container.nav > li[style*=\"display: none\"] {\n display: none !important;\n}\n\n/* \u041A\u043B\u0430\u0441\u0441 \u0434\u043B\u044F \u0441\u043A\u0440\u044B\u0442\u0438\u044F \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0430 \u0441\u0443\u0431\u0442\u0430\u0431\u043E\u0432 */\n.sub-tab-container.subtab-hidden {\n display: none !important;\n visibility: hidden !important;\n pointer-events: none !important;\n height: 0 !important;\n overflow: hidden !important;\n}"],
|
||||
"mappings": ";AAAA;AACI;AACA;AACA;AACA;AACA;AAAA;AAGJ;AACI;AACA;AACA;AACA;AAAA;AAIJ;AAAA;AAEI;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAGJ;AAAA;AAEI;AAAA;AAGJ;AAAA;AAEI;AACA;AACA;AACA;AACA;AAAA;AAIJ;AACI;AACA;AACA;AAAA;AAIJ;AAAA;AAEI;AACA;AACA;AACA;AAAA;AAIJ;AACI;AACA;AACA;AACA;AAAA;AAGJ;AACI;AAAA;AAIJ;AACI;AACA;AACA;AACA;AACA;AAAA;",
|
||||
"mappings": "AAAA,0DAGI,uBACA,qCACA,uCAGJ,4BARA,mBAUI,0BACA,sBAXJ,oBAgBA,iEAEI,+BACA,wBAnBJ,2BAqBI,qBACA,sBACA,yBACA,0BAxBJ,mBA0BI,iCACA,0BAGJ,6EAEI,qBAGJ,+EAEI,qBACA,0BACA,uCACA,uCAxCJ,0BA6CA,sCACI,wBACA,2BACA,0BAIJ,yKAEI,uBAtDJ,uCAyDI,uBAIJ,uBACI,aACA,gBA/DJ,mBAoEA,kDACI,uBAIJ,iCACI,uBACA,4BACA,8BACA,mBACA",
|
||||
"names": []
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/* ../../../../../tmp/tmp-18495-vJNc0fl1RN65/custom_subtabs/custom_subtabs/public/css/custom_subtabs.bundle.css */
|
||||
.sub-tab-container {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
display: flex !important;
|
||||
justify-content: flex-start !important;
|
||||
border-bottom: 1px solid #ddd !important;
|
||||
}
|
||||
.sub-tab-container .sub-tab {
|
||||
margin: 0 !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.sub-tab-container .sub-tab a,
|
||||
.sub-tab-container .sub-tab button {
|
||||
text-decoration: none !important;
|
||||
display: block !important;
|
||||
padding: 6px 15px !important;
|
||||
color: #555 !important;
|
||||
border: none !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: normal !important;
|
||||
margin: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.sub-tab-container .sub-tab a:hover,
|
||||
.sub-tab-container .sub-tab button:hover {
|
||||
color: #000 !important;
|
||||
}
|
||||
.sub-tab-container .sub-tab.active a,
|
||||
.sub-tab-container .sub-tab.active button {
|
||||
color: #000 !important;
|
||||
font-weight: 600 !important;
|
||||
border-bottom: 2px solid #000 !important;
|
||||
background-color: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.form-section:has(.sub-tab-container) {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.form-section .section-body:empty,
|
||||
.form-section .section-body:has(> .form-column > form > .frappe-control[data-fieldname="html_vohk"] > span.tooltip-content:only-child) {
|
||||
display: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
.sub-tab-container.nav {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.sub-tab-container.nav > li[style*="display: none"] {
|
||||
display: none !important;
|
||||
}
|
||||
.sub-tab-container.subtab-hidden {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
height: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
/*# sourceMappingURL=custom_subtabs.bundle.HFXJTVZI.css.map */
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/* ../../../../../tmp/tmp-18495-RhkE9CzEb0G6/custom_subtabs/custom_subtabs/public/css/custom_subtabs.bundle.css */
|
||||
.sub-tab-container {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
display: flex !important;
|
||||
justify-content: flex-start !important;
|
||||
border-bottom: 1px solid #ddd !important;
|
||||
}
|
||||
.sub-tab-container .sub-tab {
|
||||
margin: 0 !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.sub-tab-container .sub-tab a,
|
||||
.sub-tab-container .sub-tab button {
|
||||
text-decoration: none !important;
|
||||
display: block !important;
|
||||
padding: 6px 15px !important;
|
||||
color: #555 !important;
|
||||
border: none !important;
|
||||
font-size: 14px !important;
|
||||
font-weight: normal !important;
|
||||
margin: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.sub-tab-container .sub-tab a:hover,
|
||||
.sub-tab-container .sub-tab button:hover {
|
||||
color: #000 !important;
|
||||
}
|
||||
.sub-tab-container .sub-tab.active a,
|
||||
.sub-tab-container .sub-tab.active button {
|
||||
color: #000 !important;
|
||||
font-weight: 600 !important;
|
||||
border-bottom: 2px solid #000 !important;
|
||||
background-color: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.form-section:has(.sub-tab-container) {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.form-section .section-body:empty,
|
||||
.form-section .section-body:has(> .form-column > form > .frappe-control[data-fieldname="html_vohk"] > span.tooltip-content:only-child) {
|
||||
display: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
.sub-tab-container.nav {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.sub-tab-container.nav > li[style*="display: none"] {
|
||||
display: none !important;
|
||||
}
|
||||
.sub-tab-container.subtab-hidden {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
height: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
/*# sourceMappingURL=custom_subtabs.bundle.6DBL7IOJ.css.map */
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
.sub-tab-container{margin:0!important;padding:0!important;display:flex!important;justify-content:flex-start!important;border-bottom:1px solid #ddd!important}.sub-tab-container .sub-tab{margin:0!important;background:none!important;border:none!important;padding:0!important}.sub-tab-container .sub-tab a,.sub-tab-container .sub-tab button{text-decoration:none!important;display:block!important;padding:6px 15px!important;color:#555!important;border:none!important;font-size:14px!important;font-weight:400!important;margin:0!important;background:transparent!important;box-shadow:none!important}.sub-tab-container .sub-tab a:hover,.sub-tab-container .sub-tab button:hover{color:#000!important}.sub-tab-container .sub-tab.active a,.sub-tab-container .sub-tab.active button{color:#000!important;font-weight:600!important;border-bottom:2px solid #000!important;background-color:transparent!important;border-radius:0!important}.form-section:has(.sub-tab-container){padding-top:0!important;padding-bottom:0!important;margin-bottom:0!important}.form-section .section-body:empty,.form-section .section-body:has(> .form-column > form > .frappe-control[data-fieldname="html_vohk"] > span.tooltip-content:only-child){display:none!important;padding:0!important;margin:0!important;min-height:0!important}.sub-tab-container.nav{display:flex;list-style:none;padding:0;margin:0}.sub-tab-container.nav>li[style*="display: none"]{display:none!important}.sub-tab-container.subtab-hidden{display:none!important;visibility:hidden!important;pointer-events:none!important;height:0!important;overflow:hidden!important}
|
||||
/*# sourceMappingURL=custom_subtabs.bundle.CJRWDWV2.css.map */
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"version": 3,
|
||||
"sources": ["../../../../../../../../tmp/tmp-18495-RhkE9CzEb0G6/custom_subtabs/custom_subtabs/public/css/custom_subtabs.bundle.css"],
|
||||
"sources": ["../../../../../../../../tmp/tmp-15812-HYmC3u994rit/custom_subtabs/custom_subtabs/public/css/custom_subtabs.bundle.css"],
|
||||
"sourcesContent": [".sub-tab-container {\n margin: 0 !important;\n padding: 0 !important;\n display: flex !important;\n justify-content: flex-start !important;\n border-bottom: 1px solid #ddd !important;\n}\n\n.sub-tab-container .sub-tab {\n margin: 0 !important;\n background: none !important;\n border: none !important;\n padding: 0 !important;\n}\n\n/* Support both <a> and <button> for v15 and v16 compatibility */\n.sub-tab-container .sub-tab a,\n.sub-tab-container .sub-tab button {\n text-decoration: none !important;\n display: block !important;\n padding: 6px 15px !important;\n color: #555 !important;\n border: none !important;\n font-size: 14px !important;\n font-weight: normal !important;\n margin: 0 !important;\n background: transparent !important;\n box-shadow: none !important;\n}\n\n.sub-tab-container .sub-tab a:hover,\n.sub-tab-container .sub-tab button:hover {\n color: #000 !important;\n}\n\n.sub-tab-container .sub-tab.active a,\n.sub-tab-container .sub-tab.active button {\n color: #000 !important;\n font-weight: 600 !important;\n border-bottom: 2px solid #000 !important;\n background-color: transparent !important;\n border-radius: 0 !important;\n}\n\n/* \u041A\u041B\u042E\u0427\u0415\u0412\u041E\u0415 \u0418\u0421\u041F\u0420\u0410\u0412\u041B\u0415\u041D\u0418\u0415: \u0443\u0431\u0440\u0430\u0442\u044C padding \u0443 form-section \u0441 \u0441\u0443\u0431\u0442\u0430\u0431\u0430\u043C\u0438 */\n.form-section:has(.sub-tab-container) {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n margin-bottom: 0 !important;\n}\n\n/* \u0421\u043A\u0440\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u0435 section-body */\n.form-section .section-body:empty,\n.form-section .section-body:has(> .form-column > form > .frappe-control[data-fieldname=\"html_vohk\"] > span.tooltip-content:only-child) {\n display: none !important;\n padding: 0 !important;\n margin: 0 !important;\n min-height: 0 !important;\n}\n\n/* \u0418\u0441\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 sub-tab-container */\n.sub-tab-container.nav {\n display: flex;\n list-style: none;\n padding: 0;\n margin: 0;\n}\n\n.sub-tab-container.nav > li[style*=\"display: none\"] {\n display: none !important;\n}\n\n/* \u041A\u043B\u0430\u0441\u0441 \u0434\u043B\u044F \u0441\u043A\u0440\u044B\u0442\u0438\u044F \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0430 \u0441\u0443\u0431\u0442\u0430\u0431\u043E\u0432 */\n.sub-tab-container.subtab-hidden {\n display: none !important;\n visibility: hidden !important;\n pointer-events: none !important;\n height: 0 !important;\n overflow: hidden !important;\n}"],
|
||||
"mappings": ";AAAA;AACI;AACA;AACA;AACA;AACA;AAAA;AAGJ;AACI;AACA;AACA;AACA;AAAA;AAIJ;AAAA;AAEI;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AAGJ;AAAA;AAEI;AAAA;AAGJ;AAAA;AAEI;AACA;AACA;AACA;AACA;AAAA;AAIJ;AACI;AACA;AACA;AAAA;AAIJ;AAAA;AAEI;AACA;AACA;AACA;AAAA;AAIJ;AACI;AACA;AACA;AACA;AAAA;AAGJ;AACI;AAAA;AAIJ;AACI;AACA;AACA;AACA;AACA;AAAA;",
|
||||
"mappings": "AAAA,0DAGI,uBACA,qCACA,uCAGJ,4BARA,mBAUI,0BACA,sBAXJ,oBAgBA,iEAEI,+BACA,wBAnBJ,2BAqBI,qBACA,sBACA,yBACA,0BAxBJ,mBA0BI,iCACA,0BAGJ,6EAEI,qBAGJ,+EAEI,qBACA,0BACA,uCACA,uCAxCJ,0BA6CA,sCACI,wBACA,2BACA,0BAIJ,yKAEI,uBAtDJ,uCAyDI,uBAIJ,uBACI,aACA,gBA/DJ,mBAoEA,kDACI,uBAIJ,iCACI,uBACA,4BACA,8BACA,mBACA",
|
||||
"names": []
|
||||
}
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
(() => {
|
||||
// ../custom_subtabs/custom_subtabs/public/js/custom_subtabs.bundle.js
|
||||
function setup_custom_subtabs(wrapper, currentDoctype) {
|
||||
var _a;
|
||||
if (!wrapper || !wrapper.length) {
|
||||
return;
|
||||
}
|
||||
const tabs = wrapper.find("ul.form-tabs > li.nav-item");
|
||||
const hierarchy = (_a = frappe.boot.tab_hierarchy) == null ? void 0 : _a[currentDoctype];
|
||||
if (!hierarchy) {
|
||||
return;
|
||||
}
|
||||
const subtabs = hierarchy.subtabs || {};
|
||||
if (Object.keys(subtabs).length === 0) {
|
||||
return;
|
||||
}
|
||||
const tr = (s) => typeof __ === "function" ? __(s) : s;
|
||||
function getTabLink(tabElement) {
|
||||
const $tab = $(tabElement);
|
||||
let link = $tab.find("button.nav-link");
|
||||
if (!link.length) {
|
||||
link = $tab.find("a.nav-link");
|
||||
}
|
||||
return link;
|
||||
}
|
||||
if (wrapper.attr("data-subtabs-processed") === "true") {
|
||||
return;
|
||||
}
|
||||
const childToParentMap = {};
|
||||
for (const [parentTab, children] of Object.entries(subtabs)) {
|
||||
children.forEach((child) => {
|
||||
childToParentMap[child] = parentTab;
|
||||
});
|
||||
}
|
||||
tabs.each((index, tab) => {
|
||||
const tabLabel = $(tab).text().trim();
|
||||
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;
|
||||
}
|
||||
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) => {
|
||||
const subtabElement = tabs.filter((_, el) => $(el).text().trim() === tr(subtab));
|
||||
if (subtabElement.length) {
|
||||
const subtabClone = subtabElement.clone(true);
|
||||
subtabClone.addClass("sub-tab");
|
||||
subtabClone.attr("data-subtab-name", subtab);
|
||||
const clonedLink = getTabLink(subtabClone);
|
||||
clonedLink.removeAttr("href");
|
||||
clonedLink.attr("data-target-id", clonedLink.attr("aria-controls"));
|
||||
clonedLink.off("click");
|
||||
subtabClone.attr("data-parent-id", tabContentId);
|
||||
subtabContainer.append(subtabClone);
|
||||
subtabElement.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
function ensureParentChainVisible(subtabName) {
|
||||
let currentChild = subtabName;
|
||||
const parentsToShow = [];
|
||||
while (childToParentMap[currentChild]) {
|
||||
const parentName = childToParentMap[currentChild];
|
||||
parentsToShow.push(parentName);
|
||||
const siblings = subtabs[parentName] || [];
|
||||
siblings.forEach((sibling) => {
|
||||
const siblingElement = wrapper.find(`.sub-tab[data-subtab-name="${sibling}"]`);
|
||||
siblingElement.css("display", "");
|
||||
});
|
||||
currentChild = parentName;
|
||||
}
|
||||
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");
|
||||
}
|
||||
});
|
||||
}
|
||||
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");
|
||||
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");
|
||||
}
|
||||
});
|
||||
clickedSubtab.addClass("active");
|
||||
clickedButton.addClass("active").attr("aria-selected", "true");
|
||||
const targetPane = $(`#${targetId}`);
|
||||
if (targetPane.length) {
|
||||
targetPane.addClass("active show");
|
||||
}
|
||||
const parentPane = $(`#${parentContainerId}`);
|
||||
if (parentPane.length) {
|
||||
parentPane.addClass("active show").css("display", "block");
|
||||
if (targetPane.length) {
|
||||
parentPane.after(targetPane);
|
||||
}
|
||||
}
|
||||
ensureParentChainVisible(subtabName);
|
||||
});
|
||||
function setupMainTabHandler() {
|
||||
if (wrapper.data("subtabs-observer")) {
|
||||
wrapper.data("subtabs-observer").disconnect();
|
||||
}
|
||||
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");
|
||||
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");
|
||||
}
|
||||
});
|
||||
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"] });
|
||||
});
|
||||
wrapper.data("subtabs-observer", observer);
|
||||
}
|
||||
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");
|
||||
}
|
||||
});
|
||||
wrapper.attr("data-subtabs-processed", "true");
|
||||
}
|
||||
frappe.ui.form.on("*", {
|
||||
refresh: function(frm) {
|
||||
setTimeout(() => {
|
||||
var _a;
|
||||
const wrapper = ((_a = frm.layout) == null ? void 0 : _a.wrapper) || frm.$wrapper;
|
||||
setup_custom_subtabs(wrapper, frm.doctype);
|
||||
}, 500);
|
||||
},
|
||||
onload: function(frm) {
|
||||
}
|
||||
});
|
||||
})();
|
||||
//# sourceMappingURL=custom_subtabs.bundle.DIIPT3ZZ.js.map
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,2 @@
|
|||
(()=>{function x(a,t,n){var m;if(!t||!t.length)return;let i=(m=frappe.boot.tab_hierarchy)==null?void 0:m[n];if(!i)return;let e=i.subtabs||{};if(Object.keys(e).length===0||(D(a),t.attr("data-subtabs-processed")==="true"))return;let s=c=>typeof __=="function"?__(c):c,o=t.find("ul.form-tabs > li.nav-item");o.each((c,k)=>{let I=$(k).text().trim(),E=Object.keys(e).find(f=>s(f)===I);if(!E)return;let p=_(k).attr("aria-controls"),d=$(document.getElementById(p));if(!d.length)return;let r=d.find(".form-section.card-section");r.length||(r=d.find(".form-section")),r.length||(r=d.children().first()),r.length||(r=d);let b=r.find("> .sub-tab-container");b.length||(b=$(`<div class="sub-tab-container nav" data-parent-id="${p}"></div>`),r.prepend(b)),e[E].forEach(f=>{let h=o.filter((P,j)=>$(j).text().trim()===s(f));if(!h.length)return;let l=h.clone(!0);l.addClass("sub-tab"),l.attr("data-subtab-name",f),l.attr("data-parent-id",p);let v=_(l);v.off("click"),v.attr("data-target-id",v.attr("aria-controls")),b.append(l),h.remove()})}),t.on("click.customsubtabs",".sub-tab button.nav-link, .sub-tab a.nav-link",function(c){c.preventDefault(),c.stopPropagation(),O(a,t,$(this).closest(".sub-tab"))}),t.attr("data-subtabs-processed","true");let u=t.find("ul.form-tabs > li.nav-item .nav-link.active").first();y(t,[u.attr("aria-controls")])}function _(a){let t=$(a),n=t.find("button.nav-link");return n.length?n:t.find("a.nav-link")}function g(a){let t=_(a);return t.attr("data-target-id")||t.attr("aria-controls")}function L(a,t){let n=[],i=t;for(;i&&i.length;){n.unshift({pane_id:g(i),parent_id:i.attr("data-parent-id")});let e=i.attr("data-parent-id");i=a.find(".sub-tab").filter((s,o)=>g($(o))===e)}return{chain:n,root_id:n.length?n[0].parent_id:null}}function O(a,t,n){let i=L(t,n);if(!i.root_id)return;a.__active_subtab=i;let e=(a.layout.tabs||[]).find(s=>s.wrapper.attr("id")===i.root_id);e?e.set_active():C(a,t,i)}function C(a,t,n){let i=[n.root_id];for(let e of n.chain){let s=document.getElementById(e.pane_id),o=document.getElementById(e.parent_id);!s||!o||(s.classList.add("active","show"),s.classList.remove("hide"),s.previousElementSibling!==o&&o.after(s),i.push(e.pane_id))}y(t,i),B(t,n.chain.map(e=>e.pane_id))}function T(a,t,n){delete a.__active_subtab,y(t,[n]),B(t,[])}function y(a,t){a.find(".sub-tab-container").each(function(){let n=$(this).attr("data-parent-id");$(this).toggleClass("subtab-hidden",!t.includes(n))})}function B(a,t){a.find(".sub-tab").each(function(){let n=t.includes(g($(this)));$(this).toggleClass("active",n),_($(this)).toggleClass("active",n).attr("aria-selected",n?"true":"false")})}function D(a){let t=a.layout&&a.layout.tabs;if(!t||!t.length)return;let n=t[0].constructor;if(n.prototype.__subtabs_patched)return;let i=n.prototype.set_active;n.prototype.set_active=function(){i.apply(this,arguments);let e=this.frm,s=e&&e.layout&&e.layout.wrapper;if(!s||s.attr("data-subtabs-processed")!=="true")return;let o=this.wrapper&&this.wrapper.attr("id");if(!o)return;let u=e.__active_subtab;if(!u||u.root_id!==o){T(e,s,o);return}C(e,s,u)},n.prototype.__subtabs_patched=!0}frappe.ui.form.on("*",{refresh:function(a){setTimeout(()=>{var t;x(a,((t=a.layout)==null?void 0:t.wrapper)||a.$wrapper,a.doctype)},500)}});})();
|
||||
//# sourceMappingURL=custom_subtabs.bundle.QW446NRW.js.map
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,292 +1,235 @@
|
|||
// Shared function for setting up subtabs
|
||||
function setup_custom_subtabs(wrapper, currentDoctype) {
|
||||
if (!wrapper || !wrapper.length) {
|
||||
return;
|
||||
}
|
||||
// Subtabs: nest Frappe form tabs one or two levels deep.
|
||||
//
|
||||
// A Tab Break with `js_parent_subtab` set is a subtab. Its nav item is moved out of
|
||||
// the main tab bar and into a secondary bar rendered inside its parent's pane.
|
||||
//
|
||||
// Frappe's tab model has no idea any of this exists. `Tab.set_active()` (form/tab.js)
|
||||
// strips `show active` off *every* pane except the one it is activating, and
|
||||
// `Layout.set_tab_as_active()` calls it on every `refresh_sections()` — which Frappe
|
||||
// runs on every `frm.refresh_field()` and on every value change. So the moment a
|
||||
// field changed, the open subtab's pane was stripped and its content vanished; the
|
||||
// content appeared to flash for a fraction of a second and disappear.
|
||||
//
|
||||
// The fix is to stop fighting Frappe's model and instead ride on it:
|
||||
//
|
||||
// * clicking a subtab registers its top-level ANCESTOR as the active tab, so
|
||||
// Frappe's own bookkeeping (frm.active_tab_map, the URL hash) stays coherent and
|
||||
// keeps re-activating the right tab;
|
||||
// * `Tab.set_active` is wrapped so that once Frappe has finished re-activating that
|
||||
// ancestor, we re-assert the rest of the chain.
|
||||
//
|
||||
// The whole ancestor chain has to be re-asserted, not just the leaf: with two levels
|
||||
// of nesting, the top pane holds the first subtab bar, the middle pane holds the
|
||||
// second, and the leaf pane holds the content. All three must be `active show` at
|
||||
// once — something Frappe's one-tab-at-a-time model will never do on its own.
|
||||
|
||||
const tabs = wrapper.find("ul.form-tabs > li.nav-item");
|
||||
const hierarchy = frappe.boot.tab_hierarchy?.[currentDoctype];
|
||||
function setup_custom_subtabs(frm, wrapper, currentDoctype) {
|
||||
if (!wrapper || !wrapper.length) return;
|
||||
|
||||
if (!hierarchy) {
|
||||
return;
|
||||
}
|
||||
const hierarchy = frappe.boot.tab_hierarchy?.[currentDoctype];
|
||||
if (!hierarchy) return;
|
||||
|
||||
const subtabs = hierarchy.subtabs || {};
|
||||
const subtabs = hierarchy.subtabs || {};
|
||||
if (Object.keys(subtabs).length === 0) return;
|
||||
|
||||
if (Object.keys(subtabs).length === 0) {
|
||||
return;
|
||||
}
|
||||
patch_tab_activation(frm);
|
||||
|
||||
// tab_hierarchy хранит СЫРЫЕ (английские) label'ы, а Frappe рисует вкладки
|
||||
// через __(), т.е. в DOM текст уже переведён (AZ/RU). Поэтому сопоставлять
|
||||
// вкладку из DOM с ключом иерархии нужно через перевод этого ключа.
|
||||
// Внутренние структуры (childToParentMap, data-subtab-name) оставляем на
|
||||
// английском — переводим только на границе сравнения с DOM.
|
||||
const tr = (s) => (typeof __ === 'function' ? __(s) : s);
|
||||
if (wrapper.attr("data-subtabs-processed") === "true") return;
|
||||
|
||||
// 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;
|
||||
}
|
||||
// tab_hierarchy stores raw (English) labels, while Frappe renders tab labels
|
||||
// through __(), so the DOM text is already translated. Compare through the
|
||||
// translation; keep the English label in the internal bookkeeping.
|
||||
const tr = (s) => (typeof __ === "function" ? __(s) : s);
|
||||
|
||||
// Check if already processed
|
||||
if (wrapper.attr('data-subtabs-processed') === 'true') {
|
||||
return;
|
||||
}
|
||||
const tabs = wrapper.find("ul.form-tabs > li.nav-item");
|
||||
|
||||
// Build parent-child map
|
||||
const childToParentMap = {};
|
||||
for (const [parentTab, children] of Object.entries(subtabs)) {
|
||||
children.forEach(child => {
|
||||
childToParentMap[child] = parentTab;
|
||||
});
|
||||
}
|
||||
tabs.each((_, tab) => {
|
||||
const label = $(tab).text().trim();
|
||||
const parent_key = Object.keys(subtabs).find((k) => tr(k) === label);
|
||||
if (!parent_key) return;
|
||||
|
||||
// Process subtabs - move them into containers
|
||||
tabs.each((index, tab) => {
|
||||
const tabLabel = $(tab).text().trim();
|
||||
const parent_pane_id = tab_link(tab).attr("aria-controls");
|
||||
const parent_pane = $(document.getElementById(parent_pane_id));
|
||||
if (!parent_pane.length) return;
|
||||
|
||||
// tabLabel — переведённый текст из DOM; ключ иерархии — английский.
|
||||
// Находим английский ключ-родитель, чей перевод совпал с DOM.
|
||||
const parentKey = Object.keys(subtabs).find((k) => tr(k) === tabLabel);
|
||||
let section = parent_pane.find(".form-section.card-section");
|
||||
if (!section.length) section = parent_pane.find(".form-section");
|
||||
if (!section.length) section = parent_pane.children().first();
|
||||
if (!section.length) section = parent_pane;
|
||||
|
||||
if (parentKey) {
|
||||
const tabLink = getTabLink(tab);
|
||||
const tabContentId = tabLink.attr("aria-controls");
|
||||
const tabContent = $(`#${tabContentId}`);
|
||||
let bar = section.find("> .sub-tab-container");
|
||||
if (!bar.length) {
|
||||
bar = $(`<div class="sub-tab-container nav" data-parent-id="${parent_pane_id}"></div>`);
|
||||
section.prepend(bar);
|
||||
}
|
||||
|
||||
if (!tabContent.length) {
|
||||
return;
|
||||
}
|
||||
subtabs[parent_key].forEach((child) => {
|
||||
const original = tabs.filter((__, el) => $(el).text().trim() === tr(child));
|
||||
if (!original.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;
|
||||
}
|
||||
const clone = original.clone(true);
|
||||
clone.addClass("sub-tab");
|
||||
clone.attr("data-subtab-name", child);
|
||||
clone.attr("data-parent-id", parent_pane_id);
|
||||
|
||||
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);
|
||||
}
|
||||
// clone(true) copies Frappe's own click handler (tab.js binds set_active on
|
||||
// the nav-link), which would deactivate the parent pane — the very pane this
|
||||
// button lives in. Subtab clicks are handled by our delegated handler below.
|
||||
const link = tab_link(clone);
|
||||
link.off("click");
|
||||
link.attr("data-target-id", link.attr("aria-controls"));
|
||||
|
||||
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);
|
||||
bar.append(clone);
|
||||
original.remove();
|
||||
});
|
||||
});
|
||||
|
||||
subtabContainer.append(subtabClone);
|
||||
subtabElement.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
wrapper.on("click.customsubtabs", ".sub-tab button.nav-link, .sub-tab a.nav-link", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
activate_subtab(frm, wrapper, $(this).closest(".sub-tab"));
|
||||
});
|
||||
|
||||
// Function to show parent chain
|
||||
function ensureParentChainVisible(subtabName) {
|
||||
let currentChild = subtabName;
|
||||
const parentsToShow = [];
|
||||
wrapper.attr("data-subtabs-processed", "true");
|
||||
|
||||
// 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');
|
||||
// Whatever tab Frappe already considers active decides which subtab bar shows.
|
||||
const active = wrapper.find("ul.form-tabs > li.nav-item .nav-link.active").first();
|
||||
sync_bars(wrapper, [active.attr("aria-controls")]);
|
||||
}
|
||||
|
||||
// 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);
|
||||
},
|
||||
function tab_link(el) {
|
||||
const $el = $(el);
|
||||
const button = $el.find("button.nav-link");
|
||||
return button.length ? button : $el.find("a.nav-link");
|
||||
}
|
||||
|
||||
onload: function(frm) {
|
||||
// Form onload hook
|
||||
}
|
||||
function pane_id_of(subtab) {
|
||||
const link = tab_link(subtab);
|
||||
return link.attr("data-target-id") || link.attr("aria-controls");
|
||||
}
|
||||
|
||||
// Walk from a subtab up to its top-level tab. Returns the panes to re-assert,
|
||||
// outermost first, plus the id of the real Frappe tab that roots the chain.
|
||||
function ancestry(wrapper, subtab) {
|
||||
const chain = [];
|
||||
let node = subtab;
|
||||
|
||||
while (node && node.length) {
|
||||
chain.unshift({ pane_id: pane_id_of(node), parent_id: node.attr("data-parent-id") });
|
||||
|
||||
const parent_id = node.attr("data-parent-id");
|
||||
node = wrapper.find(".sub-tab").filter((_, el) => pane_id_of($(el)) === parent_id);
|
||||
}
|
||||
|
||||
return { chain: chain, root_id: chain.length ? chain[0].parent_id : null };
|
||||
}
|
||||
|
||||
function activate_subtab(frm, wrapper, subtab) {
|
||||
const state = ancestry(wrapper, subtab);
|
||||
if (!state.root_id) return;
|
||||
|
||||
frm.__active_subtab = state;
|
||||
|
||||
// Hand the activation to Frappe, naming the top-level ancestor. Frappe records it
|
||||
// in frm.active_tab_map and the URL hash, so every later refresh re-activates the
|
||||
// same tab — and our wrapper puts the rest of the chain back on top of it.
|
||||
const root = (frm.layout.tabs || []).find((t) => t.wrapper.attr("id") === state.root_id);
|
||||
if (root) {
|
||||
root.set_active();
|
||||
} else {
|
||||
reassert_chain(frm, wrapper, state);
|
||||
}
|
||||
}
|
||||
|
||||
function reassert_chain(frm, wrapper, state) {
|
||||
const visible = [state.root_id];
|
||||
|
||||
for (const step of state.chain) {
|
||||
const pane = document.getElementById(step.pane_id);
|
||||
const parent = document.getElementById(step.parent_id);
|
||||
if (!pane || !parent) continue;
|
||||
|
||||
pane.classList.add("active", "show");
|
||||
pane.classList.remove("hide");
|
||||
|
||||
// A subtab's pane can sit before its parent's in the DOM, depending on
|
||||
// field_order. Left alone, the subtab bar renders below its own content and
|
||||
// looks like it disappeared. Keep the pane directly after its parent.
|
||||
if (pane.previousElementSibling !== parent) {
|
||||
parent.after(pane);
|
||||
}
|
||||
|
||||
visible.push(step.pane_id);
|
||||
}
|
||||
|
||||
sync_bars(wrapper, visible);
|
||||
sync_buttons(wrapper, state.chain.map((s) => s.pane_id));
|
||||
}
|
||||
|
||||
function clear_subtabs(frm, wrapper, active_id) {
|
||||
delete frm.__active_subtab;
|
||||
sync_bars(wrapper, [active_id]);
|
||||
sync_buttons(wrapper, []);
|
||||
}
|
||||
|
||||
// A subtab bar belongs to a pane; show it only while that pane is on screen.
|
||||
function sync_bars(wrapper, visible_pane_ids) {
|
||||
wrapper.find(".sub-tab-container").each(function () {
|
||||
const owner = $(this).attr("data-parent-id");
|
||||
$(this).toggleClass("subtab-hidden", !visible_pane_ids.includes(owner));
|
||||
});
|
||||
}
|
||||
|
||||
function sync_buttons(wrapper, active_pane_ids) {
|
||||
wrapper.find(".sub-tab").each(function () {
|
||||
const on = active_pane_ids.includes(pane_id_of($(this)));
|
||||
$(this).toggleClass("active", on);
|
||||
tab_link($(this)).toggleClass("active", on).attr("aria-selected", on ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
// Wrap Frappe's Tab.set_active once per session. Everything that re-activates a tab —
|
||||
// a click, a refresh_sections(), a refresh_field() — funnels through it, so this is
|
||||
// the one place that can put the subtab chain back after Frappe has torn it down.
|
||||
function patch_tab_activation(frm) {
|
||||
const tabs = frm.layout && frm.layout.tabs;
|
||||
if (!tabs || !tabs.length) return;
|
||||
|
||||
const Tab = tabs[0].constructor;
|
||||
if (Tab.prototype.__subtabs_patched) return;
|
||||
|
||||
const set_active = Tab.prototype.set_active;
|
||||
|
||||
Tab.prototype.set_active = function () {
|
||||
set_active.apply(this, arguments);
|
||||
|
||||
const form = this.frm;
|
||||
const wrapper = form && form.layout && form.layout.wrapper;
|
||||
if (!wrapper || wrapper.attr("data-subtabs-processed") !== "true") return;
|
||||
|
||||
const activated = this.wrapper && this.wrapper.attr("id");
|
||||
if (!activated) return;
|
||||
|
||||
const state = form.__active_subtab;
|
||||
|
||||
// Activating anything other than the chain's root means the user left the
|
||||
// subtab — or Frappe restored a different tab. Either way the selection is over.
|
||||
if (!state || state.root_id !== activated) {
|
||||
clear_subtabs(form, wrapper, activated);
|
||||
return;
|
||||
}
|
||||
|
||||
reassert_chain(form, wrapper, state);
|
||||
};
|
||||
|
||||
Tab.prototype.__subtabs_patched = true;
|
||||
}
|
||||
|
||||
frappe.ui.form.on("*", {
|
||||
refresh: function (frm) {
|
||||
// The layout is rebuilt after refresh; let it settle before rewiring the bars.
|
||||
setTimeout(() => {
|
||||
setup_custom_subtabs(frm, frm.layout?.wrapper || frm.$wrapper, frm.doctype);
|
||||
}, 500);
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue