@@ -79,6 +79,10 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
79
79
80
80
MAX_TABS = 4 ;
81
81
82
+ // Link drag and drop
83
+ _linkDropZone = null ;
84
+ _isLinkDragging = false ;
85
+
82
86
init ( ) {
83
87
this . handleTabEvent = this . _handleTabEvent . bind ( this ) ;
84
88
@@ -123,6 +127,11 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
123
127
tabBox . addEventListener ( 'dragover' , this . onBrowserDragOverToSplit . bind ( this ) ) ;
124
128
this . onBrowserDragEndToSplit = this . onBrowserDragEndToSplit . bind ( this ) ;
125
129
}
130
+
131
+ // If enabled initialize the link drag and drop
132
+ if ( Services . prefs . getBoolPref ( 'zen.splitView.enable-link-drop' ) ) {
133
+ this . #initLinkDragDropSplit( ) ;
134
+ }
126
135
}
127
136
128
137
insertIntoContextMenu ( ) {
@@ -1894,6 +1903,279 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
1894
1903
}
1895
1904
return true ;
1896
1905
}
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
+ }
1897
2179
}
1898
2180
1899
2181
window . gZenViewSplitter = new ZenViewSplitter ( ) ;
0 commit comments