@@ -79,6 +79,10 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
7979
8080 MAX_TABS = 4 ;
8181
82+ // Link drag and drop
83+ _linkDropZone = null ;
84+ _isLinkDragging = false ;
85+
8286 init ( ) {
8387 this . handleTabEvent = this . _handleTabEvent . bind ( this ) ;
8488
@@ -123,6 +127,11 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
123127 tabBox . addEventListener ( 'dragover' , this . onBrowserDragOverToSplit . bind ( this ) ) ;
124128 this . onBrowserDragEndToSplit = this . onBrowserDragEndToSplit . bind ( this ) ;
125129 }
130+
131+ // If enabled initialize the link drag and drop
132+ if ( Services . prefs . getBoolPref ( 'zen.splitView.enable-link-drop' ) ) {
133+ this . #initLinkDragDropSplit( ) ;
134+ }
126135 }
127136
128137 insertIntoContextMenu ( ) {
@@ -1894,6 +1903,279 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
18941903 }
18951904 return true ;
18961905 }
1906+
1907+ #initLinkDragDropSplit( ) {
1908+ this . _handleLinkDragEnter = this . _handleLinkDragEnter . bind ( this ) ;
1909+ this . _handleLinkDragLeave = this . _handleLinkDragLeave . bind ( this ) ;
1910+ this . _handleLinkDragDrop = this . _handleLinkDragDrop . bind ( this ) ;
1911+ this . _handleLinkDragEnd = this . _handleLinkDragEnd . bind ( this ) ;
1912+
1913+ const tabBox = document . getElementById ( 'tabbrowser-tabbox' ) ;
1914+
1915+ tabBox . addEventListener ( 'dragenter' , this . _handleLinkDragEnter , true ) ;
1916+ tabBox . addEventListener ( 'dragleave' , this . _handleLinkDragLeave , false ) ;
1917+ tabBox . addEventListener ( 'drop' , this . _handleLinkDragDrop , false ) ;
1918+ tabBox . addEventListener ( 'dragend' , this . _handleLinkDragEnd , false ) ;
1919+ }
1920+
1921+ _createLinkDropZone ( ) {
1922+ if ( this . _linkDropZone ) return ;
1923+
1924+ this . _linkDropZone = document . createXULElement ( 'box' ) ;
1925+ this . _linkDropZone . id = 'zen-drop-link-zone' ;
1926+
1927+ const content = document . createXULElement ( 'vbox' ) ;
1928+ content . setAttribute ( 'align' , 'center' ) ;
1929+ content . setAttribute ( 'pack' , 'center' ) ;
1930+ content . setAttribute ( 'flex' , '1' ) ;
1931+
1932+ const text = document . createXULElement ( 'description' ) ;
1933+ text . setAttribute ( 'value' , 'Drop link to split' ) ; // Localization! data-l10n-id
1934+
1935+ content . appendChild ( text ) ;
1936+ this . _linkDropZone . appendChild ( content ) ;
1937+
1938+ this . _linkDropZone . addEventListener ( 'dragover' , ( event ) => {
1939+ event . preventDefault ( ) ;
1940+ event . stopPropagation ( ) ;
1941+ event . dataTransfer . dropEffect = 'link' ;
1942+ if ( ! this . _linkDropZone . hasAttribute ( 'has-focus' ) ) {
1943+ this . _linkDropZone . setAttribute ( 'has-focus' , 'true' ) ;
1944+ }
1945+ } ) ;
1946+
1947+ this . _linkDropZone . addEventListener ( 'dragleave' , ( event ) => {
1948+ event . stopPropagation ( ) ;
1949+ if ( ! this . _linkDropZone . contains ( event . relatedTarget ) ) {
1950+ this . _linkDropZone . removeAttribute ( 'has-focus' ) ;
1951+ }
1952+ } ) ;
1953+
1954+ this . _linkDropZone . addEventListener ( 'drop' , this . _handleDropForSplit . bind ( this ) ) ;
1955+
1956+ const tabBox = document . getElementById ( 'tabbrowser-tabbox' ) ;
1957+ tabBox . appendChild ( this . _linkDropZone ) ;
1958+ }
1959+
1960+ _showLinkDropZone ( ) {
1961+ if ( ! this . _linkDropZone ) this . _createLinkDropZone ( ) ;
1962+
1963+ this . _linkDropZone . setAttribute ( 'enabled' , 'true' ) ;
1964+ }
1965+
1966+ _hideLinkDropZone ( force = false ) {
1967+ if ( ! this . _linkDropZone || ! this . _linkDropZone . hasAttribute ( 'enabled' ) ) return ;
1968+
1969+ if ( this . _isLinkDragging && ! force ) return ;
1970+
1971+ this . _linkDropZone . removeAttribute ( 'enabled' ) ;
1972+ this . _linkDropZone . removeAttribute ( 'has-focus' ) ;
1973+ }
1974+
1975+ _validateURI ( dataTransfer ) {
1976+ let dt = dataTransfer ;
1977+
1978+ const URL_TYPES = [ 'text/uri-list' , 'text/x-moz-url' , 'text/plain' ] ;
1979+
1980+ const FIXUP_FLAGS = Ci . nsIURIFixup . FIXUP_FLAG_FIX_SCHEME_TYPOS ;
1981+
1982+ const matchedType = URL_TYPES . find ( ( type ) => {
1983+ const raw = dt . getData ( type ) ;
1984+ return typeof raw === 'string' && raw . trim ( ) . length > 0 ;
1985+ } ) ;
1986+
1987+ const uriString = dt . getData ( matchedType ) . trim ( ) ;
1988+
1989+ const info = Services . uriFixup . getFixupURIInfo ( uriString , FIXUP_FLAGS ) ;
1990+
1991+ if ( ! info || ! info . fixedURI ) {
1992+ return null ;
1993+ }
1994+
1995+ return info . fixedURI . spec ;
1996+ }
1997+
1998+ _handleLinkDragEnter ( event ) {
1999+ // If rearrangeViewEnabled - don't do anything
2000+ if ( this . rearrangeViewEnabled ) {
2001+ return ;
2002+ }
2003+
2004+ const shouldBeDisabled = ! this . canOpenLinkInSplitView ( ) ;
2005+ if ( shouldBeDisabled ) return ;
2006+
2007+ // If the target is our drop zone or one of its children, or already active, do nothing here.
2008+ if (
2009+ this . _linkDropZone &&
2010+ ( this . _linkDropZone . contains ( event . target ) || this . _linkDropZone . hasAttribute ( 'enabled' ) )
2011+ ) {
2012+ return ;
2013+ }
2014+
2015+ // If the data is not a valid URI, we don't want to do anything
2016+ if ( ! this . _validateURI ( event . dataTransfer ) ) {
2017+ return ;
2018+ }
2019+
2020+ this . _isLinkDragging = true ;
2021+ this . _showLinkDropZone ( ) ;
2022+
2023+ event . preventDefault ( ) ;
2024+ event . stopPropagation ( ) ;
2025+ }
2026+
2027+ _handleLinkDragLeave ( event ) {
2028+ if (
2029+ event . target === document . documentElement ||
2030+ ( event . clientX <= 0 && event . clientY <= 0 ) ||
2031+ event . clientX >= window . innerWidth ||
2032+ event . clientY >= window . innerHeight
2033+ ) {
2034+ if ( this . _linkDropZone && ! this . _linkDropZone . contains ( event . relatedTarget ) ) {
2035+ this . _isLinkDragging = false ;
2036+ this . _hideLinkDropZone ( ) ;
2037+ }
2038+ }
2039+ }
2040+
2041+ _handleLinkDragDrop ( event ) {
2042+ if ( ! this . _linkDropZone || ! this . _linkDropZone . contains ( event . target ) ) {
2043+ if ( this . _linkDropZone && this . _linkDropZone . hasAttribute ( 'enabled' ) ) {
2044+ this . _isLinkDragging = false ;
2045+ this . _hideLinkDropZone ( true ) ; // true for forced hiding
2046+ }
2047+ }
2048+ }
2049+
2050+ _handleLinkDragEnd ( event ) {
2051+ this . _isLinkDragging = false ;
2052+ this . _hideLinkDropZone ( true ) ; // true for forced hiding
2053+ }
2054+
2055+ _handleDropForSplit ( event ) {
2056+ let linkDropZone = this . _linkDropZone ;
2057+ event . preventDefault ( ) ;
2058+ event . stopPropagation ( ) ;
2059+
2060+ const url = this . _validateURI ( event . dataTransfer ) ;
2061+
2062+ if ( ! url ) {
2063+ this . _hideDropZoneAndResetState ( ) ;
2064+ return ;
2065+ }
2066+
2067+ const currentTab = gZenGlanceManager . getTabOrGlanceParent ( gBrowser . selectedTab ) ;
2068+ const newTab = this . openAndSwitchToTab ( url , { inBackground : false } ) ;
2069+
2070+ if ( ! newTab ) {
2071+ this . _hideDropZoneAndResetState ( ) ;
2072+ return ;
2073+ }
2074+
2075+ const linkDropSide = this . _calculateDropSide ( event , linkDropZone ) ;
2076+
2077+ this . _createOrUpdateSplitViewWithSide ( currentTab , newTab , linkDropSide ) ;
2078+
2079+ this . _hideDropZoneAndResetState ( ) ;
2080+ }
2081+ _calculateDropSide ( event , linkDropZone ) {
2082+ const rect = linkDropZone . getBoundingClientRect ( ) ;
2083+ const x = event . clientX - rect . left ;
2084+ const y = event . clientY - rect . top ;
2085+ const width = rect . width ;
2086+ const height = rect . height ;
2087+
2088+ const edgeSizeRatio = 0.3 ; // 30% of the size, maybe increase to 35%
2089+ const hEdge = width * edgeSizeRatio ;
2090+ const vEdge = height * edgeSizeRatio ;
2091+
2092+ const isInLeftEdge = x < hEdge ;
2093+ const isInRightEdge = x > width - hEdge ;
2094+ const isInTopEdge = y < vEdge ;
2095+ const isInBottomEdge = y > height - vEdge ;
2096+
2097+ if ( isInTopEdge ) {
2098+ if ( isInLeftEdge && x / width < y / height ) return 'left' ; // More left in angle
2099+ if ( isInRightEdge && ( width - x ) / width < y / height ) return 'right' ; // More right in angle
2100+ return 'top' ;
2101+ }
2102+ if ( isInBottomEdge ) {
2103+ if ( isInLeftEdge && x / width < ( height - y ) / height ) return 'left' ;
2104+ if ( isInRightEdge && ( width - x ) / width < ( height - y ) / height ) return 'right' ;
2105+ return 'bottom' ;
2106+ }
2107+ if ( isInLeftEdge ) {
2108+ return 'left' ;
2109+ }
2110+ if ( isInRightEdge ) {
2111+ return 'right' ;
2112+ }
2113+ return 'center' ;
2114+ }
2115+
2116+ _createOrUpdateSplitViewWithSide ( currentTab , newTab , linkDropSide ) {
2117+ const SIDES = [ 'left' , 'right' , 'top' , 'bottom' ] ;
2118+ const groupIndex = this . _data . findIndex ( ( group ) => group . tabs . includes ( currentTab ) ) ;
2119+
2120+ if ( groupIndex > - 1 ) {
2121+ const group = this . _data [ groupIndex ] ;
2122+
2123+ if ( group . tabs . length >= this . MAX_TABS ) {
2124+ console . warn ( `Cannot add tab to split, MAX_TABS (${ this . MAX_TABS } ) reached.` ) ;
2125+ return ;
2126+ }
2127+
2128+ const splitViewGroup = this . _getSplitViewGroup ( group . tabs ) ;
2129+ if ( splitViewGroup && newTab . group !== splitViewGroup ) {
2130+ this . _moveTabsToContainer ( [ newTab ] , currentTab ) ;
2131+ gBrowser . moveTabToGroup ( newTab , splitViewGroup ) ;
2132+ }
2133+
2134+ if ( ! group . tabs . includes ( newTab ) ) {
2135+ group . tabs . push ( newTab ) ;
2136+
2137+ const targetNode = this . getSplitNodeFromTab ( currentTab ) ;
2138+ const isValidSide = SIDES . includes ( linkDropSide ) ;
2139+
2140+ if ( targetNode && isValidSide ) {
2141+ this . splitIntoNode ( targetNode , new SplitLeafNode ( newTab , 50 ) , linkDropSide , 0.5 ) ;
2142+ } else {
2143+ const parentNode = targetNode ?. parent || group . layoutTree ;
2144+ this . addTabToSplit ( newTab , parentNode , false ) ;
2145+ }
2146+
2147+ this . activateSplitView ( group , true ) ;
2148+ }
2149+ return ;
2150+ }
2151+
2152+ const splitConfig = {
2153+ left : { tabs : [ newTab , currentTab ] , gridType : 'vsep' , initialIndex : 0 } ,
2154+ right : { tabs : [ currentTab , newTab ] , gridType : 'vsep' , initialIndex : 1 } ,
2155+ top : { tabs : [ newTab , currentTab ] , gridType : 'hsep' , initialIndex : 0 } ,
2156+ bottom : { tabs : [ currentTab , newTab ] , gridType : 'hsep' , initialIndex : 1 } ,
2157+ } ;
2158+
2159+ const {
2160+ tabs : tabsToSplit ,
2161+ gridType,
2162+ initialIndex,
2163+ } = splitConfig [ linkDropSide ] || {
2164+ // If linkDropSide is invalid should use the default "vsep"
2165+ tabs : [ currentTab , newTab ] ,
2166+ gridType : 'vsep' ,
2167+ initialIndex : 1 ,
2168+ } ;
2169+
2170+ this . splitTabs ( tabsToSplit , gridType , initialIndex ) ;
2171+ }
2172+
2173+ _hideDropZoneAndResetState ( ) {
2174+ if ( this . _linkDropZone && this . _linkDropZone . hasAttribute ( 'enabled' ) ) {
2175+ this . _isLinkDragging = false ;
2176+ this . _hideLinkDropZone ( true ) ;
2177+ }
2178+ }
18972179}
18982180
18992181window . gZenViewSplitter = new ZenViewSplitter ( ) ;
0 commit comments