odant-work 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,6 +69,104 @@ ODA({is: 'work-form',
69
69
  from { opacity: 0; }
70
70
  to { opacity: 1; }
71
71
  }
72
+ .form-tabs-bar {
73
+ @apply --horizontal;
74
+ @apply --no-flex;
75
+ align-items: center;
76
+ gap: 4px;
77
+ max-width: 62vw;
78
+ overflow-x: auto;
79
+ scrollbar-width: thin;
80
+ }
81
+ .form-tab {
82
+ @apply --horizontal;
83
+ @apply --no-flex;
84
+ align-items: center;
85
+ gap: 5px;
86
+ max-width: 220px;
87
+ height: 24px;
88
+ padding: 2px 8px;
89
+ font-size: 12px;
90
+ font-weight: 500;
91
+ border: 1px solid var(--border-color, #b8b7ae);
92
+ border-radius: 4px;
93
+ background: light-dark(#e9e8df, #262830);
94
+ color: light-dark(#333, #cbd5e1);
95
+ cursor: pointer;
96
+ user-select: none;
97
+ box-sizing: border-box;
98
+ transition: background 0.12s ease, border-color 0.12s ease;
99
+ }
100
+ .form-tab:hover:not(.active) {
101
+ background: light-dark(#f3f5fa, #30333d);
102
+ }
103
+ .form-tab.active {
104
+ background: light-dark(#ffffff, #1b1c20);
105
+ color: light-dark(#111, #f8fafc);
106
+ border-color: light-dark(#4f6bed, #60a5fa);
107
+ font-weight: 700;
108
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
109
+ }
110
+ .form-tab .tab-label {
111
+ overflow: hidden;
112
+ text-overflow: ellipsis;
113
+ white-space: nowrap;
114
+ }
115
+ .form-tab .tab-close {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ width: 15px;
120
+ height: 15px;
121
+ margin-left: 2px;
122
+ border-radius: 3px;
123
+ font-size: 13px;
124
+ line-height: 1;
125
+ opacity: 0.7;
126
+ }
127
+ .form-tab .tab-close:hover {
128
+ opacity: 1;
129
+ background: rgba(220, 38, 38, 0.16);
130
+ color: #dc2626;
131
+ }
132
+ .obj-footer-actions {
133
+ @apply --horizontal;
134
+ @apply --no-flex;
135
+ align-items: center;
136
+ gap: 6px;
137
+ }
138
+ button.obj-footer-btn {
139
+ @apply --horizontal;
140
+ @apply --no-flex;
141
+ @apply --content;
142
+ align-items: center;
143
+ justify-content: center;
144
+ gap: 5px;
145
+ min-height: 24px;
146
+ padding: 3px 12px;
147
+ font-size: 12.5px;
148
+ font-weight: 600;
149
+ border: 1px solid var(--border-color);
150
+ border-radius: 3px;
151
+ cursor: pointer;
152
+ user-select: none;
153
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06);
154
+ }
155
+ button.obj-footer-btn.primary {
156
+ font-weight: 700;
157
+ min-width: 58px;
158
+ }
159
+ button.obj-footer-btn.icon-only {
160
+ padding: 3px 7px;
161
+ }
162
+ button.obj-footer-btn:hover:not(:disabled) {
163
+ border-color: var(--accent-color);
164
+ background: var(--accent-soft);
165
+ }
166
+ button.obj-footer-btn:disabled {
167
+ opacity: 0.45;
168
+ cursor: default;
169
+ }
72
170
  </style>
73
171
  <div ~show="!fullScreen" accent-invert slot="header" shadow horizontal flex style="padding: 2px; gap: 4px; align-items: center;">
74
172
  <div center flex horizontal style="overflow: hidden; flex-wrap: balance; gap: 4px; margin: 4px;">
@@ -110,7 +208,7 @@ ODA({is: 'work-form',
110
208
  </div>
111
209
  <div id="tools" raised no-flex horizontal style="min-height: 32px; align-items: center; gap: 8px; border-bottom: 1px solid;">
112
210
  <item-node
113
- ~for="formViews"
211
+ ~for="visibleFormViews"
114
212
  :$item="$for.item"
115
213
  :hide-label="view?.id !== $for.item.id"
116
214
  :icon-size
@@ -143,10 +241,23 @@ ODA({is: 'work-form',
143
241
  </div>
144
242
  </div>
145
243
  <div id="formFooter" slot="footer" ~show="hasFooter" header horizontal no-flex style="justify-items: space-between; align-items: center; padding: 4px 8px; min-height: 34px; gap: 6px; box-sizing: border-box; border-top: 1px solid var(--border-color);">
146
- <item-tools id="mainTools" :$item :state-version="focusedStateVersion" filter="service" ::tool-items="mainToolItems" @tools-changed="onToolsListChanged('mainToolItems', $event)" show-labels></item-tools>
147
244
  <item-tools id="focusedTools" focused-only :target-item="focusedItem" :fallback-item="fallbackFocusedItem" :$item="focusedItem" :state-version="focusedStateVersion" filter="service" ::tool-items="focusedToolItems" @tools-changed="onToolsListChanged('focusedToolItems', $event)" show-labels></item-tools>
148
245
  <slot name="footer-left"></slot>
149
246
  <div flex></div>
247
+ <div class="obj-footer-actions" ~if="isObjectViewActive">
248
+ <button type="button" class="obj-footer-btn primary" ~if="view_control?.canSave" :disabled="view_control?.busy" @tap="onObjectOk()">OK</button>
249
+ <button type="button" class="obj-footer-btn icon-only" ~if="view_control?.canSave" :disabled="view_control?.busy" title="Сохранить" @tap="onObjectSave()">
250
+ <oda-icon icon="icons:save" icon-size="16"></oda-icon>
251
+ </button>
252
+ <button type="button" class="obj-footer-btn" :disabled="view_control?.busy" @tap="onObjectClose()">Закрыть</button>
253
+ </div>
254
+ <div class="form-tabs-bar" ~if="formTabs?.length > 1">
255
+ <div class="form-tab" ~for="formTabs" ~class="{active: activeTabKey === $for.item.key}" :title="$for.item.label" @tap="activateTab($for.item)">
256
+ <oda-icon ~if="$for.item.icon" :icon="$for.item.icon" icon-size="15"></oda-icon>
257
+ <span class="tab-label">{{$for.item.label}}</span>
258
+ <span class="tab-close" ~if="$for.item.closable" title="Закрыть вкладку" @tap.stop="closeTab($for.item, $event)">×</span>
259
+ </div>
260
+ </div>
150
261
  <div no-flex horizontal ~show="dialog" style="gap: 6px;">
151
262
  <oda-button info-invert icon="icons:check" :icon-size @tap="ok" label="OK"></oda-button>
152
263
  <oda-button icon="icons:close" :icon-size @tap="cancel" label="Отмена"></oda-button>
@@ -293,7 +404,10 @@ ODA({is: 'work-form',
293
404
  }
294
405
  },
295
406
  async ready() {
296
- window.execute ??= ($item, nav) => {
407
+ window.execute = ($item, nav) => {
408
+ if ($item && $item.type !== '$handler') {
409
+ return this.openInForm($item, nav);
410
+ }
297
411
  const short = $item?.short || $item?.path || '';
298
412
  if (nav && short)
299
413
  this._saveNav(short, nav);
@@ -302,11 +416,11 @@ ODA({is: 'work-form',
302
416
  return;
303
417
  }
304
418
  let url = short + '/';
305
- if ($item.type !== '$handler')
306
- url += '~/handlers/pages/' + ($item.page || 'form') + '/'
419
+ if ($item?.type !== '$handler')
420
+ url += '~/handlers/pages/' + ($item?.page || 'form') + '/'
307
421
  url = encodeURI(url);
308
422
  window.open(url, url)
309
- }
423
+ };
310
424
  // для закрытия вкладки браузера (form без explorer)
311
425
  if (window.parent !== window)
312
426
  window.close = () => {
@@ -379,6 +493,12 @@ ODA({is: 'work-form',
379
493
  return [];
380
494
  return this.loadFormViews($item, role);
381
495
  },
496
+ get visibleFormViews() {
497
+ const views = this.formViews;
498
+ if (!Array.isArray(views))
499
+ return [];
500
+ return views.filter(v => !(v?.DATA?.hiddenInViewList || v?.hiddenInViewList) || v?.id === this.view?.id);
501
+ },
382
502
  async loadFormViews($item, role) {
383
503
  try {
384
504
  const root = await $item.fetch('handlers', { path: '//form', role });
@@ -436,14 +556,23 @@ ODA({is: 'work-form',
436
556
  this._savePath = ($class?.short || this.$item?.short) + '/' + this.localName + (this.$saveKey ? '[' + this.$saveKey + ']' : '');
437
557
 
438
558
  await n.ensureRole?.();
559
+ if (this._switchingTab)
560
+ return;
439
561
  const nav = this._takeNav(n.short || n.path);
440
- if (nav)
562
+ if (nav) {
563
+ if (nav.view === 'objects' && nav.id)
564
+ nav.view = 'object';
441
565
  this._pendingNav = nav;
566
+ }
442
567
  const view_name = nav?.view || this.host.default_view || n?.form || this.host.view_name;
443
568
  this.view ||= await this.resolveView(view_name);
444
569
  }
445
570
  }
446
571
  },
572
+ formTabs: [],
573
+ activeTabKey: '',
574
+ _newObjSeq: 0,
575
+ _switchingTab: false,
447
576
  topMainToolItems: [],
448
577
  topFocusedToolItems: [],
449
578
  mainToolItems: [],
@@ -451,11 +580,16 @@ ODA({is: 'work-form',
451
580
  hasSlottedToolbar: false,
452
581
  focusedStateVersion: 0,
453
582
  fallbackFocusedItem: null,
583
+ get isObjectViewActive() {
584
+ return Boolean(this.view?.id === 'object' || this.view_control?.isObjectView);
585
+ },
454
586
  get hasToolbar() {
587
+ if (this.view?.allowTools === false || this.view?.DATA?.allowTools === false)
588
+ return false;
455
589
  return Boolean(this.topMainToolItems?.length || this.topFocusedToolItems?.length || this.hasSlottedToolbar);
456
590
  },
457
591
  get hasFooter() {
458
- return Boolean(this.dialog || this.mainToolItems?.length || this.focusedToolItems?.length);
592
+ return Boolean(this.dialog || this.isObjectViewActive || this.focusedToolItems?.length || (this.formTabs?.length > 1));
459
593
  },
460
594
  _checkToolbarSlots() {
461
595
  const names = ['tool-left', 'tool-center', 'tool-right'];
@@ -471,7 +605,7 @@ ODA({is: 'work-form',
471
605
  this.async(() => this._wakeBars(), 5);
472
606
  },
473
607
  _wakeBars() {
474
- for (const sel of ['#formToolbar', '#topMainTools', '#topFocusedTools', '#formFooter', '#mainTools', '#focusedTools']) {
608
+ for (const sel of ['#formToolbar', '#topMainTools', '#topFocusedTools', '#formFooter', '#focusedTools']) {
475
609
  const node = this.$?.(sel);
476
610
  if (node) {
477
611
  if (node[R]?.states)
@@ -486,7 +620,7 @@ ODA({is: 'work-form',
486
620
  this.fallbackFocusedItem = vc.fallbackFocusedItem || null;
487
621
  this.focusedItem = vc.$focusedItem !== undefined ? vc.$focusedItem : (vc.focusedItem || null);
488
622
  this.focusedStateVersion++;
489
- for (const [sel, isFocused] of [['#topMainTools', false], ['#topFocusedTools', true], ['#mainTools', false], ['#focusedTools', true]]) {
623
+ for (const [sel, isFocused] of [['#topMainTools', false], ['#topFocusedTools', true], ['#focusedTools', true]]) {
490
624
  const t = this.$?.(sel);
491
625
  if (t) {
492
626
  t.$item = this.$item;
@@ -622,6 +756,224 @@ ODA({is: 'work-form',
622
756
  this.host.view_name = handler.id;
623
757
  this.view = handler;
624
758
  },
759
+ _computeTabKey($item, viewId, nav = null) {
760
+ const short = $item?.short || $item?.path || '';
761
+ if (viewId === 'object') {
762
+ const objKey = nav?.id ? `id:${nav.id}` : nav?.copyFrom ? `copy:${nav.copyFrom}` : `new:${nav?._seq || (++this._newObjSeq)}`;
763
+ if (nav && !nav.id && !nav.copyFrom && !nav._seq)
764
+ nav._seq = Number(objKey.slice(4));
765
+ return `${short}::object::${objKey}`;
766
+ }
767
+ return `${short}::${viewId || ''}`;
768
+ },
769
+ _computeTabLabel($item, viewHandler, nav = null) {
770
+ if (nav?.label)
771
+ return String(nav.label);
772
+ const rawLabel = $item?.DATA?.label || $item?.label || $item?.id || ($item?.short || '').split('/').pop() || 'Форма';
773
+ const code = ($item?.short || $item?.path || '').split('/').pop() || '';
774
+ const isAcc = $item?.type === '$account' || /^\d+(?:\.\d+)?$/.test(code);
775
+ const itemTitle = (isAcc && code && !String(rawLabel).startsWith(code + ' ') && String(rawLabel) !== code)
776
+ ? `${code} ${rawLabel}`
777
+ : String(rawLabel);
778
+ return itemTitle;
779
+ },
780
+ _ensureCurrentTab(viewHandler = this.view, nav = null) {
781
+ if (!this.$item || !viewHandler)
782
+ return null;
783
+ const viewId = viewHandler.id || '';
784
+ const list = Array.isArray(this.formTabs) ? [...this.formTabs] : [];
785
+ if (list.length === 0) {
786
+ const key = this._computeTabKey(this.$item, viewId, nav);
787
+ const rootTab = {
788
+ key,
789
+ $item: this.$item,
790
+ viewId,
791
+ view: viewHandler,
792
+ nav: nav || null,
793
+ label: this._computeTabLabel(this.$item, viewHandler, nav),
794
+ icon: viewHandler.DATA?.icon || viewHandler.icon || this.$item.icon || 'icons:dashboard',
795
+ closable: false,
796
+ };
797
+ this.formTabs = [rootTab];
798
+ this.activeTabKey = key;
799
+ return rootTab;
800
+ }
801
+ let cur = list.find(t => t.key === this.activeTabKey);
802
+ if (cur && cur.$item === this.$item && cur.viewId !== 'object' && viewId !== 'object') {
803
+ const newKey = this._computeTabKey(this.$item, viewId, nav);
804
+ if (cur.key !== newKey && !list.some(t => t.key === newKey)) {
805
+ cur.key = newKey;
806
+ this.activeTabKey = newKey;
807
+ }
808
+ cur.viewId = viewId;
809
+ cur.view = viewHandler;
810
+ cur.icon = viewHandler.DATA?.icon || viewHandler.icon || this.$item.icon || cur.icon;
811
+ this.formTabs = [...list];
812
+ }
813
+ return cur || list[0];
814
+ },
815
+ updateActiveTabLabel(newLabel) {
816
+ if (!newLabel || !Array.isArray(this.formTabs))
817
+ return;
818
+ const list = [...this.formTabs];
819
+ const cur = list.find(t => t.key === this.activeTabKey);
820
+ if (cur && cur.label !== newLabel) {
821
+ cur.label = String(newLabel);
822
+ this.formTabs = list;
823
+ }
824
+ },
825
+ async openInForm(targetItem, rawNav = null) {
826
+ if (!targetItem)
827
+ return;
828
+ await targetItem.ensureRole?.();
829
+ const nav = rawNav && typeof rawNav === 'object' ? { ...rawNav } : {};
830
+ if (nav.view === 'objects' && nav.id) {
831
+ nav.view = 'object';
832
+ }
833
+ // Зафиксируем текущую (начальную) вкладку перед открытием дочерней
834
+ this._ensureCurrentTab(this.view);
835
+
836
+ const prevItem = this.$item;
837
+ this._switchingTab = true;
838
+ this.$item = targetItem;
839
+ this._switchingTab = false;
840
+
841
+ const targetViewName = nav.view || targetItem.form || '';
842
+ const viewHandler = await this.resolveView(targetViewName);
843
+ if (!viewHandler) {
844
+ this._switchingTab = true;
845
+ this.$item = prevItem;
846
+ this._switchingTab = false;
847
+ return;
848
+ }
849
+ const viewId = viewHandler.id;
850
+ const key = this._computeTabKey(targetItem, viewId, nav);
851
+ const list = Array.isArray(this.formTabs) ? [...this.formTabs] : [];
852
+ let tab = list.find(t => t.key === key);
853
+ if (!tab) {
854
+ tab = {
855
+ key,
856
+ $item: targetItem,
857
+ viewId,
858
+ view: viewHandler,
859
+ nav,
860
+ label: this._computeTabLabel(targetItem, viewHandler, nav),
861
+ icon: viewHandler.DATA?.icon || viewHandler.icon || targetItem.icon || 'icons:dashboard',
862
+ closable: list.length > 0,
863
+ };
864
+ list.push(tab);
865
+ this.formTabs = list;
866
+ }
867
+ else {
868
+ tab.nav = nav;
869
+ tab.view = viewHandler;
870
+ if (nav.label)
871
+ tab.label = String(nav.label);
872
+ this.formTabs = list;
873
+ }
874
+ await this.activateTab(tab, nav);
875
+ },
876
+ async activateTab(tab, explicitNav = null) {
877
+ if (!tab || !tab.$item)
878
+ return;
879
+ this.activeTabKey = tab.key;
880
+ if (this.$item !== tab.$item) {
881
+ this._switchingTab = true;
882
+ this.$item = tab.$item;
883
+ this._switchingTab = false;
884
+ }
885
+ const viewHandler = tab.view || await this.resolveView(tab.viewId);
886
+ if (!viewHandler)
887
+ return;
888
+ tab.view = viewHandler;
889
+ if (explicitNav)
890
+ this._pendingNav = explicitNav;
891
+ if (this.view === viewHandler) {
892
+ await this._mountActiveView(viewHandler);
893
+ }
894
+ else {
895
+ this.view = viewHandler;
896
+ }
897
+ },
898
+ async closeTab(tab, e) {
899
+ e?.stopPropagation?.();
900
+ const list = Array.isArray(this.formTabs) ? [...this.formTabs] : [];
901
+ const idx = list.findIndex(t => t.key === tab?.key);
902
+ if (idx <= 0)
903
+ return;
904
+ const targetTab = list[idx];
905
+ const ctrl = targetTab.control || this.controls[targetTab.key];
906
+ if (ctrl && typeof ctrl.isDirty === 'function' && ctrl.isDirty()) {
907
+ if (typeof ctrl.confirmClose === 'function') {
908
+ const canClose = await ctrl.confirmClose();
909
+ if (!canClose)
910
+ return;
911
+ }
912
+ }
913
+ if (ctrl && this.contains(ctrl))
914
+ this.removeChild(ctrl);
915
+ delete this.controls[targetTab.key];
916
+ list.splice(idx, 1);
917
+ this.formTabs = list;
918
+ if (this.activeTabKey === targetTab.key && list.length) {
919
+ const nextTab = list[Math.max(0, idx - 1)];
920
+ await this.activateTab(nextTab);
921
+ }
922
+ },
923
+ async closeActiveTab(reloadPrevious = false) {
924
+ const list = Array.isArray(this.formTabs) ? [...this.formTabs] : [];
925
+ const idx = list.findIndex(t => t.key === this.activeTabKey);
926
+ if (idx <= 0)
927
+ return;
928
+ const targetTab = list[idx];
929
+ const ctrl = targetTab.control || this.controls[targetTab.key];
930
+ if (ctrl && this.contains(ctrl))
931
+ this.removeChild(ctrl);
932
+ delete this.controls[targetTab.key];
933
+ list.splice(idx, 1);
934
+ this.formTabs = list;
935
+ const nextTab = list[Math.max(0, idx - 1)];
936
+ if (nextTab) {
937
+ await this.activateTab(nextTab);
938
+ if (reloadPrevious && nextTab.control && typeof nextTab.control.reload === 'function') {
939
+ await nextTab.control.reload();
940
+ }
941
+ }
942
+ },
943
+ async onObjectOk() {
944
+ const vc = this.view_control;
945
+ if (!vc)
946
+ return;
947
+ if (typeof vc.saveAndClose === 'function') {
948
+ await vc.saveAndClose();
949
+ return;
950
+ }
951
+ if (typeof vc.save === 'function') {
952
+ const ok = await vc.save();
953
+ if (!ok)
954
+ return;
955
+ }
956
+ await this.closeActiveTab(true);
957
+ },
958
+ async onObjectSave() {
959
+ const vc = this.view_control;
960
+ if (!vc)
961
+ return;
962
+ if (typeof vc.saveAndStay === 'function') {
963
+ await vc.saveAndStay();
964
+ return;
965
+ }
966
+ await vc.save?.();
967
+ },
968
+ async onObjectClose() {
969
+ const list = Array.isArray(this.formTabs) ? this.formTabs : [];
970
+ const cur = list.find(t => t.key === this.activeTabKey);
971
+ if (cur && cur.closable) {
972
+ await this.closeTab(cur);
973
+ return;
974
+ }
975
+ await this.close();
976
+ },
625
977
  ok(e) {
626
978
  alert('ok')
627
979
  },
@@ -629,72 +981,79 @@ ODA({is: 'work-form',
629
981
  alert('cancel')
630
982
  },
631
983
  controls: {},
632
- view: {
633
- async set(n) {
634
- if (!n) return;
635
- let el = this.controls[n.id];
984
+ async _mountActiveView(n) {
985
+ if (!n) return;
986
+ const nav = this._pendingNav || this._takeNav(this.$item?.short || this.$item?.path);
987
+ const curTab = this._ensureCurrentTab(n, nav);
988
+ const cacheKey = curTab?.key || `${this.$item?.short || ''}::${n.id}`;
989
+ let el = curTab?.control || this.controls[cacheKey];
990
+ for (let id in this.controls) {
991
+ let old = this.controls[id];
992
+ if (old !== el && this.contains(old))
993
+ this.removeChild(old);
994
+ }
995
+ if (!el) {
996
+ const ext = String(this.$item?.ext || '').toLowerCase();
997
+ const is = (n.id === 'file' && ext) ? `item-file-${ext}` : `item-${n.id}`;
998
+ await n.importView(is);
999
+ if (this.view !== n)
1000
+ return;
1001
+ if (!customElements.get(is)) {
1002
+ console.error(`[work-form] вид '${n.id}' не зарегистрирован как <${is}>`);
1003
+ return;
1004
+ }
1005
+ el = curTab?.control || this.controls[cacheKey];
1006
+ if (!el) {
1007
+ el = ODA.createComponent(is, { $item: this.$item, $context: this.$item, slot: 'main', $handler: n });
1008
+ el.$form = this;
1009
+ this.controls[cacheKey] = el;
1010
+ this.controls[n.id] = el;
1011
+ if (curTab)
1012
+ curTab.control = el;
1013
+ }
636
1014
  for (let id in this.controls) {
637
1015
  let old = this.controls[id];
638
1016
  if (old !== el && this.contains(old))
639
1017
  this.removeChild(old);
640
1018
  }
641
- if (!el) {
642
- const ext = String(this.$item?.ext || '').toLowerCase();
643
- const is = (n.id === 'file' && ext) ? `item-file-${ext}` : `item-${n.id}`;
644
- await n.importView(is);
645
- if (this.view !== n)
646
- return;
647
- if (!customElements.get(is)) {
648
- // Вид не зарегистрирован под нужным тегом — создавать элемент
649
- // бессмысленно: он останется пустым (как на скриншоте).
650
- console.error(`[work-form] вид '${n.id}' не зарегистрирован как <${is}>`);
651
- return;
652
- }
653
- // Сеттер асинхронный: пока грузился модуль, параллельный вызов (роль, повторный $item)
654
- // мог уже создать это представление — второй экземпляр не нужен.
655
- el = this.controls[n.id];
656
- if (!el) {
657
- el = ODA.createComponent(is, { $item: this.$item, $context: this.$item, slot: 'main', $handler: n });
658
- el.$form = this;
659
- this.controls[n.id] = el;
660
- }
661
- for (let id in this.controls) {
662
- let old = this.controls[id];
663
- if (old !== el && this.contains(old))
664
- this.removeChild(old);
665
- }
666
- if (!el.isConnected)
667
- this.appendChild(el);
668
- }
669
- else if (!el.isConnected)
1019
+ if (!el.isConnected)
670
1020
  this.appendChild(el);
671
- el.$form = this;
672
- for (const oldSlotEl of [
673
- ...this.querySelectorAll('[slot="tool-left"], [slot="tool-center"], [slot="tool-right"], [slot="footer-left"]'),
674
- ...(this.$?.('#formToolbar')?.querySelectorAll?.('[data-toolbar-widget]') || []),
675
- ]) {
676
- if (oldSlotEl._ownerView && oldSlotEl._ownerView !== el)
677
- oldSlotEl.remove();
678
- }
679
- this.view_control = el;
1021
+ }
1022
+ else if (!el.isConnected)
1023
+ this.appendChild(el);
1024
+ el.$form = this;
1025
+ if (curTab)
1026
+ curTab.control = el;
1027
+ for (const oldSlotEl of [
1028
+ ...this.querySelectorAll('[slot="tool-left"], [slot="tool-center"], [slot="tool-right"], [slot="footer-left"]'),
1029
+ ...(this.$?.('#formToolbar')?.querySelectorAll?.('[data-toolbar-widget]') || []),
1030
+ ]) {
1031
+ if (oldSlotEl._ownerView && oldSlotEl._ownerView !== el)
1032
+ oldSlotEl.remove();
1033
+ }
1034
+ this.view_control = el;
1035
+ if (n.allowTools !== false && n.DATA?.allowTools !== false)
680
1036
  el.mountToolbarSlots?.(this);
681
- this._checkToolbarSlots();
682
- this.syncFromView(el);
683
- this.host.view_name = n.id;
684
- const nav = this._pendingNav || this._takeNav(this.$item?.short || this.$item?.path);
685
- if (nav && typeof el?.applyNav === 'function') {
686
- this._pendingNav = null;
687
- Promise.resolve(el.applyNav(nav)).catch(err => console.error(err));
1037
+ this._checkToolbarSlots();
1038
+ this.syncFromView(el);
1039
+ this.host.view_name = n.id;
1040
+ if (nav && typeof el?.applyNav === 'function') {
1041
+ this._pendingNav = null;
1042
+ Promise.resolve(el.applyNav(nav)).catch(err => console.error(err));
1043
+ }
1044
+ try {
1045
+ const exp = window.top?.explorer || window.parent?.explorer;
1046
+ const cons = exp?.$?.('#consultant') || exp?.$?.('item-consultant');
1047
+ if (cons) {
1048
+ const ctx = this.buildConsultantContext();
1049
+ cons.activeFormContext = ctx;
1050
+ cons.activeFormSummary = ctx;
688
1051
  }
689
- try {
690
- const exp = window.top?.explorer || window.parent?.explorer;
691
- const cons = exp?.$?.('#consultant') || exp?.$?.('item-consultant');
692
- if (cons) {
693
- const ctx = this.buildConsultantContext();
694
- cons.activeFormContext = ctx;
695
- cons.activeFormSummary = ctx;
696
- }
697
- } catch { /* ignore */ }
1052
+ } catch { /* ignore */ }
1053
+ },
1054
+ view: {
1055
+ async set(n) {
1056
+ await this._mountActiveView(n);
698
1057
  }
699
1058
  },
700
1059
  $listeners: {
@@ -159,6 +159,10 @@ export default {
159
159
  const vc = host?.view_control;
160
160
  const filter = this.filter;
161
161
  const focusedOnly = this.focusedOnly;
162
+ if (filter === 'button' && (host?.view?.allowTools === false || host?.view?.DATA?.allowTools === false)) {
163
+ this._setTools([]);
164
+ return;
165
+ }
162
166
  const rawTarget = this.targetItem;
163
167
  const isMulti = Array.isArray(rawTarget) && rawTarget.length > 1;
164
168
  const singleTarget = Array.isArray(rawTarget) ? (rawTarget[0] || null) : rawTarget;
@@ -276,7 +280,7 @@ export default {
276
280
  tool.$handler.execute(e);
277
281
  return;
278
282
  }
279
- if (tool.type === '$handler' && target && window.execute) {
283
+ if (tool.type === '$handler' && target && (typeof host?.openInForm === 'function' || window.execute)) {
280
284
  const nav = typeof vc?.buildDrilldownNav === 'function'
281
285
  ? vc.buildDrilldownNav(tool.id, target)
282
286
  : {
@@ -287,7 +291,10 @@ export default {
287
291
  packetsKey: vc?.packetsKey || '',
288
292
  unit: vc?.unit || '',
289
293
  };
290
- window.execute(target, nav);
294
+ if (typeof host?.openInForm === 'function')
295
+ host.openInForm(target, nav);
296
+ else
297
+ window.execute(target, nav);
291
298
  return;
292
299
  }
293
300
  if (typeof tool.execute === 'function')