Skip to content

Commit

Permalink
feat(YfmTabs): auto-switching tabs in yfm-tabs when dragging over it
Browse files Browse the repository at this point in the history
  • Loading branch information
d3m1d0v committed Nov 16, 2023
1 parent a7612c3 commit 0e7a564
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 1 deletion.
3 changes: 3 additions & 0 deletions src/extensions/yfm/YfmTabs/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {Action, ExtensionAuto} from '../../../core';

import {
dragAutoSwitch,
joinBackwardToOpenTab,
removeTabWhenCursorAtTheStartOfTab,
tabEnter,
Expand Down Expand Up @@ -34,6 +35,8 @@ export const YfmTabs: ExtensionAuto = (builder) => {
);

builder.addAction(actionName, () => createYfmTabs);

builder.addPlugin(dragAutoSwitch);
};

declare global {
Expand Down
161 changes: 160 additions & 1 deletion src/extensions/yfm/YfmTabs/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {Command, TextSelection, Transaction} from 'prosemirror-state';
import {Command, Plugin, PluginView, TextSelection, Transaction} from 'prosemirror-state';
import type {Transform} from 'prosemirror-transform';
import type {EditorView} from 'prosemirror-view';
import {
findChildren,
findDomRefAtPos,
findParentNodeOfType,
findParentNodeOfTypeClosestToPos,
NodeWithPos,
Expand All @@ -27,6 +30,162 @@ import {
} from '../../';
import {atEndOfPanel} from './utils';
import {TabAttrs, TabPanelAttrs} from './YfmTabsSpecs/const';
import throttle from 'lodash/throttle';

export const dragAutoSwitch = () =>
new Plugin({
view: TabsAutoSwitchOnDragOver.view,
});

class TabsAutoSwitchOnDragOver implements PluginView {
private static readonly TAB_SELECTOR = '.yfm-tab:not([data-diplodoc-is-active=true])';
private static readonly OPEN_TIMEOUT = 500; //ms
private static readonly THROTTLE_WAIT = 50; //ms

static readonly view = (view: EditorView): PluginView => new this(view);

private _tabElem: HTMLElement | null = null;
private _editorView: EditorView;
private _timeout: ReturnType<typeof setTimeout> | null = null;
private readonly _docListener;

constructor(view: EditorView) {
this._editorView = view;
this._docListener = throttle(
this._onDocEvent.bind(this),
TabsAutoSwitchOnDragOver.THROTTLE_WAIT,
);
document.addEventListener('mousemove', this._docListener);
document.addEventListener('dragover', this._docListener);
}

destroy(): void {
this._clear();
this._docListener.cancel();
document.removeEventListener('mousemove', this._docListener);
document.removeEventListener('dragover', this._docListener);
}

private _onDocEvent(event: MouseEvent) {
const view = this._editorView;
if (!view.dragging) return;
const pos = view.posAtCoords({left: event.clientX, top: event.clientY});
if (pos) {
const elem = findDomRefAtPos(pos.pos, view.domAtPos.bind(view)) as HTMLElement;
const cutElem = elem.closest(TabsAutoSwitchOnDragOver.TAB_SELECTOR);
if (cutElem === this._tabElem) return;
this._clear();
if (cutElem) this._setTabElem(cutElem as HTMLElement);
}
}

private _clear() {
if (this._timeout !== null) clearTimeout(this._timeout);
this._timeout = null;
this._tabElem = null;
}

private _setTabElem(elem: HTMLElement) {
this._tabElem = elem;
this._timeout = setTimeout(
this._switchTab.bind(this),
TabsAutoSwitchOnDragOver.OPEN_TIMEOUT,
);
}

private _switchTab() {
if (this._editorView.dragging && this._tabElem) {
const pos = this._editorView.posAtDOM(this._tabElem, 0, -1);
const $pos = this._editorView.state.doc.resolve(pos);
const {state} = this._editorView;

let {depth} = $pos;
let tabId = '';
let tabsNode: NodeWithPos | null = null;
do {
const node = $pos.node(depth);
if (node.type === tabType(state.schema)) {
tabId = node.attrs[TabAttrs.dataDiplodocid];
continue;
}

if (node.type === tabsType(state.schema)) {
tabsNode = {node, pos: $pos.before(depth)};
break;
}
} while (--depth >= 0);

if (tabId && tabsNode) {
const {tr} = state;
if (switchYfmTab(tabsNode, tabId, tr)) {
this._editorView.dispatch(tr.setMeta('addToHistory', false));
}
}
}
this._clear();
}
}

function switchYfmTab(
{node: tabsNode, pos: tabsPos}: NodeWithPos,
tabId: string,
tr: Transform,
): boolean {
const {schema} = tabsNode.type;
if (tabsNode.type !== tabsType(schema)) return false;

const tabsList = tabsNode.firstChild;
if (tabsList?.type !== tabsListType(schema)) return false;

const tabsListPos = tabsPos + 1;

let panelId: string | null = null;
tabsList.forEach((node, offset) => {
if (node.type !== tabType(schema)) return;

const tabPos = tabsListPos + 1 + offset;
const tabAttrs = {
...node.attrs,
[TabAttrs.ariaSelected]: 'false',
[TabAttrs.dataDiplodocIsActive]: 'false',
};

if (node.attrs[TabAttrs.dataDiplodocid] === tabId) {
panelId = node.attrs[TabAttrs.ariaControls];
tabAttrs[TabAttrs.ariaSelected] = 'true';
tabAttrs[TabAttrs.dataDiplodocIsActive] = 'true';
}

tr.setNodeMarkup(tabPos, null, tabAttrs);
});

if (!panelId) return false;

tabsNode.forEach((node, offset) => {
if (node.type !== tabPanelType(schema)) return;

const tabPanelPos = tabsPos + 1 + offset;
const tabPanelAttrs = {
...node.attrs,
};
const tabPanelClassList = new Set(
((node.attrs[TabPanelAttrs.class] as string) ?? '')
.split(' ')
.filter((val) => Boolean(val.trim())),
);

if (node.attrs[TabPanelAttrs.id] === panelId) {
tabPanelClassList.add('active');
} else {
tabPanelClassList.delete('active');
}

tabPanelAttrs[TabPanelAttrs.class] = Array.from(tabPanelClassList).join(' ');
tr.setNodeMarkup(tabPanelPos, null, tabPanelAttrs);
});

return true;
}

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

0 comments on commit 0e7a564

Please sign in to comment.