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' ;
24import {
35 findChildren ,
6+ findDomRefAtPos ,
47 findParentNodeOfType ,
58 findParentNodeOfTypeClosestToPos ,
69 NodeWithPos ,
@@ -27,6 +30,162 @@ import {
2730} from '../../' ;
2831import { atEndOfPanel } from './utils' ;
2932import { 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
31190export const tabPanelArrowDown : Command = ( state , dispatch , view ) => {
32191 const { selection : sel } = state ;
0 commit comments