dsh-sidebar-suite 0.2.9 → 0.4.0
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.
- package/CHANGELOG.md +36 -1
- package/DETAIL.md +57 -1
- package/README.md +72 -2
- package/lib/client.js +1016 -4
- package/lib/temp-session-host.mjs +69 -1
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +3 -3
- package/temp-session-config.txt +7 -0
- package//344/275/277/347/224/250/350/257/264/346/230/216.md +40 -8
package/lib/client.js
CHANGED
|
@@ -19,8 +19,15 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
'use strict'
|
|
20
20
|
|
|
21
21
|
var ENTRY = 'dsh-sidebar-suite'
|
|
22
|
+
// Reported to tab consumers so they can feature-detect. Kept in lockstep
|
|
23
|
+
// with package.json by tests/test_030.mjs.
|
|
24
|
+
var VERSION = '0.4.0'
|
|
22
25
|
var NS = 'sidebar-suite'
|
|
23
26
|
var LS_KEY = 'dsh-sidebar-suite:state'
|
|
27
|
+
// Where the third-party tab choice lives. Kept out of LS_KEY on purpose:
|
|
28
|
+
// the config form describes the sidebar, this describes the user's
|
|
29
|
+
// current position inside it, and the two have different lifetimes.
|
|
30
|
+
var TAB_STORE_KEY = 'dsh-sidebar-suite:active-tab'
|
|
24
31
|
var STYLE_ID = 'dsh-sidebar-suite-style'
|
|
25
32
|
// Where feedback goes. A plugin bundle cannot open a mail client or post a
|
|
26
33
|
// form without a server, so the entry point assembles a ready-to-send
|
|
@@ -79,6 +86,10 @@ window.__ModuleLoader__.load({
|
|
|
79
86
|
'tab.to.ext.hint': 'Switch the shared space to the panels group.',
|
|
80
87
|
'tab.to.ws': 'Show workspaces',
|
|
81
88
|
'tab.to.ws.hint': 'Switch the shared space to the workspaces group.',
|
|
89
|
+
'tab.ext': 'Panels',
|
|
90
|
+
'tab.ws': 'Workspaces',
|
|
91
|
+
'tabs.label': 'Sidebar pages',
|
|
92
|
+
'tabs.own': 'This page is provided by {name}.',
|
|
82
93
|
'pop.open': 'Open panels',
|
|
83
94
|
'pop.close': 'Close panels',
|
|
84
95
|
'pop.hint': 'Open or close the panels overlay.',
|
|
@@ -95,6 +106,15 @@ window.__ModuleLoader__.load({
|
|
|
95
106
|
'temp.feedbackHint': 'Report it on the plugin detail page.',
|
|
96
107
|
'temp.restartHint': 'Fully quit DSH (tray icon → Quit) and reopen it — a hot reload does not reload the plugin host.',
|
|
97
108
|
'temp.badge': 'TEMP',
|
|
109
|
+
'temp.menu.label': 'Temporary session actions',
|
|
110
|
+
'temp.menu.keep': 'Keep as a normal session',
|
|
111
|
+
'temp.menu.keep.hint': 'It will no longer be deleted when DSH quits.',
|
|
112
|
+
'temp.menu.delete': 'Delete now',
|
|
113
|
+
'temp.menu.delete.hint': 'Removes this session from disk. It cannot be undone.',
|
|
114
|
+
'temp.menu.kept': 'Kept as a normal session — it now survives quitting DSH.',
|
|
115
|
+
'temp.menu.deleted': 'Temporary session deleted.',
|
|
116
|
+
'temp.menu.hidden': 'Files were locked, so the row was hidden instead. Deletion is retried next launch.',
|
|
117
|
+
'temp.menu.failed': 'Action failed: ',
|
|
98
118
|
'policy.title': 'Temporary session cleanup',
|
|
99
119
|
'policy.help': 'Choose when temporary sessions are removed. Defaults: delete on quit = on, daily cleanup = off. Saved to the plugin settings file and applied immediately — no restart.',
|
|
100
120
|
'policy.onQuit': 'Delete when DSH quits',
|
|
@@ -129,6 +149,10 @@ window.__ModuleLoader__.load({
|
|
|
129
149
|
'tab.to.ext.hint': '把共用的一栏切换到扩展区。',
|
|
130
150
|
'tab.to.ws': '显示工作区',
|
|
131
151
|
'tab.to.ws.hint': '把共用的一栏切换到工作区。',
|
|
152
|
+
'tab.ext': '扩展',
|
|
153
|
+
'tab.ws': '工作区',
|
|
154
|
+
'tabs.label': '侧栏页签',
|
|
155
|
+
'tabs.own': '这一页由 {name} 提供。',
|
|
132
156
|
'pop.open': '展开扩展',
|
|
133
157
|
'pop.close': '收起扩展',
|
|
134
158
|
'pop.hint': '打开或收起扩展浮层。',
|
|
@@ -145,6 +169,15 @@ window.__ModuleLoader__.load({
|
|
|
145
169
|
'temp.feedbackHint': '可在插件详情页反馈。',
|
|
146
170
|
'temp.restartHint': '请从托盘图标彻底退出 DSH 再打开 —— 热重载不会重载插件后端。',
|
|
147
171
|
'temp.badge': '临时',
|
|
172
|
+
'temp.menu.label': '临时会话操作',
|
|
173
|
+
'temp.menu.keep': '保留为正式会话',
|
|
174
|
+
'temp.menu.keep.hint': '退出 DSH 时不再删除它。',
|
|
175
|
+
'temp.menu.delete': '立即删除',
|
|
176
|
+
'temp.menu.delete.hint': '把这个会话从磁盘删掉,无法恢复。',
|
|
177
|
+
'temp.menu.kept': '已保留为正式会话:退出 DSH 时不会再删除它。',
|
|
178
|
+
'temp.menu.deleted': '临时会话已删除。',
|
|
179
|
+
'temp.menu.hidden': '文件被占用,已先把它从列表里隐藏,下次启动再删。',
|
|
180
|
+
'temp.menu.failed': '操作失败:',
|
|
148
181
|
'policy.title': '临时会话自动删除',
|
|
149
182
|
'policy.help': '决定临时会话什么时候被清掉。默认:退出 DSH 时删除=开,每天定时清理=关。写入插件设置文件,改完立即生效,不用重启。',
|
|
150
183
|
'policy.onQuit': '退出 DSH 时删除',
|
|
@@ -191,6 +224,8 @@ window.__ModuleLoader__.load({
|
|
|
191
224
|
'options.styleOwn': '独占一行 / Own row',
|
|
192
225
|
'options.expandAll': '会话默认全展开 / Always expand sessions',
|
|
193
226
|
'options.expandHint': '关掉后恢复 DSH 原来的折叠行为;已经展开的列表要切换一次工作区才会收回去。 / Turning it off restores host folding; already-expanded groups collapse after you switch workspaces once.',
|
|
227
|
+
'options.rowMenu': '临时会话右键菜单 / Right-click menu on temp sessions',
|
|
228
|
+
'options.rowMenuHint': '右键临时会话可「保留为正式会话」或「立即删除」。若和别的插件抢右键菜单,关掉这一项即可。 / Right-clicking a temp session offers "keep as normal" or "delete now". Turn this off if another plugin owns the context menu.',
|
|
194
229
|
'options.maxKept': '最多保留 / Keep at most',
|
|
195
230
|
'options.maxKeptHint': '个临时会话(1–200) / temp sessions (1–200)',
|
|
196
231
|
'options.purge': '立即清理临时会话 / Clean up now',
|
|
@@ -299,6 +334,19 @@ window.__ModuleLoader__.load({
|
|
|
299
334
|
// In the collapsed rail there is no horizontal room for two buttons.
|
|
300
335
|
'html[data-dsbs-temp-solo="true"] [data-dsbs-temp-slot]{display:none!important}',
|
|
301
336
|
'html[data-dsbs-temp-solo="true"]:not([data-dsbs-chat-mode]) [data-dsbs-newchat] button[class*="newSession"]{width:auto!important;height:38px!important;font-size:14px!important;line-height:22px!important;margin:0 calc(var(--dsh-sidebar-paint-inset,3px) + 1px) 10px!important}',
|
|
337
|
+
// ---- per-row menu on temporary sessions (0.4.0) -------------------
|
|
338
|
+
// Right-clicking a temporary row offers "keep as normal" and "delete
|
|
339
|
+
// now". Styled from host tokens like every other surface here; the row
|
|
340
|
+
// itself gains no children, so React re-renders cannot wipe it.
|
|
341
|
+
'[data-dsbs-temp-menu]{position:fixed;z-index:9400;min-width:196px;max-width:min(280px,calc(100vw - 16px));box-sizing:border-box;padding:4px;display:flex;flex-direction:column;gap:1px;border-radius:10px;background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base,#1f1f1f));color:var(--dsw-alias-label-primary,inherit);border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.3));box-shadow:0 12px 32px rgba(0,0,0,.32)}',
|
|
342
|
+
'[data-dsbs-temp-menu][hidden]{display:none!important}',
|
|
343
|
+
'[data-dsbs-temp-menu-head]{padding:4px 8px 6px;margin-bottom:2px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));font-size:11px;line-height:1.5;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.9));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
344
|
+
'[data-dsbs-temp-menu] button{appearance:none;-webkit-appearance:none;display:block;width:100%;box-sizing:border-box;border:0;background:transparent;color:inherit;font:inherit;font-size:12px;line-height:1.45;text-align:left;padding:6px 8px;border-radius:7px;cursor:pointer;white-space:normal}',
|
|
345
|
+
'[data-dsbs-temp-menu] button:hover,[data-dsbs-temp-menu] button:focus-visible{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.16));outline:none}',
|
|
346
|
+
'[data-dsbs-temp-menu] button:focus-visible{outline:2px solid var(--dsw-alias-label-primary,currentColor);outline-offset:-2px}',
|
|
347
|
+
'[data-dsbs-temp-menu] button[data-dsbs-temp-action="delete"]{color:var(--dsw-alias-state-business-danger,#d9534f)}',
|
|
348
|
+
'[data-dsbs-temp-menu-sub]{display:block;font-size:10px;line-height:1.4;opacity:.68}',
|
|
349
|
+
'html[data-dsbs-temp-menu="false"] [data-dsbs-temp-menu]{display:none!important}',
|
|
302
350
|
// A temporary session is a NORMAL DSH session, so its sidebar row is the
|
|
303
351
|
// host's own row. Nothing is added to the DOM: we tag the row and let CSS
|
|
304
352
|
// draw the badge, which survives every React re-render of the list.
|
|
@@ -356,6 +404,34 @@ window.__ModuleLoader__.load({
|
|
|
356
404
|
'[data-row-key^="session:"]{transition:background-color .13s ease,box-shadow .13s ease,color .13s ease;font-variant-numeric:tabular-nums}',
|
|
357
405
|
'[data-row-key^="session:"][aria-selected="true"]{box-shadow:inset 2px 0 0 var(--dsw-alias-state-business-primary,var(--dsw-alias-label-primary,#4d6bfe))!important}',
|
|
358
406
|
'html[data-dsbs-rail="true"] [data-row-key^="session:"][aria-selected="true"]{box-shadow:none!important}',
|
|
407
|
+
|
|
408
|
+
// ---- third-party page strip (0.3.0) ------------------------------
|
|
409
|
+
// The strip only exists while at least one plugin registered a tab, so a
|
|
410
|
+
// stock profile never sees it. Everything inherits currentColor, which
|
|
411
|
+
// is what keeps it legible in both host themes without a palette here.
|
|
412
|
+
'[data-dsbs-tabs]{display:flex;align-items:center;gap:2px;padding:3px 4px 0;flex:none;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}',
|
|
413
|
+
'[data-dsbs-tabs][hidden]{display:none!important}',
|
|
414
|
+
'[data-dsbs-tabs]::-webkit-scrollbar{display:none}',
|
|
415
|
+
'[data-dsbs-tabs] button{position:relative;display:inline-flex;align-items:center;gap:5px;flex:none;min-width:0;max-width:150px;padding:4px 9px;border:0;border-radius:7px 7px 0 0;background:transparent;color:inherit;font:inherit;font-size:11px;line-height:1.3;opacity:.6;cursor:pointer;white-space:nowrap;transition:opacity .14s ease,background-color .14s ease}',
|
|
416
|
+
'[data-dsbs-tabs] button:hover{opacity:.92;background:color-mix(in srgb,currentColor 8%,transparent)}',
|
|
417
|
+
'[data-dsbs-tabs] button[aria-selected="true"]{opacity:1;font-weight:600;background:color-mix(in srgb,currentColor 12%,transparent);box-shadow:inset 0 -2px 0 var(--dsw-alias-state-business-primary,currentColor)}',
|
|
418
|
+
'[data-dsbs-tablabel]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}',
|
|
419
|
+
'[data-dsbs-tabicon]{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;flex:none}',
|
|
420
|
+
'[data-dsbs-tabicon] svg{width:14px;height:14px;display:block;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}',
|
|
421
|
+
'[data-dsbs-tabicon-text]{font-size:12px;line-height:1}',
|
|
422
|
+
'[data-dsbs-tabbadge]{flex:none;min-width:15px;padding:0 4px;border-radius:7px;font-size:10px;line-height:15px;text-align:center;background:color-mix(in srgb,currentColor 16%,transparent);font-variant-numeric:tabular-nums}',
|
|
423
|
+
// The tab's own container inherits the column the two groups gave up.
|
|
424
|
+
'[data-dsbs-tabpanel]{display:none;flex:1 1 auto;min-height:0;min-width:0;overflow:auto;flex-direction:column}',
|
|
425
|
+
'[data-dsbs-tabpanel][data-dsbs-visible="true"]{display:flex}',
|
|
426
|
+
'html[data-dsbs-ptab="true"] nav[data-dsbs-ext]{display:none!important}',
|
|
427
|
+
'html[data-dsbs-ptab="true"] div:has(> [data-slot="sidebar.workspaces"]){display:none!important}',
|
|
428
|
+
// Narrow: icon-only pages, the same rule the control bar follows. A tab
|
|
429
|
+
// without an icon keeps its label instead of becoming an empty box.
|
|
430
|
+
'html[data-dsbs-narrow="true"] [data-dsbs-tabs] button{max-width:64px;padding:4px 6px}',
|
|
431
|
+
'html[data-dsbs-narrow="true"] [data-dsbs-tabs] button[data-dsbs-hasicon] [data-dsbs-tablabel]{display:none}',
|
|
432
|
+
'html[data-dsbs-narrow="true"] [data-dsbs-tabs] button[data-dsbs-hasicon] [data-dsbs-tabbadge]{display:none}',
|
|
433
|
+
'html[data-dsbs-rail="true"] [data-dsbs-tabs]{padding:2px 2px 0}',
|
|
434
|
+
'html[data-dsbs-rail="true"] [data-dsbs-tabs] button{max-width:40px;padding:4px 5px}',
|
|
359
435
|
].join('\n')
|
|
360
436
|
|
|
361
437
|
function assign(target) {
|
|
@@ -504,6 +580,11 @@ window.__ModuleLoader__.load({
|
|
|
504
580
|
return DICTS[lang][key] || DICTS.en[key] || key
|
|
505
581
|
}
|
|
506
582
|
t.refresh = refresh
|
|
583
|
+
// Third-party tab titles may arrive as `{ zh, en }`; resolving them needs
|
|
584
|
+
// the same detector the dictionaries use, not a second guess.
|
|
585
|
+
t.langName = function () {
|
|
586
|
+
return lang
|
|
587
|
+
}
|
|
507
588
|
return t
|
|
508
589
|
}
|
|
509
590
|
|
|
@@ -559,6 +640,100 @@ window.__ModuleLoader__.load({
|
|
|
559
640
|
resizeObserver: null,
|
|
560
641
|
mutationObserver: null,
|
|
561
642
|
raf: 0,
|
|
643
|
+
// 0.3.0 — third-party tabs. `activeTab` holds a registered id (or null
|
|
644
|
+
// for the built-in groups); `tabPanels` caches one live container per
|
|
645
|
+
// tab so a mount callback runs once and its disposer stays reachable.
|
|
646
|
+
activeTab: readActiveTab(),
|
|
647
|
+
seenTabs: Object.create(null),
|
|
648
|
+
tabStrip: null,
|
|
649
|
+
tabPanels: Object.create(null),
|
|
650
|
+
tabSignature: '',
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// The public surface other plugins see. Everything here is either a
|
|
654
|
+
// function declaration (hoisted, so the object can be built before the
|
|
655
|
+
// implementations read below) or a getter that touches live state only
|
|
656
|
+
// when called.
|
|
657
|
+
var sidebarService = {
|
|
658
|
+
version: VERSION,
|
|
659
|
+
// Returns its own disposer, which is exactly what `ctx.effect(() => …)`
|
|
660
|
+
// wants, so a consumer never has to remember to unregister.
|
|
661
|
+
registerTab: function (descriptor) {
|
|
662
|
+
return registerTab(descriptor)
|
|
663
|
+
},
|
|
664
|
+
unregisterTab: function (id) {
|
|
665
|
+
return unregisterTab(id)
|
|
666
|
+
},
|
|
667
|
+
getTabs: function () {
|
|
668
|
+
return tabSnapshot()
|
|
669
|
+
},
|
|
670
|
+
activateTab: function (id) {
|
|
671
|
+
return activateTab(id)
|
|
672
|
+
},
|
|
673
|
+
getActiveTab: function () {
|
|
674
|
+
return runtime.activeTab && findTab(runtime.activeTab) ? runtime.activeTab : null
|
|
675
|
+
},
|
|
676
|
+
// A consumer can follow the strip instead of polling it: called with
|
|
677
|
+
// (tabs, reason, activeId) whenever the list or the selection changes.
|
|
678
|
+
subscribe: function (listener) {
|
|
679
|
+
if (typeof listener !== 'function') return function () {}
|
|
680
|
+
tabListeners.push(listener)
|
|
681
|
+
var off = false
|
|
682
|
+
return function () {
|
|
683
|
+
if (off) return
|
|
684
|
+
off = true
|
|
685
|
+
var at = tabListeners.indexOf(listener)
|
|
686
|
+
if (at >= 0) tabListeners.splice(at, 1)
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
// Reading the current shape lets a consumer align with the layout
|
|
690
|
+
// instead of sniffing classes off <html>.
|
|
691
|
+
getLayout: function () {
|
|
692
|
+
return {
|
|
693
|
+
mode: config.values.mode,
|
|
694
|
+
tab: config.values.tab,
|
|
695
|
+
activeTab: runtime.activeTab,
|
|
696
|
+
narrow: runtime.narrow === true,
|
|
697
|
+
rail: runtime.rail === true,
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Publish the service once the slot system is live.
|
|
703
|
+
//
|
|
704
|
+
// Two dead ends measured against this client context, both of which take
|
|
705
|
+
// the whole sidebar down:
|
|
706
|
+
// * ctx.provide() directly inside apply() stalls the plugin loader on
|
|
707
|
+
// "Loading plugins..." forever;
|
|
708
|
+
// * a static `inject: ['slots']` never activates, so apply() never runs.
|
|
709
|
+
// Publishing from inside a dependency callback is the shape that works.
|
|
710
|
+
function publishService() {
|
|
711
|
+
try {
|
|
712
|
+
if (typeof ctx.provide === 'function') ctx.provide('sidebarSuite', sidebarService)
|
|
713
|
+
} catch (error) {
|
|
714
|
+
try {
|
|
715
|
+
console.warn('[dsh-sidebar-suite] could not publish the sidebarSuite service:', error)
|
|
716
|
+
} catch (inner) {
|
|
717
|
+
void inner
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (typeof ctx.inject === 'function') {
|
|
722
|
+
try {
|
|
723
|
+
ctx.inject(['slots'], function () { publishService() })
|
|
724
|
+
} catch (error) {
|
|
725
|
+
publishService()
|
|
726
|
+
}
|
|
727
|
+
} else {
|
|
728
|
+
publishService()
|
|
729
|
+
}
|
|
730
|
+
// One window handle for the same object. Not a second API: it exists so a
|
|
731
|
+
// consumer can reach the service from a plain browser context (and so the
|
|
732
|
+
// release checks can drive the strip over CDP).
|
|
733
|
+
try {
|
|
734
|
+
window.__dshSidebarSuite = sidebarService
|
|
735
|
+
} catch (error) {
|
|
736
|
+
void error
|
|
562
737
|
}
|
|
563
738
|
|
|
564
739
|
var manualCleanups = []
|
|
@@ -572,6 +747,12 @@ window.__ModuleLoader__.load({
|
|
|
572
747
|
var configPollPending = false
|
|
573
748
|
var tempChatGuard = null
|
|
574
749
|
var tempIds = [] // session ids the Host half will delete on quit
|
|
750
|
+
// 0.4.0 — per-row menu. `tempMenuEnabled` mirrors TEMP_ROW_MENU and is the
|
|
751
|
+
// escape hatch if another plugin wants the context menu on these rows.
|
|
752
|
+
var tempMenuEnabled = true
|
|
753
|
+
var tempRowMenu = null
|
|
754
|
+
var tempMenuRow = null
|
|
755
|
+
var tempMenuDismiss = null
|
|
575
756
|
var tempBusy = false
|
|
576
757
|
var tempToastTimer = 0
|
|
577
758
|
|
|
@@ -840,6 +1021,275 @@ window.__ModuleLoader__.load({
|
|
|
840
1021
|
}
|
|
841
1022
|
}
|
|
842
1023
|
|
|
1024
|
+
// ---- per-row menu on temporary rows (0.4.0) -----------------------
|
|
1025
|
+
// A temporary session used to be all-or-nothing: either it vanished at
|
|
1026
|
+
// quit, or you turned DELETE_ON_EXIT off and every temp session became a
|
|
1027
|
+
// normal one. Now a single row can be promoted ("keep as a normal
|
|
1028
|
+
// session") or deleted on the spot.
|
|
1029
|
+
//
|
|
1030
|
+
// Both actions already existed on the Host half — `untrack` and the new
|
|
1031
|
+
// `remove` route — so this is purely a missing entrance. It never touches
|
|
1032
|
+
// rows that are not temporary: no preventDefault, no stopPropagation, the
|
|
1033
|
+
// host's own context menu keeps working everywhere else.
|
|
1034
|
+
|
|
1035
|
+
/** Nearest ancestor carrying data-row-key="session:<id>". */
|
|
1036
|
+
function tempRowOf(node) {
|
|
1037
|
+
var el = node
|
|
1038
|
+
var guard = 0
|
|
1039
|
+
while (el && el !== document && guard < 60) {
|
|
1040
|
+
guard += 1
|
|
1041
|
+
var key = el.getAttribute ? el.getAttribute('data-row-key') : ''
|
|
1042
|
+
if (key && key.indexOf('session:') === 0) return el
|
|
1043
|
+
el = el.parentNode
|
|
1044
|
+
}
|
|
1045
|
+
return null
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function tempMenuButtons() {
|
|
1049
|
+
var out = []
|
|
1050
|
+
if (!tempRowMenu) return out
|
|
1051
|
+
var list = null
|
|
1052
|
+
try { list = tempRowMenu.querySelectorAll('button[data-dsbs-temp-action]') } catch (error) { void error }
|
|
1053
|
+
if (!list) return out
|
|
1054
|
+
for (var i = 0; i < list.length; i++) out.push(list[i])
|
|
1055
|
+
return out
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function closeTempMenu(restoreFocus) {
|
|
1059
|
+
if (!tempRowMenu) return false
|
|
1060
|
+
var row = tempMenuRow
|
|
1061
|
+
try { if (tempRowMenu.parentNode) tempRowMenu.parentNode.removeChild(tempRowMenu) } catch (error) { void error }
|
|
1062
|
+
tempRowMenu = null
|
|
1063
|
+
tempMenuRow = null
|
|
1064
|
+
detachTempMenuDismiss()
|
|
1065
|
+
if (restoreFocus && row && typeof row.focus === 'function') {
|
|
1066
|
+
try { row.focus() } catch (error) { void error }
|
|
1067
|
+
}
|
|
1068
|
+
return true
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function detachTempMenuDismiss() {
|
|
1072
|
+
var handlers = tempMenuDismiss
|
|
1073
|
+
if (!handlers) return
|
|
1074
|
+
tempMenuDismiss = null
|
|
1075
|
+
try { document.removeEventListener('pointerdown', handlers.onDown, true) } catch (error) { void error }
|
|
1076
|
+
try { document.removeEventListener('keydown', handlers.onKey, true) } catch (error) { void error }
|
|
1077
|
+
try { window.removeEventListener('resize', handlers.onViewport) } catch (error) { void error }
|
|
1078
|
+
try { window.removeEventListener('scroll', handlers.onViewport, true) } catch (error) { void error }
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function attachTempMenuDismiss() {
|
|
1082
|
+
if (tempMenuDismiss) return
|
|
1083
|
+
var handlers = {
|
|
1084
|
+
onDown: function (event) {
|
|
1085
|
+
if (!tempRowMenu) return
|
|
1086
|
+
var target = event && event.target
|
|
1087
|
+
if (target && tempRowMenu.contains && tempRowMenu.contains(target)) return
|
|
1088
|
+
closeTempMenu(false)
|
|
1089
|
+
},
|
|
1090
|
+
onKey: function (event) {
|
|
1091
|
+
if (!tempRowMenu || !event) return
|
|
1092
|
+
var key = event.key
|
|
1093
|
+
if (key === 'Escape') {
|
|
1094
|
+
event.preventDefault()
|
|
1095
|
+
event.stopPropagation()
|
|
1096
|
+
closeTempMenu(true)
|
|
1097
|
+
return
|
|
1098
|
+
}
|
|
1099
|
+
if (key === 'Tab') { closeTempMenu(false); return }
|
|
1100
|
+
if (key !== 'ArrowDown' && key !== 'ArrowUp') return
|
|
1101
|
+
var buttons = tempMenuButtons()
|
|
1102
|
+
if (!buttons.length) return
|
|
1103
|
+
event.preventDefault()
|
|
1104
|
+
var active = document.activeElement
|
|
1105
|
+
var at = buttons.indexOf(active)
|
|
1106
|
+
var next
|
|
1107
|
+
if (key === 'ArrowDown') next = at < 0 ? 0 : (at + 1) % buttons.length
|
|
1108
|
+
else next = at <= 0 ? buttons.length - 1 : at - 1
|
|
1109
|
+
try { buttons[next].focus() } catch (error) { void error }
|
|
1110
|
+
},
|
|
1111
|
+
// The menu is positioned in viewport coordinates, so any scroll or
|
|
1112
|
+
// resize would leave it floating next to the wrong row.
|
|
1113
|
+
onViewport: function () { closeTempMenu(false) },
|
|
1114
|
+
}
|
|
1115
|
+
tempMenuDismiss = handlers
|
|
1116
|
+
try { document.addEventListener('pointerdown', handlers.onDown, true) } catch (error) { void error }
|
|
1117
|
+
try { document.addEventListener('keydown', handlers.onKey, true) } catch (error) { void error }
|
|
1118
|
+
try { window.addEventListener('resize', handlers.onViewport) } catch (error) { void error }
|
|
1119
|
+
try { window.addEventListener('scroll', handlers.onViewport, true) } catch (error) { void error }
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function makeTempMenuItem(action, labelKey, hintKey, onClick) {
|
|
1123
|
+
var button = document.createElement('button')
|
|
1124
|
+
button.type = 'button'
|
|
1125
|
+
button.setAttribute('role', 'menuitem')
|
|
1126
|
+
button.setAttribute('data-dsbs-temp-action', action)
|
|
1127
|
+
var label = document.createElement('span')
|
|
1128
|
+
label.textContent = t(labelKey)
|
|
1129
|
+
button.appendChild(label)
|
|
1130
|
+
var hint = t(hintKey)
|
|
1131
|
+
if (hint) {
|
|
1132
|
+
var sub = document.createElement('span')
|
|
1133
|
+
sub.setAttribute('data-dsbs-temp-menu-sub', '')
|
|
1134
|
+
sub.textContent = hint
|
|
1135
|
+
button.appendChild(sub)
|
|
1136
|
+
}
|
|
1137
|
+
button.addEventListener('click', function (event) {
|
|
1138
|
+
if (event) {
|
|
1139
|
+
event.preventDefault()
|
|
1140
|
+
event.stopPropagation()
|
|
1141
|
+
}
|
|
1142
|
+
closeTempMenu(false)
|
|
1143
|
+
onClick()
|
|
1144
|
+
})
|
|
1145
|
+
return button
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function openTempMenu(row, id, x, y) {
|
|
1149
|
+
closeTempMenu(false)
|
|
1150
|
+
var menu = document.createElement('div')
|
|
1151
|
+
menu.setAttribute('data-dsbs-temp-menu', '')
|
|
1152
|
+
menu.setAttribute('role', 'menu')
|
|
1153
|
+
menu.setAttribute('aria-label', t('temp.menu.label'))
|
|
1154
|
+
// The row's own text is the only title available (the host owns the
|
|
1155
|
+
// session list), and it is what the user just pointed at.
|
|
1156
|
+
var text = (row.textContent || '').replace(/\s+/g, ' ').trim()
|
|
1157
|
+
if (text) {
|
|
1158
|
+
var head = document.createElement('div')
|
|
1159
|
+
head.setAttribute('data-dsbs-temp-menu-head', '')
|
|
1160
|
+
head.textContent = text.length > 48 ? text.slice(0, 48) + '…' : text
|
|
1161
|
+
menu.appendChild(head)
|
|
1162
|
+
}
|
|
1163
|
+
menu.appendChild(makeTempMenuItem('keep', 'temp.menu.keep', 'temp.menu.keep.hint', function () {
|
|
1164
|
+
keepTempSession(id)
|
|
1165
|
+
}))
|
|
1166
|
+
menu.appendChild(makeTempMenuItem('delete', 'temp.menu.delete', 'temp.menu.delete.hint', function () {
|
|
1167
|
+
deleteTempSession(id)
|
|
1168
|
+
}))
|
|
1169
|
+
document.body.appendChild(menu)
|
|
1170
|
+
|
|
1171
|
+
// Measure first, then flip near the viewport edges. Both reads are
|
|
1172
|
+
// wrapped: a detached or unmeasured node must not break the menu.
|
|
1173
|
+
var width = 0
|
|
1174
|
+
var height = 0
|
|
1175
|
+
try {
|
|
1176
|
+
var rect = menu.getBoundingClientRect ? menu.getBoundingClientRect() : null
|
|
1177
|
+
width = (rect && rect.width) || menu.offsetWidth || 0
|
|
1178
|
+
height = (rect && rect.height) || menu.offsetHeight || 0
|
|
1179
|
+
} catch (error) { void error }
|
|
1180
|
+
if (!width) width = 220
|
|
1181
|
+
if (!height) height = 96
|
|
1182
|
+
var vw = window.innerWidth || 1024
|
|
1183
|
+
var vh = window.innerHeight || 768
|
|
1184
|
+
var left = Number.isFinite(x) ? x : 0
|
|
1185
|
+
var top = Number.isFinite(y) ? y : 0
|
|
1186
|
+
if (left + width > vw - 8) left = Math.max(8, vw - width - 8)
|
|
1187
|
+
if (top + height > vh - 8) top = Math.max(8, vh - height - 8)
|
|
1188
|
+
menu.style.left = Math.round(left) + 'px'
|
|
1189
|
+
menu.style.top = Math.round(top) + 'px'
|
|
1190
|
+
|
|
1191
|
+
tempRowMenu = menu
|
|
1192
|
+
tempMenuRow = row
|
|
1193
|
+
attachTempMenuDismiss()
|
|
1194
|
+
var buttons = tempMenuButtons()
|
|
1195
|
+
if (buttons.length) {
|
|
1196
|
+
try { buttons[0].focus() } catch (error) { void error }
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/** Promote one session: it stops being temporary, nothing is deleted. */
|
|
1201
|
+
function keepTempSession(id) {
|
|
1202
|
+
tempCall('api/dsh-temp-session.untrack?sessionId=' + encodeURIComponent(id), { method: 'POST' })
|
|
1203
|
+
.then(function (data) {
|
|
1204
|
+
if (!data || data.ok !== true) throw new Error((data && data.error) || 'untrack failed')
|
|
1205
|
+
applyTempIdList(data.ids, id)
|
|
1206
|
+
toast(t('temp.menu.kept'), false)
|
|
1207
|
+
scheduleSync()
|
|
1208
|
+
})
|
|
1209
|
+
.catch(function (error) {
|
|
1210
|
+
toast(t('temp.menu.failed') + String((error && error.message) || error), true)
|
|
1211
|
+
})
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/** Delete one session now, with the same fallback `purgeAll` uses. */
|
|
1215
|
+
function deleteTempSession(id) {
|
|
1216
|
+
tempCall('api/dsh-temp-session.remove?sessionId=' + encodeURIComponent(id), { method: 'POST' })
|
|
1217
|
+
.then(function (data) {
|
|
1218
|
+
if (!data || data.ok !== true) throw new Error((data && data.error) || 'delete failed')
|
|
1219
|
+
applyTempIdList(data.ids, id)
|
|
1220
|
+
toast(t(data.hidden ? 'temp.menu.hidden' : 'temp.menu.deleted'), false)
|
|
1221
|
+
refreshHostSessions()
|
|
1222
|
+
scheduleSync()
|
|
1223
|
+
})
|
|
1224
|
+
.catch(function (error) {
|
|
1225
|
+
toast(t('temp.menu.failed') + String((error && error.message) || error), true)
|
|
1226
|
+
})
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/** Adopt the id list the Host returned, or just drop the one id. */
|
|
1230
|
+
function applyTempIdList(ids, removedId) {
|
|
1231
|
+
if (Array.isArray(ids)) tempIds = ids.slice()
|
|
1232
|
+
else tempIds = tempIds.filter(function (item) { return item !== removedId })
|
|
1233
|
+
applyTempRows()
|
|
1234
|
+
if (tempRowMenu && tempMenuRow) {
|
|
1235
|
+
var key = tempMenuRow.getAttribute('data-row-key') || ''
|
|
1236
|
+
if (tempIds.indexOf(key.slice('session:'.length)) < 0) closeTempMenu(false)
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Ask the host's own session list to re-read. There is no documented API
|
|
1242
|
+
* for this, so every common name is tried and failure is silent: a row
|
|
1243
|
+
* that outlives its files disappears on the next list refresh anyway.
|
|
1244
|
+
*/
|
|
1245
|
+
function refreshHostSessions() {
|
|
1246
|
+
try {
|
|
1247
|
+
var ui = uiWorkspace()
|
|
1248
|
+
var sessions = ui && ui.sessions
|
|
1249
|
+
var names = ['reload', 'refresh', 'reloadSessions', 'invalidate']
|
|
1250
|
+
for (var i = 0; i < names.length; i++) {
|
|
1251
|
+
if (sessions && typeof sessions[names[i]] === 'function') {
|
|
1252
|
+
sessions[names[i]]()
|
|
1253
|
+
return true
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
void error
|
|
1258
|
+
}
|
|
1259
|
+
return false
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function onTempContextMenu(event) {
|
|
1263
|
+
if (runtime.disposed || !tempMenuEnabled || !event) return
|
|
1264
|
+
var row = tempRowOf(event.target)
|
|
1265
|
+
if (!row) return
|
|
1266
|
+
var key = row.getAttribute('data-row-key') || ''
|
|
1267
|
+
var id = key.slice('session:'.length)
|
|
1268
|
+
// Not one of ours: leave the event completely alone so the host (or
|
|
1269
|
+
// whichever plugin owns that row) still gets its own menu.
|
|
1270
|
+
if (!id || tempIds.indexOf(id) < 0) return
|
|
1271
|
+
event.preventDefault()
|
|
1272
|
+
event.stopPropagation()
|
|
1273
|
+
openTempMenu(row, id, event.clientX, event.clientY)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
var tempMenuUnmount = null
|
|
1277
|
+
function mountTempRowMenu() {
|
|
1278
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return
|
|
1279
|
+
document.addEventListener('contextmenu', onTempContextMenu)
|
|
1280
|
+
tempMenuUnmount = function () {
|
|
1281
|
+
try { document.removeEventListener('contextmenu', onTempContextMenu) } catch (error) { void error }
|
|
1282
|
+
closeTempMenu(false)
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function applyTempMenuSetting(data) {
|
|
1287
|
+
var next = !data || data.rowMenu !== false
|
|
1288
|
+
if (next === tempMenuEnabled) return
|
|
1289
|
+
tempMenuEnabled = next
|
|
1290
|
+
if (!next) closeTempMenu(false)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
843
1293
|
function refreshTempIds() {
|
|
844
1294
|
return tempCall('api/dsh-temp-session.list')
|
|
845
1295
|
.then(function (data) {
|
|
@@ -947,6 +1397,8 @@ window.__ModuleLoader__.load({
|
|
|
947
1397
|
expandAll = !data || data.expandAllSessions !== false
|
|
948
1398
|
expandAllKnown = true
|
|
949
1399
|
applyExpandFlag()
|
|
1400
|
+
// 0.4.0 — same treatment: a Host without this key keeps the menu on.
|
|
1401
|
+
applyTempMenuSetting(data)
|
|
950
1402
|
syncChatMode()
|
|
951
1403
|
expandAllSessions()
|
|
952
1404
|
scheduleSync()
|
|
@@ -1140,11 +1592,32 @@ window.__ModuleLoader__.load({
|
|
|
1140
1592
|
return bar
|
|
1141
1593
|
}
|
|
1142
1594
|
|
|
1595
|
+
// A paint must be able to leave the tree exactly as it found it. Asking
|
|
1596
|
+
// for an exact neighbour is not enough: the 0.3.0 tab strip (and an
|
|
1597
|
+
// active plugin's panel) legitimately sit between the control bar and
|
|
1598
|
+
// the host's panels nav, so `bar.nextSibling === nav` can never be true
|
|
1599
|
+
// once those exist. Moving the bar on every paint mutates the observed
|
|
1600
|
+
// root, which fires the MutationObserver, which schedules another paint
|
|
1601
|
+
// -- a microtask loop that never returns to the event loop and hangs DSH
|
|
1602
|
+
// on "Loading plugins..." with the renderer at 100%. Compare ORDER, not
|
|
1603
|
+
// adjacency.
|
|
1604
|
+
function isBefore(node, reference) {
|
|
1605
|
+
if (!node || !reference || !node.parentNode || node.parentNode !== reference.parentNode) return false
|
|
1606
|
+
var list = node.parentNode.children
|
|
1607
|
+
var at = -1
|
|
1608
|
+
var before = -1
|
|
1609
|
+
for (var i = 0; i < list.length; i++) {
|
|
1610
|
+
if (list[i] === node) at = i
|
|
1611
|
+
if (list[i] === reference) before = i
|
|
1612
|
+
}
|
|
1613
|
+
return at >= 0 && before >= 0 && at < before
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1143
1616
|
function ensureBar(fx) {
|
|
1144
1617
|
var bar = runtime.bar
|
|
1145
1618
|
if (bar && bar.parentNode !== fx.root) {
|
|
1146
1619
|
try {
|
|
1147
|
-
bar.parentNode.removeChild(bar)
|
|
1620
|
+
if (bar.parentNode) bar.parentNode.removeChild(bar)
|
|
1148
1621
|
} catch (error) {
|
|
1149
1622
|
void error
|
|
1150
1623
|
}
|
|
@@ -1156,13 +1629,442 @@ window.__ModuleLoader__.load({
|
|
|
1156
1629
|
}
|
|
1157
1630
|
var anchor = fx.nav || fx.regionArea
|
|
1158
1631
|
if (anchor && anchor.parentNode === fx.root) {
|
|
1159
|
-
if (bar.
|
|
1632
|
+
if (bar.parentNode !== fx.root || !isBefore(bar, anchor)) fx.root.insertBefore(bar, anchor)
|
|
1160
1633
|
} else if (bar.parentNode !== fx.root) {
|
|
1161
1634
|
fx.root.appendChild(bar)
|
|
1162
1635
|
}
|
|
1163
1636
|
return bar
|
|
1164
1637
|
}
|
|
1165
1638
|
|
|
1639
|
+
// ---- third-party tabs (0.3.0) --------------------------------------
|
|
1640
|
+
// Another plugin cannot reach into this bundle, so the whole feature is
|
|
1641
|
+
// one service object handed to the client context (`ctx.provide` at the
|
|
1642
|
+
// bottom of this file). The contract is deliberately DOM-shaped rather
|
|
1643
|
+
// than React-shaped: a consumer receives a plain container and paints
|
|
1644
|
+
// into it, which works for every plugin no matter how it was built.
|
|
1645
|
+
//
|
|
1646
|
+
// Deliberately absent: any way for a tab to restructure the sidebar.
|
|
1647
|
+
// A tab owns its own container and nothing else.
|
|
1648
|
+
var TAB_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,47}$/
|
|
1649
|
+
var tabRecords = []
|
|
1650
|
+
var tabListeners = []
|
|
1651
|
+
var tabSeq = 0
|
|
1652
|
+
|
|
1653
|
+
function readActiveTab() {
|
|
1654
|
+
try {
|
|
1655
|
+
var raw = localStorage.getItem(TAB_STORE_KEY)
|
|
1656
|
+
return typeof raw === 'string' && raw ? raw : null
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
return null
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function writeActiveTab(id) {
|
|
1663
|
+
try {
|
|
1664
|
+
if (id) localStorage.setItem(TAB_STORE_KEY, id)
|
|
1665
|
+
else localStorage.removeItem(TAB_STORE_KEY)
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
void error
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function findTab(id) {
|
|
1672
|
+
for (var i = 0; i < tabRecords.length; i++) {
|
|
1673
|
+
if (tabRecords[i].id === id) return tabRecords[i]
|
|
1674
|
+
}
|
|
1675
|
+
return null
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// `title` may be a plain string or `{ zh, en }`. Resolving it here keeps
|
|
1679
|
+
// the two languages in one descriptor instead of making every consumer
|
|
1680
|
+
// read the locale itself.
|
|
1681
|
+
function tabTitle(record) {
|
|
1682
|
+
var title = record.title
|
|
1683
|
+
if (title && typeof title === 'object') {
|
|
1684
|
+
var zh = typeof title.zh === 'string' ? title.zh : ''
|
|
1685
|
+
var en = typeof title.en === 'string' ? title.en : ''
|
|
1686
|
+
var preferred = t.langName() === 'zh' ? zh || en : en || zh
|
|
1687
|
+
return preferred || record.id
|
|
1688
|
+
}
|
|
1689
|
+
return typeof title === 'string' && title ? title : record.id
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function tabSnapshot() {
|
|
1693
|
+
var out = []
|
|
1694
|
+
for (var i = 0; i < tabRecords.length; i++) {
|
|
1695
|
+
var record = tabRecords[i]
|
|
1696
|
+
out.push({
|
|
1697
|
+
id: record.id,
|
|
1698
|
+
title: tabTitle(record),
|
|
1699
|
+
icon: record.icon,
|
|
1700
|
+
order: record.order,
|
|
1701
|
+
badge: record.badge,
|
|
1702
|
+
hidden: record.hidden,
|
|
1703
|
+
owner: record.owner,
|
|
1704
|
+
})
|
|
1705
|
+
}
|
|
1706
|
+
return out
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function notifyTabs(reason) {
|
|
1710
|
+
if (!tabListeners.length) return
|
|
1711
|
+
var list = tabSnapshot()
|
|
1712
|
+
var active = runtime.activeTab
|
|
1713
|
+
for (var i = 0; i < tabListeners.length; i++) {
|
|
1714
|
+
try {
|
|
1715
|
+
tabListeners[i](list, reason, active)
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
void error
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function tabContext(record) {
|
|
1723
|
+
return {
|
|
1724
|
+
id: record.id,
|
|
1725
|
+
t: function (key) {
|
|
1726
|
+
return t(key)
|
|
1727
|
+
},
|
|
1728
|
+
lang: function () {
|
|
1729
|
+
return t.langName()
|
|
1730
|
+
},
|
|
1731
|
+
// Lets a consumer close its own tab once the user is done with it,
|
|
1732
|
+
// rather than leaving a dead page in the strip.
|
|
1733
|
+
close: function () {
|
|
1734
|
+
if (runtime.activeTab === record.id) activateTab(null)
|
|
1735
|
+
},
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function createTabPanel(record) {
|
|
1740
|
+
var panel = document.createElement('div')
|
|
1741
|
+
panel.setAttribute('data-dsbs-tabpanel', record.id)
|
|
1742
|
+
panel.setAttribute('role', 'tabpanel')
|
|
1743
|
+
panel.setAttribute('aria-label', tabTitle(record))
|
|
1744
|
+
panel.setAttribute('data-dsbs-visible', 'false')
|
|
1745
|
+
var entry = { el: panel, cleanup: null, record: record, mounted: false }
|
|
1746
|
+
runtime.tabPanels[record.id] = entry
|
|
1747
|
+
return entry
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// Mounting is deferred until the first activation: a tab that is never
|
|
1751
|
+
// opened should cost nothing, and a container that is still `display:
|
|
1752
|
+
// none` has no geometry to measure.
|
|
1753
|
+
function mountTabPanel(entry) {
|
|
1754
|
+
if (!entry || entry.mounted) return
|
|
1755
|
+
entry.mounted = true
|
|
1756
|
+
var record = entry.record
|
|
1757
|
+
if (typeof record.mount !== 'function') return
|
|
1758
|
+
try {
|
|
1759
|
+
var result = record.mount(entry.el, tabContext(record))
|
|
1760
|
+
if (typeof result === 'function') entry.cleanup = result
|
|
1761
|
+
else if (result && typeof result.dispose === 'function') {
|
|
1762
|
+
entry.cleanup = function () {
|
|
1763
|
+
result.dispose()
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
// A broken consumer must never take the sidebar down with it: the
|
|
1768
|
+
// failure is reported inside its own tab and logged once.
|
|
1769
|
+
try {
|
|
1770
|
+
entry.el.textContent = '[dsh-sidebar-suite] tab "' + record.id + '" failed to mount: '
|
|
1771
|
+
+ (error && error.message ? error.message : String(error))
|
|
1772
|
+
} catch (inner) {
|
|
1773
|
+
void inner
|
|
1774
|
+
}
|
|
1775
|
+
try {
|
|
1776
|
+
console.error('[dsh-sidebar-suite] tab mount failed:', record.id, error)
|
|
1777
|
+
} catch (inner) {
|
|
1778
|
+
void inner
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function disposeTabPanel(id) {
|
|
1784
|
+
var entry = runtime.tabPanels[id]
|
|
1785
|
+
if (!entry) return
|
|
1786
|
+
delete runtime.tabPanels[id]
|
|
1787
|
+
try {
|
|
1788
|
+
if (typeof entry.cleanup === 'function') entry.cleanup()
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
void error
|
|
1791
|
+
}
|
|
1792
|
+
try {
|
|
1793
|
+
if (entry.el && entry.el.parentNode) entry.el.parentNode.removeChild(entry.el)
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
void error
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function disposeAllTabPanels() {
|
|
1800
|
+
var ids = []
|
|
1801
|
+
for (var id in runtime.tabPanels) ids.push(id)
|
|
1802
|
+
for (var i = 0; i < ids.length; i++) disposeTabPanel(ids[i])
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function unregisterTab(id) {
|
|
1806
|
+
var record = findTab(id)
|
|
1807
|
+
if (!record) return false
|
|
1808
|
+
tabRecords.splice(tabRecords.indexOf(record), 1)
|
|
1809
|
+
disposeTabPanel(id)
|
|
1810
|
+
if (runtime.activeTab === id) {
|
|
1811
|
+
runtime.activeTab = null
|
|
1812
|
+
writeActiveTab(null)
|
|
1813
|
+
}
|
|
1814
|
+
notifyTabs('unregister')
|
|
1815
|
+
scheduleSync()
|
|
1816
|
+
return true
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
function registerTab(descriptor) {
|
|
1820
|
+
if (!descriptor || typeof descriptor !== 'object') {
|
|
1821
|
+
throw new TypeError('[dsh-sidebar-suite] registerTab(descriptor) needs an object')
|
|
1822
|
+
}
|
|
1823
|
+
var id = String(descriptor.id == null ? '' : descriptor.id).trim()
|
|
1824
|
+
if (!TAB_ID_RE.test(id)) {
|
|
1825
|
+
throw new TypeError('[dsh-sidebar-suite] registerTab: id must match ' + TAB_ID_RE + ', got "' + id + '"')
|
|
1826
|
+
}
|
|
1827
|
+
if (id === 'ext' || id === 'ws') {
|
|
1828
|
+
throw new TypeError('[dsh-sidebar-suite] registerTab: "' + id + '" is reserved for a built-in group')
|
|
1829
|
+
}
|
|
1830
|
+
var mount = descriptor.mount || descriptor.render
|
|
1831
|
+
if (typeof mount !== 'function') {
|
|
1832
|
+
throw new TypeError('[dsh-sidebar-suite] registerTab: "' + id + '" needs mount(container) or render(container)')
|
|
1833
|
+
}
|
|
1834
|
+
var record = {
|
|
1835
|
+
id: id,
|
|
1836
|
+
title: descriptor.title,
|
|
1837
|
+
icon: typeof descriptor.icon === 'string' ? descriptor.icon : null,
|
|
1838
|
+
order: typeof descriptor.order === 'number' && isFinite(descriptor.order) ? descriptor.order : 100,
|
|
1839
|
+
badge: descriptor.badge == null ? null : descriptor.badge,
|
|
1840
|
+
hidden: descriptor.hidden === true,
|
|
1841
|
+
mount: mount,
|
|
1842
|
+
owner: descriptor.owner == null ? null : String(descriptor.owner),
|
|
1843
|
+
seq: (tabSeq += 1),
|
|
1844
|
+
}
|
|
1845
|
+
// Re-registering the same id is exactly what a hot client reload looks
|
|
1846
|
+
// like, so it replaces in place instead of stacking a duplicate page.
|
|
1847
|
+
var previous = findTab(id)
|
|
1848
|
+
if (previous) {
|
|
1849
|
+
disposeTabPanel(id)
|
|
1850
|
+
tabRecords.splice(tabRecords.indexOf(previous), 1)
|
|
1851
|
+
}
|
|
1852
|
+
tabRecords.push(record)
|
|
1853
|
+
runtime.seenTabs[id] = true
|
|
1854
|
+
notifyTabs('register')
|
|
1855
|
+
scheduleSync()
|
|
1856
|
+
var disposed = false
|
|
1857
|
+
return function disposeTab() {
|
|
1858
|
+
if (disposed) return
|
|
1859
|
+
disposed = true
|
|
1860
|
+
unregisterTab(id)
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// Built-in groups only appear as pages in layout b. Layout a folds the
|
|
1865
|
+
// two groups independently and layout c floats the panels, so a page
|
|
1866
|
+
// switcher for them there would contradict what those layouts mean --
|
|
1867
|
+
// third-party tabs still work in all three.
|
|
1868
|
+
function tabEntries() {
|
|
1869
|
+
var out = []
|
|
1870
|
+
var showBuiltin = config.values.mode === 'b' && !!runtime.nav
|
|
1871
|
+
if (showBuiltin) {
|
|
1872
|
+
out.push({ id: 'ext', builtin: true, title: t('tab.ext'), icon: null, order: -20, badge: null })
|
|
1873
|
+
out.push({ id: 'ws', builtin: true, title: t('tab.ws'), icon: null, order: -10, badge: null })
|
|
1874
|
+
}
|
|
1875
|
+
for (var i = 0; i < tabRecords.length; i++) {
|
|
1876
|
+
var record = tabRecords[i]
|
|
1877
|
+
if (record.hidden === true) continue
|
|
1878
|
+
out.push({
|
|
1879
|
+
id: record.id,
|
|
1880
|
+
builtin: false,
|
|
1881
|
+
title: tabTitle(record),
|
|
1882
|
+
icon: record.icon,
|
|
1883
|
+
order: record.order,
|
|
1884
|
+
seq: record.seq,
|
|
1885
|
+
badge: record.badge,
|
|
1886
|
+
})
|
|
1887
|
+
}
|
|
1888
|
+
out.sort(function (left, right) {
|
|
1889
|
+
if (left.builtin !== right.builtin) return left.builtin ? -1 : 1
|
|
1890
|
+
if (left.order !== right.order) return left.order - right.order
|
|
1891
|
+
return (left.seq || 0) - (right.seq || 0)
|
|
1892
|
+
})
|
|
1893
|
+
return out
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function ensureTabStrip(fx) {
|
|
1897
|
+
var strip = runtime.tabStrip
|
|
1898
|
+
if (strip && strip.parentNode !== fx.root) {
|
|
1899
|
+
try {
|
|
1900
|
+
if (strip.parentNode) strip.parentNode.removeChild(strip)
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
void error
|
|
1903
|
+
}
|
|
1904
|
+
strip = null
|
|
1905
|
+
}
|
|
1906
|
+
if (!strip) {
|
|
1907
|
+
strip = document.createElement('div')
|
|
1908
|
+
strip.setAttribute('data-dsbs-tabs', '')
|
|
1909
|
+
strip.setAttribute('role', 'tablist')
|
|
1910
|
+
strip.setAttribute('aria-label', t('tabs.label'))
|
|
1911
|
+
runtime.tabStrip = strip
|
|
1912
|
+
}
|
|
1913
|
+
// Sits under the control bar so the two read as one header. A plugin
|
|
1914
|
+
// panel may sit after it, so "immediately after the bar" is too strict
|
|
1915
|
+
// a condition for the steady state: demanding an exact previousSibling
|
|
1916
|
+
// moves the strip on every paint and spins the observer loop.
|
|
1917
|
+
if (strip.parentNode === fx.root && (!runtime.bar || isBefore(runtime.bar, strip))) return strip
|
|
1918
|
+
var anchor = runtime.bar && runtime.bar.parentNode === fx.root
|
|
1919
|
+
? runtime.bar.nextSibling
|
|
1920
|
+
: (fx.nav || fx.regionArea)
|
|
1921
|
+
if (anchor === strip) return strip
|
|
1922
|
+
if (anchor && anchor.parentNode === fx.root) fx.root.insertBefore(strip, anchor)
|
|
1923
|
+
else fx.root.appendChild(strip)
|
|
1924
|
+
return strip
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
function buildTabButton(entry, selected) {
|
|
1928
|
+
var button = document.createElement('button')
|
|
1929
|
+
button.setAttribute('type', 'button')
|
|
1930
|
+
button.setAttribute('data-dsbs-tabid', entry.id)
|
|
1931
|
+
button.setAttribute('role', 'tab')
|
|
1932
|
+
button.setAttribute('aria-selected', selected ? 'true' : 'false')
|
|
1933
|
+
button.setAttribute('title', entry.title)
|
|
1934
|
+
if (selected) button.setAttribute('data-dsbs-selected', 'true')
|
|
1935
|
+
if (entry.builtin) button.setAttribute('data-dsbs-builtin', entry.id)
|
|
1936
|
+
|
|
1937
|
+
if (typeof entry.icon === 'string' && entry.icon) {
|
|
1938
|
+
var glyph = document.createElement('span')
|
|
1939
|
+
glyph.setAttribute('data-dsbs-tabicon', '')
|
|
1940
|
+
glyph.setAttribute('aria-hidden', 'true')
|
|
1941
|
+
button.setAttribute('data-dsbs-hasicon', '')
|
|
1942
|
+
if (entry.icon.charAt(0) === '<') {
|
|
1943
|
+
// A consumer-supplied mark. Plugins already run arbitrary code in
|
|
1944
|
+
// this page, so this grants nothing new -- but the icon still gets
|
|
1945
|
+
// its own element so the label stays a text node.
|
|
1946
|
+
glyph.innerHTML = entry.icon
|
|
1947
|
+
} else {
|
|
1948
|
+
glyph.textContent = entry.icon
|
|
1949
|
+
glyph.setAttribute('data-dsbs-tabicon-text', '')
|
|
1950
|
+
}
|
|
1951
|
+
button.appendChild(glyph)
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
var label = document.createElement('span')
|
|
1955
|
+
label.setAttribute('data-dsbs-tablabel', '')
|
|
1956
|
+
label.textContent = entry.title
|
|
1957
|
+
button.appendChild(label)
|
|
1958
|
+
|
|
1959
|
+
if (entry.badge != null && entry.badge !== '') {
|
|
1960
|
+
var badge = document.createElement('span')
|
|
1961
|
+
badge.setAttribute('data-dsbs-tabbadge', '')
|
|
1962
|
+
badge.textContent = String(entry.badge)
|
|
1963
|
+
button.appendChild(badge)
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
button.addEventListener('click', function () {
|
|
1967
|
+
if (entry.builtin) {
|
|
1968
|
+
// Built-ins drive the existing group switcher, so a click keeps
|
|
1969
|
+
// meaning what the same click meant before 0.3.0.
|
|
1970
|
+
var tookOver = runtime.activeTab !== null
|
|
1971
|
+
runtime.activeTab = null
|
|
1972
|
+
writeActiveTab(null)
|
|
1973
|
+
setField('tab', entry.id)
|
|
1974
|
+
// A click here takes the column away from a consumer tab exactly
|
|
1975
|
+
// like clicking another tab does, so subscribers hear about it the
|
|
1976
|
+
// same way. Without this a consumer that trusts `subscribe` would
|
|
1977
|
+
// keep believing it is still on screen.
|
|
1978
|
+
if (tookOver) notifyTabs('active')
|
|
1979
|
+
} else {
|
|
1980
|
+
activateTab(entry.id)
|
|
1981
|
+
}
|
|
1982
|
+
})
|
|
1983
|
+
return button
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function renderTabStrip() {
|
|
1987
|
+
var strip = runtime.tabStrip
|
|
1988
|
+
if (!strip) return
|
|
1989
|
+
var entries = tabEntries()
|
|
1990
|
+
var hasPlugin = false
|
|
1991
|
+
for (var p = 0; p < entries.length; p++) {
|
|
1992
|
+
if (!entries[p].builtin) hasPlugin = true
|
|
1993
|
+
}
|
|
1994
|
+
// Nothing registered: stay out of the way entirely, so a profile with
|
|
1995
|
+
// no tab consumers looks exactly like 0.2.x.
|
|
1996
|
+
if (!hasPlugin) {
|
|
1997
|
+
strip.setAttribute('hidden', '')
|
|
1998
|
+
runtime.tabSignature = ''
|
|
1999
|
+
return
|
|
2000
|
+
}
|
|
2001
|
+
strip.removeAttribute('hidden')
|
|
2002
|
+
|
|
2003
|
+
var activePlugin = runtime.activeTab && findTab(runtime.activeTab) ? runtime.activeTab : null
|
|
2004
|
+
var signature = (activePlugin || '-') + '|' + config.values.mode + '|' + config.values.tab + '|'
|
|
2005
|
+
for (var s = 0; s < entries.length; s++) {
|
|
2006
|
+
signature += entries[s].id + ':' + entries[s].title + ':' + entries[s].icon + ':' + entries[s].badge + ';'
|
|
2007
|
+
}
|
|
2008
|
+
if (signature === runtime.tabSignature) return
|
|
2009
|
+
runtime.tabSignature = signature
|
|
2010
|
+
|
|
2011
|
+
while (strip.firstChild) strip.removeChild(strip.firstChild)
|
|
2012
|
+
for (var i = 0; i < entries.length; i++) {
|
|
2013
|
+
var entry = entries[i]
|
|
2014
|
+
var selected = entry.builtin
|
|
2015
|
+
? (!activePlugin && config.values.tab === entry.id)
|
|
2016
|
+
: (activePlugin === entry.id)
|
|
2017
|
+
strip.appendChild(buildTabButton(entry, selected))
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
function placeTabPanel(panel, fx) {
|
|
2022
|
+
if (panel.parentNode === fx.root) return
|
|
2023
|
+
var anchor = fx.nav || fx.regionArea
|
|
2024
|
+
if (anchor && anchor.parentNode === fx.root) fx.root.insertBefore(panel, anchor)
|
|
2025
|
+
else fx.root.appendChild(panel)
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function syncTabPanels(fx, activeId) {
|
|
2029
|
+
var i
|
|
2030
|
+
// The container is created the first time its tab owns the column, not
|
|
2031
|
+
// at registration: a tab that is never opened should cost nothing, and
|
|
2032
|
+
// a display:none container has no geometry to measure anyway.
|
|
2033
|
+
if (activeId && !runtime.tabPanels[activeId]) {
|
|
2034
|
+
var owning = findTab(activeId)
|
|
2035
|
+
if (owning) createTabPanel(owning)
|
|
2036
|
+
}
|
|
2037
|
+
// Panels that are no longer registered are gone already; the ones that
|
|
2038
|
+
// remain are shown only while they own the column.
|
|
2039
|
+
for (var id in runtime.tabPanels) {
|
|
2040
|
+
var entry = runtime.tabPanels[id]
|
|
2041
|
+
if (!entry || !entry.el) continue
|
|
2042
|
+
if (id === activeId) {
|
|
2043
|
+
placeTabPanel(entry.el, fx)
|
|
2044
|
+
entry.el.setAttribute('data-dsbs-visible', 'true')
|
|
2045
|
+
entry.el.removeAttribute('aria-hidden')
|
|
2046
|
+
if (!entry.mounted) mountTabPanel(entry)
|
|
2047
|
+
} else {
|
|
2048
|
+
entry.el.setAttribute('data-dsbs-visible', 'false')
|
|
2049
|
+
entry.el.setAttribute('aria-hidden', 'true')
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
void i
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function activateTab(id) {
|
|
2056
|
+
if (id != null && id !== '' && !findTab(id)) return false
|
|
2057
|
+
var next = id == null || id === '' ? null : String(id)
|
|
2058
|
+
// Clicking the tab that already owns the column returns to the groups,
|
|
2059
|
+
// which is the only way back out of a tab in layouts a and c.
|
|
2060
|
+
if (next && runtime.activeTab === next) next = null
|
|
2061
|
+
runtime.activeTab = next
|
|
2062
|
+
writeActiveTab(next)
|
|
2063
|
+
notifyTabs('active')
|
|
2064
|
+
scheduleSync()
|
|
2065
|
+
return true
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1166
2068
|
function barButton(key) {
|
|
1167
2069
|
if (key === 'temp') return tempButton
|
|
1168
2070
|
if (!runtime.bar) return null
|
|
@@ -1271,6 +2173,8 @@ window.__ModuleLoader__.load({
|
|
|
1271
2173
|
|
|
1272
2174
|
if (fx.nav && !fx.nav.hasAttribute('data-dsbs-ext')) fx.nav.setAttribute('data-dsbs-ext', '')
|
|
1273
2175
|
ensureBar(fx)
|
|
2176
|
+
ensureTabStrip(fx)
|
|
2177
|
+
renderTabStrip()
|
|
1274
2178
|
placeTempButton()
|
|
1275
2179
|
applyTempButtonVisibility()
|
|
1276
2180
|
loadTempStatus()
|
|
@@ -1339,7 +2243,28 @@ window.__ModuleLoader__.load({
|
|
|
1339
2243
|
|
|
1340
2244
|
var extHidden
|
|
1341
2245
|
var wsHidden
|
|
1342
|
-
|
|
2246
|
+
// A registered tab owning the column outranks every layout rule: the
|
|
2247
|
+
// two groups step aside and the tab's own container takes the space.
|
|
2248
|
+
var pluginTab = null
|
|
2249
|
+
if (runtime.activeTab) {
|
|
2250
|
+
if (findTab(runtime.activeTab)) {
|
|
2251
|
+
pluginTab = runtime.activeTab
|
|
2252
|
+
} else if (runtime.seenTabs[runtime.activeTab]) {
|
|
2253
|
+
// It registered once and is gone now: its owner unloaded while the
|
|
2254
|
+
// page was open. Fall back instead of leaving an empty column with
|
|
2255
|
+
// no way out of it.
|
|
2256
|
+
runtime.activeTab = null
|
|
2257
|
+
writeActiveTab(null)
|
|
2258
|
+
notifyTabs('orphan')
|
|
2259
|
+
}
|
|
2260
|
+
// Not seen yet: another plugin may still be registering during
|
|
2261
|
+
// startup, so the stored id is kept and retried on the next paint.
|
|
2262
|
+
}
|
|
2263
|
+
setFlag(html, 'data-dsbs-ptab', !!pluginTab)
|
|
2264
|
+
if (pluginTab) {
|
|
2265
|
+
extHidden = true
|
|
2266
|
+
wsHidden = true
|
|
2267
|
+
} else if (state.mode === 'a') {
|
|
1343
2268
|
extHidden = state.foldExt === true
|
|
1344
2269
|
wsHidden = state.foldWs === true
|
|
1345
2270
|
} else if (state.mode === 'b') {
|
|
@@ -1352,6 +2277,13 @@ window.__ModuleLoader__.load({
|
|
|
1352
2277
|
}
|
|
1353
2278
|
setGroupHidden(fx.nav, 'ext', extHidden)
|
|
1354
2279
|
setGroupHidden(fx.regionArea, 'ws', wsHidden)
|
|
2280
|
+
syncTabPanels(fx, pluginTab)
|
|
2281
|
+
// The strip is the only affordance that survives a hidden bar, so it
|
|
2282
|
+
// is repainted on every sync rather than only when its content changed.
|
|
2283
|
+
if (runtime.tabStrip && pluginTab) {
|
|
2284
|
+
var selectedTab = runtime.tabStrip.querySelector('[data-dsbs-tabid="' + pluginTab + '"]')
|
|
2285
|
+
if (selectedTab) selectedTab.setAttribute('aria-selected', 'true')
|
|
2286
|
+
}
|
|
1355
2287
|
|
|
1356
2288
|
observeRoot(fx.root)
|
|
1357
2289
|
positionPop(fx)
|
|
@@ -1437,8 +2369,32 @@ window.__ModuleLoader__.load({
|
|
|
1437
2369
|
}
|
|
1438
2370
|
|
|
1439
2371
|
var pending = false
|
|
2372
|
+
var syncStreak = 0
|
|
2373
|
+
var streakTimer = null
|
|
2374
|
+
var streakDeferred = false
|
|
2375
|
+
// Safety net for the observer feedback loop: a paint that mutates the
|
|
2376
|
+
// observed root chains MutationObserver -> scheduleSync -> paint in
|
|
2377
|
+
// microtasks forever, so the renderer never gets a turn and DSH hangs on
|
|
2378
|
+
// "Loading plugins...". A healthy startup needs only a handful of syncs
|
|
2379
|
+
// between two macrotasks. Cap the streak, yield once, then resume; this
|
|
2380
|
+
// cannot lose a state change, and it keeps unrelated future regressions
|
|
2381
|
+
// from taking the whole app down.
|
|
1440
2382
|
function scheduleSync() {
|
|
1441
2383
|
if (runtime.disposed || pending) return
|
|
2384
|
+
if (!streakTimer) {
|
|
2385
|
+
streakTimer = setTimeout(function () {
|
|
2386
|
+
streakTimer = null
|
|
2387
|
+
syncStreak = 0
|
|
2388
|
+
var again = streakDeferred
|
|
2389
|
+
streakDeferred = false
|
|
2390
|
+
if (again && !runtime.disposed) scheduleSync()
|
|
2391
|
+
}, 0)
|
|
2392
|
+
}
|
|
2393
|
+
if (syncStreak >= 16) {
|
|
2394
|
+
streakDeferred = true
|
|
2395
|
+
return
|
|
2396
|
+
}
|
|
2397
|
+
syncStreak += 1
|
|
1442
2398
|
pending = true
|
|
1443
2399
|
Promise.resolve().then(function () {
|
|
1444
2400
|
pending = false
|
|
@@ -1892,6 +2848,8 @@ window.__ModuleLoader__.load({
|
|
|
1892
2848
|
inChatMode: data.inChatMode === 'show' ? 'show' : 'hide',
|
|
1893
2849
|
buttonStyle: data.buttonStyle === 'own' ? 'own' : 'pair',
|
|
1894
2850
|
expandAllSessions: data.expandAllSessions !== false,
|
|
2851
|
+
// Older Host halves have no rowMenu key; the menu ships on.
|
|
2852
|
+
rowMenu: data.rowMenu !== false,
|
|
1895
2853
|
maxKept: kept > 0 ? Math.min(Math.round(kept), 200) : 20,
|
|
1896
2854
|
configRevision: typeof data.configRevision === 'number' ? data.configRevision : null,
|
|
1897
2855
|
configFile: typeof data.configFile === 'string' ? data.configFile : '',
|
|
@@ -2094,6 +3052,10 @@ window.__ModuleLoader__.load({
|
|
|
2094
3052
|
persist({ expandAllSessions: next })
|
|
2095
3053
|
}),
|
|
2096
3054
|
h('div', { style: dim }, t('options.expandHint')),
|
|
3055
|
+
policyRow(t('options.rowMenu'), policy.rowMenu === true, function (next) {
|
|
3056
|
+
persist({ rowMenu: next })
|
|
3057
|
+
}),
|
|
3058
|
+
h('div', { style: dim }, t('options.rowMenuHint')),
|
|
2097
3059
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' } },
|
|
2098
3060
|
h('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary,inherit)' } }, t('options.maxKept')),
|
|
2099
3061
|
h('input', {
|
|
@@ -2397,6 +3359,14 @@ window.__ModuleLoader__.load({
|
|
|
2397
3359
|
}
|
|
2398
3360
|
}, 'sidebar-suite: dismissal')
|
|
2399
3361
|
|
|
3362
|
+
effect(function () {
|
|
3363
|
+
// 0.4.0 — right-click on a temporary row. One delegated listener on the
|
|
3364
|
+
// document, because the host recreates session rows on every list
|
|
3365
|
+
// refresh; per-row listeners would need re-binding after each render.
|
|
3366
|
+
mountTempRowMenu()
|
|
3367
|
+
return tempMenuUnmount
|
|
3368
|
+
}, 'sidebar-suite: temp row menu')
|
|
3369
|
+
|
|
2400
3370
|
effect(function () {
|
|
2401
3371
|
// Safety net: the sidebar root is created and destroyed by the host, so
|
|
2402
3372
|
// a root replacement is invisible to an observer that watches the old
|
|
@@ -2442,6 +3412,13 @@ window.__ModuleLoader__.load({
|
|
|
2442
3412
|
teardown = function () {
|
|
2443
3413
|
if (runtime.disposed) return
|
|
2444
3414
|
runtime.disposed = true
|
|
3415
|
+
try {
|
|
3416
|
+
if (streakTimer) clearTimeout(streakTimer)
|
|
3417
|
+
streakTimer = null
|
|
3418
|
+
streakDeferred = false
|
|
3419
|
+
} catch (error) {
|
|
3420
|
+
void error
|
|
3421
|
+
}
|
|
2445
3422
|
try {
|
|
2446
3423
|
clearTimeout(tempToastTimer)
|
|
2447
3424
|
var toastNode = document.querySelector('[data-dsbs-temp-toast]')
|
|
@@ -2495,16 +3472,51 @@ window.__ModuleLoader__.load({
|
|
|
2495
3472
|
} catch (error) {
|
|
2496
3473
|
void error
|
|
2497
3474
|
}
|
|
3475
|
+
try {
|
|
3476
|
+
// 0.4.0 — the row menu lives in <body> and its document listeners
|
|
3477
|
+
// outlive the sidebar if they are not dropped explicitly.
|
|
3478
|
+
closeTempMenu(false)
|
|
3479
|
+
detachTempMenuDismiss()
|
|
3480
|
+
if (tempMenuUnmount) { tempMenuUnmount(); tempMenuUnmount = null }
|
|
3481
|
+
tempRowMenu = null
|
|
3482
|
+
tempMenuRow = null
|
|
3483
|
+
} catch (error) {
|
|
3484
|
+
void error
|
|
3485
|
+
}
|
|
2498
3486
|
try {
|
|
2499
3487
|
if (runtime.bar && runtime.bar.parentNode) runtime.bar.parentNode.removeChild(runtime.bar)
|
|
2500
3488
|
} catch (error) {
|
|
2501
3489
|
void error
|
|
2502
3490
|
}
|
|
3491
|
+
try {
|
|
3492
|
+
// Unmount every live tab first: a consumer's disposer may still
|
|
3493
|
+
// touch its container, so removing the DOM afterwards is safer
|
|
3494
|
+
// than yanking it out from under a cleanup callback.
|
|
3495
|
+
disposeAllTabPanels()
|
|
3496
|
+
if (runtime.tabStrip && runtime.tabStrip.parentNode) runtime.tabStrip.parentNode.removeChild(runtime.tabStrip)
|
|
3497
|
+
} catch (error) {
|
|
3498
|
+
void error
|
|
3499
|
+
}
|
|
3500
|
+
try {
|
|
3501
|
+
runtime.tabStrip = null
|
|
3502
|
+
runtime.tabSignature = ''
|
|
3503
|
+
tabListeners.length = 0
|
|
3504
|
+
tabRecords.length = 0
|
|
3505
|
+
} catch (error) {
|
|
3506
|
+
void error
|
|
3507
|
+
}
|
|
2503
3508
|
try {
|
|
2504
3509
|
if (window[OWNER_KEY] === owner) delete window[OWNER_KEY]
|
|
2505
3510
|
} catch (error) {
|
|
2506
3511
|
void error
|
|
2507
3512
|
}
|
|
3513
|
+
try {
|
|
3514
|
+
// Only drop the handle if it is still ours: a newer generation may
|
|
3515
|
+
// already have replaced it, and that one must keep working.
|
|
3516
|
+
if (window.__dshSidebarSuite === sidebarService) delete window.__dshSidebarSuite
|
|
3517
|
+
} catch (error) {
|
|
3518
|
+
void error
|
|
3519
|
+
}
|
|
2508
3520
|
try {
|
|
2509
3521
|
if (runtime.root) runtime.root.style.removeProperty('--dsbs-pop-top')
|
|
2510
3522
|
} catch (error) {
|
|
@@ -2538,6 +3550,7 @@ window.__ModuleLoader__.load({
|
|
|
2538
3550
|
'data-dsbs-narrow',
|
|
2539
3551
|
'data-dsbs-temp-solo',
|
|
2540
3552
|
'data-dsbs-temp-style',
|
|
3553
|
+
'data-dsbs-ptab',
|
|
2541
3554
|
]
|
|
2542
3555
|
for (var i = 0; i < attributes.length; i++) {
|
|
2543
3556
|
html.removeAttribute(attributes[i])
|
|
@@ -2564,7 +3577,6 @@ window.__ModuleLoader__.load({
|
|
|
2564
3577
|
}, 'sidebar-suite: teardown')
|
|
2565
3578
|
owner.teardown = teardown
|
|
2566
3579
|
}
|
|
2567
|
-
|
|
2568
3580
|
return { inject: ['locale'], apply: apply }
|
|
2569
3581
|
},
|
|
2570
3582
|
})
|