1+ /* eslint-disable */
12/* -*- indent-tabs-mode: nil; tab-width: 2; -*- */
23/* vim: set ts=2 sw=2 et ai : */
34/**
1920 @license
2021**/
2122
23+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2224import browser from 'webextension-polyfill' ;
2325import { DisplayedContainer } from 'weeg-containers' ;
2426import { EventSink } from "weeg-events" ;
@@ -32,6 +34,7 @@ export class MenulistContainerElement extends HTMLElement {
3234 private _tabCount = 0 ;
3335 private _hidden = false ;
3436 private readonly _isPrivate ;
37+ private readonly _cookieStoreId : string ;
3538
3639 public readonly onContainerHide = new EventSink < void > ( ) ;
3740 public readonly onContainerUnhide = new EventSink < void > ( ) ;
@@ -52,6 +55,7 @@ export class MenulistContainerElement extends HTMLElement {
5255 throw new Error ( "Shadow root is null" ) ;
5356 }
5457 this . _isPrivate = isPrivate || displayedContainer . cookieStore . isPrivate ;
58+ this . _cookieStoreId = displayedContainer . cookieStore . id ;
5559 this . buildElement ( ) ;
5660 this . setDisplayedContainer ( displayedContainer ) ;
5761 this . containerCloseButton . title = browser . i18n . getMessage ( 'tooltipContainerCloseAll' ) ;
@@ -60,6 +64,7 @@ export class MenulistContainerElement extends HTMLElement {
6064 this . containerHighlightButton . title = browser . i18n . getMessage ( 'focusToThisContainer' ) ;
6165 this . tabCount = 0 ;
6266 this . registerEventListeners ( ) ;
67+ this . setupDragHandlers ( ) ;
6368 }
6469
6570 private buildElement ( ) {
@@ -141,8 +146,160 @@ export class MenulistContainerElement extends HTMLElement {
141146 this . containerHighlightButton . onclick = ( ) => {
142147 this . onContainerHighlight . dispatch ( ) ;
143148 } ;
149+ this . setupClickHandlers ( ) ;
144150 }
145151
152+ private setupDragHandlers ( ) {
153+ const containerTabsElement = this . shadowRoot ?. querySelector ( '#container-tabs' ) as HTMLDivElement | null ;
154+ if ( ! containerTabsElement ) return ;
155+
156+ containerTabsElement . addEventListener ( 'dragstart' , ( ev : DragEvent ) => {
157+ const tabElement = ( ev . target as HTMLElement ) . closest ( 'menulist-tab' ) as HTMLElement | null ;
158+ if ( ! tabElement || ! ev . dataTransfer ) return ;
159+
160+ const tabId = parseInt ( tabElement . getAttribute ( 'data-tab-id' ) || '-1' , 10 ) ;
161+ const tabIndex = parseInt ( tabElement . getAttribute ( 'data-index' ) || '0' , 10 ) ;
162+ if ( tabId === - 1 ) return ;
163+
164+ ev . dataTransfer . setData ( 'application/json' , JSON . stringify ( {
165+ type : 'tab' ,
166+ id : tabId ,
167+ index : tabIndex ,
168+ pinned : false ,
169+ cookieStoreId : this . _cookieStoreId ,
170+ } ) ) ;
171+ ev . dataTransfer . dropEffect = 'move' ;
172+ } ) ;
173+
174+ containerTabsElement . addEventListener ( 'dragover' , ( ev : DragEvent ) => {
175+ const tabElement = ( ev . target as HTMLElement ) . closest ( 'menulist-tab' ) as HTMLElement | null ;
176+ if ( ! tabElement || ! ev . dataTransfer ) return ;
177+
178+ const json = ev . dataTransfer . getData ( 'application/json' ) ;
179+ if ( ! json ) return ;
180+
181+ try {
182+ const data = JSON . parse ( json ) ;
183+ if ( 'tab' !== data . type || data . pinned ) return ;
184+ if ( data . cookieStoreId !== this . _cookieStoreId ) return ;
185+ ev . preventDefault ( ) ;
186+ } catch ( _e ) {
187+ // Invalid JSON, ignore
188+ }
189+ } ) ;
190+
191+ containerTabsElement . addEventListener ( 'drop' , ( ev : DragEvent ) => {
192+ const tabElement = ( ev . target as HTMLElement ) . closest ( 'menulist-tab' ) as HTMLElement | null ;
193+ if ( ! tabElement || ! ev . dataTransfer ) return ;
194+
195+ const targetTabId = parseInt ( tabElement . getAttribute ( 'data-tab-id' ) || '-1' , 10 ) ;
196+ const targetIndex = parseInt ( tabElement . getAttribute ( 'data-index' ) || '0' , 10 ) ;
197+ if ( targetTabId === - 1 ) return ;
198+
199+ const json = ev . dataTransfer . getData ( 'application/json' ) ;
200+ if ( ! json ) return ;
201+
202+ try {
203+ const data = JSON . parse ( json ) ;
204+ if ( 'tab' !== data . type || data . pinned ) return ;
205+ if ( data . cookieStoreId !== this . _cookieStoreId ) return ;
206+ ev . preventDefault ( ) ;
207+
208+ browser . tabs . move ( data . id , { index : targetIndex } ) . catch ( ( e ) => {
209+ console . error ( e ) ;
210+ } ) ;
211+ } catch ( _e ) {
212+ // Invalid JSON, ignore
213+ }
214+ } ) ;
215+ }
216+
217+ private setupClickHandlers ( ) : void {
218+ const containerTabsElement = this . shadowRoot ?. querySelector ( '#container-tabs' ) as HTMLDivElement | null ;
219+ if ( ! containerTabsElement ) return ;
220+
221+ // Click event delegation
222+ containerTabsElement . addEventListener ( 'click' , ( ev : MouseEvent ) => {
223+ const tabElement = ( ev . target as HTMLElement ) . closest ( 'menulist-tab' ) as HTMLElement | null ;
224+ if ( ! tabElement ) return ;
225+
226+ const tabId = parseInt ( tabElement . getAttribute ( 'data-tab-id' ) || '-1' , 10 ) ;
227+ if ( tabId === - 1 ) return ;
228+
229+ // Get the actual clicked element from composedPath (for Shadow DOM)
230+ const path = ev . composedPath ( ) ;
231+ const shadowTarget = path [ 0 ] as HTMLElement ;
232+ const action = shadowTarget . getAttribute ?.( 'data-action' ) || shadowTarget . closest ( '[data-action]' ) ?. getAttribute ( 'data-action' ) ;
233+
234+ if ( ! action ) return ;
235+
236+ switch ( action ) {
237+ case 'tab-click' :
238+ // Focus the tab
239+ browser . tabs . update ( tabId , { active : true } ) . catch ( ( e ) => {
240+ console . error ( 'Failed to focus tab:' , e ) ;
241+ } ) ;
242+ break ;
243+
244+ case 'close' :
245+ // Close the tab
246+ browser . tabs . remove ( tabId ) . catch ( ( e ) => {
247+ console . error ( 'Failed to close tab:' , e ) ;
248+ } ) ;
249+ break ;
250+
251+ case 'pin' :
252+ // Toggle pin status
253+ const isPinned = tabElement . querySelector ( '#tab-pin-button' ) ?. classList . contains ( 'pinned' ) ;
254+ browser . tabs . update ( tabId , { pinned : ! isPinned } ) . catch ( ( e ) => {
255+ console . error ( 'Failed to pin/unpin tab:' , e ) ;
256+ } ) ;
257+ break ;
258+
259+ case 'set-tag' :
260+ // Import ModalSetTagElement dynamically to avoid circular dependency
261+ import ( './modal-set-tag' ) . then ( ( { ModalSetTagElement } ) => {
262+ document . body . appendChild ( new ModalSetTagElement ( tabId ) ) ;
263+ } ) . catch ( ( e ) => {
264+ console . error ( 'Failed to load ModalSetTagElement:' , e ) ;
265+ } ) ;
266+ break ;
267+ }
268+ } ) ;
269+
270+ // Auxclick event (middle mouse button)
271+ containerTabsElement . addEventListener ( 'auxclick' , ( ev : MouseEvent ) => {
272+ if ( ev . button !== 1 ) return ; // Only handle middle click
273+
274+ const tabElement = ( ev . target as HTMLElement ) . closest ( 'menulist-tab' ) as HTMLElement | null ;
275+ if ( ! tabElement ) return ;
276+
277+ const tabId = parseInt ( tabElement . getAttribute ( 'data-tab-id' ) || '-1' , 10 ) ;
278+ if ( tabId === - 1 ) return ;
279+
280+ // Middle click closes the tab
281+ browser . tabs . remove ( tabId ) . catch ( ( e ) => {
282+ console . error ( 'Failed to close tab:' , e ) ;
283+ } ) ;
284+ } ) ;
285+
286+ // Contextmenu event
287+ containerTabsElement . addEventListener ( 'contextmenu' , ( ev : MouseEvent ) => {
288+ const tabElement = ( ev . target as HTMLElement ) . closest ( 'menulist-tab' ) as HTMLElement | null ;
289+ if ( ! tabElement ) return ;
290+
291+ const tabId = parseInt ( tabElement . getAttribute ( 'data-tab-id' ) || '-1' , 10 ) ;
292+ if ( tabId === - 1 ) return ;
293+
294+ // Override context menu for Firefox
295+ browser . menus . overrideContext ( {
296+ context : 'tab' ,
297+ tabId : tabId ,
298+ } ) ;
299+ } , { capture : true } ) ;
300+ }
301+
302+
146303 public setDisplayedContainer ( displayedContainer : DisplayedContainer ) {
147304 if ( this . _isPrivate ) {
148305 console . assert ( displayedContainer . cookieStore . userContextId == 0 , "Private window should have default container only" ) ;
0 commit comments