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:
Ali 2026-07-13 12:22:07 +00:00
parent 7d9fb69f3b
commit 63476b39aa
11 changed files with 231 additions and 615 deletions

View File

@ -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 */

View File

@ -1,7 +1,7 @@
{ {
"version": 3, "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}"], "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": [] "names": []
} }

View File

@ -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 */

View File

@ -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 */

View File

@ -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 */

View File

@ -1,7 +1,7 @@
{ {
"version": 3, "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}"], "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": [] "names": []
} }

View File

@ -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

View File

@ -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

View File

@ -1,292 +1,235 @@
// Shared function for setting up subtabs // Subtabs: nest Frappe form tabs one or two levels deep.
function setup_custom_subtabs(wrapper, currentDoctype) { //
if (!wrapper || !wrapper.length) { // A Tab Break with `js_parent_subtab` set is a subtab. Its nav item is moved out of
return; // 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.
function setup_custom_subtabs(frm, wrapper, currentDoctype) {
if (!wrapper || !wrapper.length) return;
const tabs = wrapper.find("ul.form-tabs > li.nav-item");
const hierarchy = frappe.boot.tab_hierarchy?.[currentDoctype]; const hierarchy = frappe.boot.tab_hierarchy?.[currentDoctype];
if (!hierarchy) return;
if (!hierarchy) {
return;
}
const subtabs = hierarchy.subtabs || {}; const subtabs = hierarchy.subtabs || {};
if (Object.keys(subtabs).length === 0) return;
if (Object.keys(subtabs).length === 0) { patch_tab_activation(frm);
return;
if (wrapper.attr("data-subtabs-processed") === "true") return;
// 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);
const tabs = wrapper.find("ul.form-tabs > li.nav-item");
tabs.each((_, tab) => {
const label = $(tab).text().trim();
const parent_key = Object.keys(subtabs).find((k) => tr(k) === label);
if (!parent_key) return;
const parent_pane_id = tab_link(tab).attr("aria-controls");
const parent_pane = $(document.getElementById(parent_pane_id));
if (!parent_pane.length) return;
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;
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);
} }
// tab_hierarchy хранит СЫРЫЕ (английские) label'ы, а Frappe рисует вкладки subtabs[parent_key].forEach((child) => {
// через __(), т.е. в DOM текст уже переведён (AZ/RU). Поэтому сопоставлять const original = tabs.filter((__, el) => $(el).text().trim() === tr(child));
// вкладку из DOM с ключом иерархии нужно через перевод этого ключа. if (!original.length) return;
// Внутренние структуры (childToParentMap, data-subtab-name) оставляем на
// английском — переводим только на границе сравнения с DOM.
const tr = (s) => (typeof __ === 'function' ? __(s) : s);
// Helper function to find tab link element const clone = original.clone(true);
function getTabLink(tabElement) { clone.addClass("sub-tab");
const $tab = $(tabElement); clone.attr("data-subtab-name", child);
let link = $tab.find("button.nav-link"); clone.attr("data-parent-id", parent_pane_id);
if (!link.length) {
link = $tab.find("a.nav-link");
}
return link;
}
// Check if already processed // clone(true) copies Frappe's own click handler (tab.js binds set_active on
if (wrapper.attr('data-subtabs-processed') === 'true') { // the nav-link), which would deactivate the parent pane — the very pane this
return; // 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"));
// Build parent-child map bar.append(clone);
const childToParentMap = {}; original.remove();
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 wrapper.on("click.customsubtabs", ".sub-tab button.nav-link, .sub-tab a.nav-link", function (e) {
function ensureParentChainVisible(subtabName) { e.preventDefault();
let currentChild = subtabName; e.stopPropagation();
const parentsToShow = []; activate_subtab(frm, wrapper, $(this).closest(".sub-tab"));
// 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; 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")]);
} }
// Show all parent content function tab_link(el) {
parentsToShow.forEach(parentName => { const $el = $(el);
const parentTabContentId = `${currentDoctype.toLowerCase().replace(/\s+/g, '-')}-${parentName.toLowerCase().replace(/\s+/g, '_')}_tab`; const button = $el.find("button.nav-link");
const parentContent = $(`#${parentTabContentId}`); return button.length ? button : $el.find("a.nav-link");
if (parentContent.length && !parentContent.hasClass('active')) {
parentContent.addClass('active show').css('display', 'block');
}
});
} }
// Handle subtab clicks function pane_id_of(subtab) {
wrapper.on("click.customsubtabs", ".sub-tab button.nav-link, .sub-tab a.nav-link", function(event) { const link = tab_link(subtab);
event.preventDefault(); return link.attr("data-target-id") || link.attr("aria-controls");
event.stopPropagation(); }
const clickedButton = $(this); // Walk from a subtab up to its top-level tab. Returns the panes to re-assert,
const clickedSubtab = clickedButton.closest(".sub-tab"); // outermost first, plus the id of the real Frappe tab that roots the chain.
const subtabName = clickedSubtab.attr("data-subtab-name"); function ancestry(wrapper, subtab) {
const targetId = clickedButton.attr("data-target-id") || clickedButton.attr("aria-controls"); const chain = [];
const parentContainerId = clickedSubtab.attr("data-parent-id"); let node = subtab;
// Hide all tabs and content while (node && node.length) {
wrapper.find(".nav-item").removeClass("active"); chain.unshift({ pane_id: pane_id_of(node), parent_id: node.attr("data-parent-id") });
wrapper.find("button.nav-link, a.nav-link").removeClass("active").attr("aria-selected", "false");
wrapper.find(".tab-pane").removeClass("active show");
// Скрыть все контейнеры субтабов, кроме родительского const parent_id = node.attr("data-parent-id");
wrapper.find(".sub-tab-container").each(function() { node = wrapper.find(".sub-tab").filter((_, el) => pane_id_of($(el)) === parent_id);
const containerParentId = $(this).attr("data-parent-id"); }
if (containerParentId !== parentContainerId) {
$(this).addClass("subtab-hidden"); 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 { } else {
$(this).removeClass("subtab-hidden"); 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));
}); });
// 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");
} }
// Надёжно вернуть РОДИТЕЛЬСКУЮ панель (в ней живут кнопки субтабов) по её реальному function sync_buttons(wrapper, active_pane_ids) {
// 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 () { wrapper.find(".sub-tab").each(function () {
const subtabButton = getTabLink($(this)); const on = active_pane_ids.includes(pane_id_of($(this)));
const subtabContentId = subtabButton.attr("data-target-id") || subtabButton.attr("aria-controls"); $(this).toggleClass("active", on);
if (subtabContentId) { tab_link($(this)).toggleClass("active", on).attr("aria-selected", on ? "true" : "false");
$(`#${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");
}
}); });
} }
}
});
});
// Наблюдаем за всеми кнопками табов (не субтабов) // Wrap Frappe's Tab.set_active once per session. Everything that re-activates a tab —
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() { // a click, a refresh_sections(), a refresh_field() — funnels through it, so this is
observer.observe(this, { attributes: true, attributeFilter: ['class'] }); // 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;
// Сохраняем observer чтобы можно было удалить позже const Tab = tabs[0].constructor;
wrapper.data('subtabs-observer', observer); 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;
} }
// Initialize handler reassert_chain(form, wrapper, state);
setupMainTabHandler(); };
// Инициализация: скрыть все контейнеры, кроме активного таба Tab.prototype.__subtabs_patched = true;
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("*", {
frappe.ui.form.on('*', {
refresh: function (frm) { refresh: function (frm) {
// The layout is rebuilt after refresh; let it settle before rewiring the bars.
setTimeout(() => { setTimeout(() => {
const wrapper = frm.layout?.wrapper || frm.$wrapper; setup_custom_subtabs(frm, frm.layout?.wrapper || frm.$wrapper, frm.doctype);
setup_custom_subtabs(wrapper, frm.doctype);
}, 500); }, 500);
}, },
onload: function(frm) {
// Form onload hook
}
}); });