Skip to content

Commit c4fdcbc

Browse files
authored
feat(YfmTabs): auto-switching tabs in yfm-tabs when dragging over it (#157)
1 parent 1fc9098 commit c4fdcbc

File tree

2 files changed

+163
-1
lines changed

2 files changed

+163
-1
lines changed

src/extensions/yfm/YfmTabs/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {Action, ExtensionAuto} from '../../../core';
22

33
import {
4+
dragAutoSwitch,
45
joinBackwardToOpenTab,
56
removeTabWhenCursorAtTheStartOfTab,
67
tabEnter,
@@ -34,6 +35,8 @@ export const YfmTabs: ExtensionAuto = (builder) => {
3435
);
3536

3637
builder.addAction(actionName, () => createYfmTabs);
38+
39+
builder.addPlugin(dragAutoSwitch);
3740
};
3841

3942
declare global {

src/extensions/yfm/YfmTabs/plugins.ts

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import {Command, TextSelection, Transaction} from 'prosemirror-state';
1+
import {Command, Plugin, PluginView, TextSelection, Transaction} from 'prosemirror-state';
2+
import type {Transform} from 'prosemirror-transform';
3+
import type {EditorView} from 'prosemirror-view';
24
import {
35
findChildren,
6+
findDomRefAtPos,
47
findParentNodeOfType,
58
findParentNodeOfTypeClosestToPos,
69
NodeWithPos,
@@ -27,6 +30,162 @@ import {
2730
} from '../../';
2831
import {atEndOfPanel} from './utils';
2932
import {TabAttrs, TabPanelAttrs} from './YfmTabsSpecs/const';
33+
import throttle from 'lodash/throttle';
34+
35+
export const dragAutoSwitch = () =>
36+
new Plugin({
37+
view: TabsAutoSwitchOnDragOver.view,
38+
});
39+
40+
class TabsAutoSwitchOnDragOver implements PluginView {
41+
private static readonly TAB_SELECTOR = '.yfm-tab:not([data-diplodoc-is-active=true])';
42+
private static readonly OPEN_TIMEOUT = 500; //ms
43+
private static readonly THROTTLE_WAIT = 50; //ms
44+
45+
static readonly view = (view: EditorView): PluginView => new this(view);
46+
47+
private _tabElem: HTMLElement | null = null;
48+
private _editorView: EditorView;
49+
private _timeout: ReturnType<typeof setTimeout> | null = null;
50+
private readonly _docListener;
51+
52+
constructor(view: EditorView) {
53+
this._editorView = view;
54+
this._docListener = throttle(
55+
this._onDocEvent.bind(this),
56+
TabsAutoSwitchOnDragOver.THROTTLE_WAIT,
57+
);
58+
document.addEventListener('mousemove', this._docListener);
59+
document.addEventListener('dragover', this._docListener);
60+
}
61+
62+
destroy(): void {
63+
this._clear();
64+
this._docListener.cancel();
65+
document.removeEventListener('mousemove', this._docListener);
66+
document.removeEventListener('dragover', this._docListener);
67+
}
68+
69+
private _onDocEvent(event: MouseEvent) {
70+
const view = this._editorView;
71+
if (!view.dragging) return;
72+
const pos = view.posAtCoords({left: event.clientX, top: event.clientY});
73+
if (pos) {
74+
const elem = findDomRefAtPos(pos.pos, view.domAtPos.bind(view)) as HTMLElement;
75+
const cutElem = elem.closest(TabsAutoSwitchOnDragOver.TAB_SELECTOR);
76+
if (cutElem === this._tabElem) return;
77+
this._clear();
78+
if (cutElem) this._setTabElem(cutElem as HTMLElement);
79+
}
80+
}
81+
82+
private _clear() {
83+
if (this._timeout !== null) clearTimeout(this._timeout);
84+
this._timeout = null;
85+
this._tabElem = null;
86+
}
87+
88+
private _setTabElem(elem: HTMLElement) {
89+
this._tabElem = elem;
90+
this._timeout = setTimeout(
91+
this._switchTab.bind(this),
92+
TabsAutoSwitchOnDragOver.OPEN_TIMEOUT,
93+
);
94+
}
95+
96+
private _switchTab() {
97+
if (this._editorView.dragging && this._tabElem) {
98+
const pos = this._editorView.posAtDOM(this._tabElem, 0, -1);
99+
const $pos = this._editorView.state.doc.resolve(pos);
100+
const {state} = this._editorView;
101+
102+
let {depth} = $pos;
103+
let tabId = '';
104+
let tabsNode: NodeWithPos | null = null;
105+
do {
106+
const node = $pos.node(depth);
107+
if (node.type === tabType(state.schema)) {
108+
tabId = node.attrs[TabAttrs.dataDiplodocid];
109+
continue;
110+
}
111+
112+
if (node.type === tabsType(state.schema)) {
113+
tabsNode = {node, pos: $pos.before(depth)};
114+
break;
115+
}
116+
} while (--depth >= 0);
117+
118+
if (tabId && tabsNode) {
119+
const {tr} = state;
120+
if (switchYfmTab(tabsNode, tabId, tr)) {
121+
this._editorView.dispatch(tr.setMeta('addToHistory', false));
122+
}
123+
}
124+
}
125+
this._clear();
126+
}
127+
}
128+
129+
function switchYfmTab(
130+
{node: tabsNode, pos: tabsPos}: NodeWithPos,
131+
tabId: string,
132+
tr: Transform,
133+
): boolean {
134+
const {schema} = tabsNode.type;
135+
if (tabsNode.type !== tabsType(schema)) return false;
136+
137+
const tabsList = tabsNode.firstChild;
138+
if (tabsList?.type !== tabsListType(schema)) return false;
139+
140+
const tabsListPos = tabsPos + 1;
141+
142+
let panelId: string | null = null;
143+
tabsList.forEach((node, offset) => {
144+
if (node.type !== tabType(schema)) return;
145+
146+
const tabPos = tabsListPos + 1 + offset;
147+
const tabAttrs = {
148+
...node.attrs,
149+
[TabAttrs.ariaSelected]: 'false',
150+
[TabAttrs.dataDiplodocIsActive]: 'false',
151+
};
152+
153+
if (node.attrs[TabAttrs.dataDiplodocid] === tabId) {
154+
panelId = node.attrs[TabAttrs.ariaControls];
155+
tabAttrs[TabAttrs.ariaSelected] = 'true';
156+
tabAttrs[TabAttrs.dataDiplodocIsActive] = 'true';
157+
}
158+
159+
tr.setNodeMarkup(tabPos, null, tabAttrs);
160+
});
161+
162+
if (!panelId) return false;
163+
164+
tabsNode.forEach((node, offset) => {
165+
if (node.type !== tabPanelType(schema)) return;
166+
167+
const tabPanelPos = tabsPos + 1 + offset;
168+
const tabPanelAttrs = {
169+
...node.attrs,
170+
};
171+
const tabPanelClassList = new Set(
172+
((node.attrs[TabPanelAttrs.class] as string) ?? '')
173+
.split(' ')
174+
.filter((val) => Boolean(val.trim())),
175+
);
176+
177+
if (node.attrs[TabPanelAttrs.id] === panelId) {
178+
tabPanelClassList.add('active');
179+
} else {
180+
tabPanelClassList.delete('active');
181+
}
182+
183+
tabPanelAttrs[TabPanelAttrs.class] = Array.from(tabPanelClassList).join(' ');
184+
tr.setNodeMarkup(tabPanelPos, null, tabPanelAttrs);
185+
});
186+
187+
return true;
188+
}
30189

31190
export const tabPanelArrowDown: Command = (state, dispatch, view) => {
32191
const {selection: sel} = state;

0 commit comments

Comments
 (0)