Skip to content

Commit fc52d11

Browse files
committed
work-in-progress
1 parent 148a05a commit fc52d11

File tree

8 files changed

+488
-269
lines changed

8 files changed

+488
-269
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { addDisposableWindowListener } from '../../events';
2+
import {
3+
CompositeDisposable,
4+
Disposable,
5+
MutableDisposable,
6+
} from '../../lifecycle';
7+
8+
export class PopupService extends CompositeDisposable {
9+
private readonly _element: HTMLElement;
10+
private _active: HTMLElement | null = null;
11+
private _activeDisposable = new MutableDisposable();
12+
13+
constructor(private readonly root: HTMLElement) {
14+
super();
15+
16+
this._element = document.createElement('div');
17+
this._element.className = 'dv-popover-anchor';
18+
this._element.style.position = 'relative';
19+
20+
this.root.prepend(this._element);
21+
22+
this.addDisposables(
23+
Disposable.from(() => {
24+
this.close();
25+
}),
26+
this._activeDisposable
27+
);
28+
}
29+
30+
openPopover(
31+
element: HTMLElement,
32+
position: { x: number; y: number }
33+
): void {
34+
this.close();
35+
36+
const wrapper = document.createElement('div');
37+
wrapper.style.position = 'absolute';
38+
wrapper.style.zIndex = '99';
39+
wrapper.appendChild(element);
40+
41+
const anchorBox = this._element.getBoundingClientRect();
42+
const offsetX = anchorBox.left;
43+
const offsetY = anchorBox.top;
44+
45+
wrapper.style.top = `${position.y - offsetY}px`;
46+
wrapper.style.left = `${position.x - offsetX}px`;
47+
48+
this._element.appendChild(wrapper);
49+
50+
this._active = wrapper;
51+
52+
this._activeDisposable.value = new CompositeDisposable(
53+
addDisposableWindowListener(window, 'pointerdown', (event) => {
54+
const target = event.target;
55+
56+
if (!(target instanceof HTMLElement)) {
57+
return;
58+
}
59+
60+
let el: HTMLElement | null = target;
61+
62+
while (el && el !== wrapper) {
63+
el = el?.parentElement ?? null;
64+
}
65+
66+
if (el) {
67+
return; // clicked within popover
68+
}
69+
70+
this.close();
71+
})
72+
);
73+
}
74+
75+
close(): void {
76+
if (this._active) {
77+
this._active.remove();
78+
this._activeDisposable.dispose();
79+
this._active = null;
80+
}
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.dv-tabs-container {
2+
display: flex;
3+
overflow-x: overlay;
4+
overflow-y: hidden;
5+
6+
scrollbar-width: thin; // firefox
7+
8+
&::-webkit-scrollbar {
9+
height: 3px;
10+
}
11+
12+
/* Track */
13+
&::-webkit-scrollbar-track {
14+
background: transparent;
15+
}
16+
17+
/* Handle */
18+
&::-webkit-scrollbar-thumb {
19+
background: var(--dv-tabs-container-scrollbar-color);
20+
}
21+
22+
.dv-tab {
23+
-webkit-user-drag: element;
24+
outline: none;
25+
min-width: 75px;
26+
cursor: pointer;
27+
position: relative;
28+
box-sizing: border-box;
29+
30+
&:not(:first-child)::before {
31+
content: ' ';
32+
position: absolute;
33+
top: 0;
34+
left: 0;
35+
z-index: 5;
36+
pointer-events: none;
37+
background-color: var(--dv-tab-divider-color);
38+
width: 1px;
39+
height: 100%;
40+
}
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import { getPanelData } from '../../../dnd/dataTransfer';
2+
import { OverflowObserver } from '../../../dom';
3+
import { addDisposableListener, Emitter, Event } from '../../../events';
4+
import {
5+
CompositeDisposable,
6+
Disposable,
7+
IValueDisposable,
8+
} from '../../../lifecycle';
9+
import { DockviewComponent } from '../../dockviewComponent';
10+
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
11+
import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel';
12+
import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
13+
import { Tab } from '../tab/tab';
14+
import { TabDragEvent, TabDropIndexEvent } from './tabsContainer';
15+
16+
export class Tabs extends CompositeDisposable {
17+
private readonly _element: HTMLElement;
18+
private readonly _tabsList: HTMLElement;
19+
20+
private tabs: IValueDisposable<Tab>[] = [];
21+
private selectedIndex = -1;
22+
private _hasOverflow = false;
23+
private _dropdownAnchor: HTMLElement | null = null;
24+
25+
private readonly _onTabDragStart = new Emitter<TabDragEvent>();
26+
readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event;
27+
28+
private readonly _onDrop = new Emitter<TabDropIndexEvent>();
29+
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
30+
31+
private readonly _onWillShowOverlay =
32+
new Emitter<WillShowOverlayLocationEvent>();
33+
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent> =
34+
this._onWillShowOverlay.event;
35+
36+
get element(): HTMLElement {
37+
return this._element;
38+
}
39+
40+
get panels(): string[] {
41+
return this.tabs.map((_) => _.value.panel.id);
42+
}
43+
44+
get size(): number {
45+
return this.tabs.length;
46+
}
47+
48+
constructor(
49+
private readonly group: DockviewGroupPanel,
50+
private readonly accessor: DockviewComponent
51+
) {
52+
super();
53+
54+
this._element = document.createElement('div');
55+
this._element.className = 'dv-tabs-panel';
56+
this._element.style.display = 'flex';
57+
this._element.style.overflow = 'auto';
58+
this._tabsList = document.createElement('div');
59+
this._tabsList.className = 'dv-tabs-container';
60+
this._element.appendChild(this._tabsList);
61+
62+
const observer = new OverflowObserver(this._tabsList);
63+
64+
this.addDisposables(
65+
observer,
66+
observer.onDidChange((event) => {
67+
const hasOverflow = event.hasScrollX || event.hasScrollY;
68+
if (this._hasOverflow !== hasOverflow) {
69+
this.toggleDropdown(hasOverflow);
70+
}
71+
}),
72+
addDisposableListener(this.element, 'pointerdown', (event) => {
73+
if (event.defaultPrevented) {
74+
return;
75+
}
76+
77+
const isLeftClick = event.button === 0;
78+
79+
if (isLeftClick) {
80+
this.accessor.doSetGroupActive(this.group);
81+
}
82+
}),
83+
Disposable.from(() => {
84+
for (const { value, disposable } of this.tabs) {
85+
disposable.dispose();
86+
value.dispose();
87+
}
88+
89+
this.tabs = [];
90+
})
91+
);
92+
}
93+
94+
indexOf(id: string): number {
95+
return this.tabs.findIndex((tab) => tab.value.panel.id === id);
96+
}
97+
98+
isActive(tab: Tab): boolean {
99+
return (
100+
this.selectedIndex > -1 &&
101+
this.tabs[this.selectedIndex].value === tab
102+
);
103+
}
104+
105+
setActivePanel(panel: IDockviewPanel): void {
106+
this.tabs.forEach((tab) => {
107+
const isActivePanel = panel.id === tab.value.panel.id;
108+
tab.value.setActive(isActivePanel);
109+
});
110+
}
111+
112+
openPanel(panel: IDockviewPanel, index: number = this.tabs.length): void {
113+
if (this.tabs.find((tab) => tab.value.panel.id === panel.id)) {
114+
return;
115+
}
116+
const tab = new Tab(panel, this.accessor, this.group);
117+
tab.setContent(panel.view.tab);
118+
119+
const disposable = new CompositeDisposable(
120+
tab.onDragStart((event) => {
121+
this._onTabDragStart.fire({ nativeEvent: event, panel });
122+
}),
123+
tab.onChanged((event) => {
124+
const isFloatingGroupsEnabled =
125+
!this.accessor.options.disableFloatingGroups;
126+
127+
const isFloatingWithOnePanel =
128+
this.group.api.location.type === 'floating' &&
129+
this.size === 1;
130+
131+
if (
132+
isFloatingGroupsEnabled &&
133+
!isFloatingWithOnePanel &&
134+
event.shiftKey
135+
) {
136+
event.preventDefault();
137+
138+
const panel = this.accessor.getGroupPanel(tab.panel.id);
139+
140+
const { top, left } = tab.element.getBoundingClientRect();
141+
const { top: rootTop, left: rootLeft } =
142+
this.accessor.element.getBoundingClientRect();
143+
144+
this.accessor.addFloatingGroup(panel as DockviewPanel, {
145+
x: left - rootLeft,
146+
y: top - rootTop,
147+
inDragMode: true,
148+
});
149+
return;
150+
}
151+
152+
const isLeftClick = event.button === 0;
153+
154+
if (!isLeftClick || event.defaultPrevented) {
155+
return;
156+
}
157+
158+
if (this.group.activePanel !== panel) {
159+
this.group.model.openPanel(panel);
160+
}
161+
}),
162+
tab.onDrop((event) => {
163+
this._onDrop.fire({
164+
event: event.nativeEvent,
165+
index: this.tabs.findIndex((x) => x.value === tab),
166+
});
167+
}),
168+
tab.onWillShowOverlay((event) => {
169+
this._onWillShowOverlay.fire(
170+
new WillShowOverlayLocationEvent(event, {
171+
kind: 'tab',
172+
panel: this.group.activePanel,
173+
api: this.accessor.api,
174+
group: this.group,
175+
getData: getPanelData,
176+
})
177+
);
178+
})
179+
);
180+
181+
const value: IValueDisposable<Tab> = { value: tab, disposable };
182+
183+
this.addTab(value, index);
184+
}
185+
186+
delete(id: string): void {
187+
const index = this.indexOf(id);
188+
const tabToRemove = this.tabs.splice(index, 1)[0];
189+
190+
const { value, disposable } = tabToRemove;
191+
192+
disposable.dispose();
193+
value.dispose();
194+
value.element.remove();
195+
}
196+
197+
private addTab(
198+
tab: IValueDisposable<Tab>,
199+
index: number = this.tabs.length
200+
): void {
201+
if (index < 0 || index > this.tabs.length) {
202+
throw new Error('invalid location');
203+
}
204+
205+
this._tabsList.insertBefore(
206+
tab.value.element,
207+
this._tabsList.children[index]
208+
);
209+
210+
this.tabs = [
211+
...this.tabs.slice(0, index),
212+
tab,
213+
...this.tabs.slice(index),
214+
];
215+
216+
if (this.selectedIndex < 0) {
217+
this.selectedIndex = index;
218+
}
219+
}
220+
221+
private toggleDropdown(show: boolean): void {
222+
this._hasOverflow = show;
223+
if (this._dropdownAnchor) {
224+
this._dropdownAnchor.remove();
225+
this._dropdownAnchor = null;
226+
}
227+
228+
if (!show) {
229+
return;
230+
}
231+
232+
this._dropdownAnchor = document.createElement('div');
233+
this._dropdownAnchor.style.width = '10px';
234+
this._dropdownAnchor.style.height = '100%';
235+
this._dropdownAnchor.style.flexShrink = '0';
236+
this._dropdownAnchor.style.backgroundColor = 'red';
237+
238+
this.element.appendChild(this._dropdownAnchor);
239+
240+
addDisposableListener(this._dropdownAnchor, 'click', (event) => {
241+
const el = document.createElement('div');
242+
el.style.width = '200px';
243+
el.style.maxHeight = '600px';
244+
el.style.overflow = 'auto';
245+
el.style.backgroundColor = 'lightgreen';
246+
247+
this.tabs.map((tab) => {
248+
const tab2 = new Tab(
249+
tab.value.panel,
250+
this.accessor,
251+
this.group
252+
);
253+
tab2.setContent(tab.value.panel.view.newTab);
254+
el.appendChild(tab2.element);
255+
});
256+
257+
this.accessor.popupService.openPopover(el, {
258+
x: event.clientX,
259+
y: event.clientY,
260+
});
261+
});
262+
}
263+
}

0 commit comments

Comments
 (0)