[Mod] Tabs filtering by title/url



  • Hello,

    Since I usually use tons of tabs, I made a mod to be able to filter them by title and/or url.

    v2.0 now compatible with Vivaldi 1.2.490.43 (latest stable version) and v1.3.519.25 (latest snapshot).
    (previous versions worked only with Vivaldi 1.3.501.6)

    Edit 07/03/2016 : Bug : tabs in stacks are not displayed in tabs list. For now, you should disable tab-stacking if you plan to use this mod.

    unfiltered :
    [img]http://i.imgur.com/18fW1RK.jpg[/img]

    Typed "viva" to filter:
    [img]http://i.imgur.com/gFxaPKl.jpg[/img]

    [code]
    /*
    Modification for Vivaldi browser that adds a text input to filter tabs.
    Version : 2.0

    Installation :
    - name this file "tab-filtering.js"
    - save it in Vivaldi's resources/vivaldi folder (where "browser.html" file is located).
    - edit "browser.html" to add "<script src="tab-filtering.js"></script>" before "</body>"

    Known limitations / bugs :
    - drag/droping tabs ignores filtering
    - tab cycling ignores filtering

    Changelog :
    v1.1
    bugfix : now unfilter when input is reset on focus
    v1.2
    bugfix : filtered tabs are now in correct order
    v2.0
    feature : added a button to select all visible tabs
    enhancement : compatible with 1.2.490.43 (latest stable version) and v1.3.519.25 (latest snapshot).
    enhancement : added variables for CSS code
    enhancement : filtering by url now works with hibernated pages.
    enhancement ! filter ignores dot, underscore, and space characters.
    bugfix : "+" (new tab) button is repositioned correctly
    bugfix : filtered tabs are now in correct order (different bug)
    bugfix : tab views are repositioned when a tab view is added/removed from the list
    bugfix : hidden (filtered) tabs are unselected. This allows to use shift+lmb while filtering.
    bugfix : failsafe when numbers of tab views and page views are inconsistent (due to tab closing)
    bugfix : handles "vivaldi://settings/startup/" title mismatch
    bugfix : reposition tabs when window is resized

    The MIT License (MIT)
    Copyright (c) 2016 "OVNI"

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */

    !function() {

    // stuff that I might use...
    // window.PageActions.clearSelection()
    // window.PageActions.moveToIndex( window.PageStore.getActivePage(), 0)

    ///
    ///
    ///
    var Settings = {

    ignoredChars : /\.|_| /g,
    
    fieldPlaceholder : "Search...",
    checkboxTitle : "Select visible tabs",
    
    divCss : "display:flex;",
    fieldCss : "height:24px; width:100%; margin:3px;",
    checkboxCss : "margin-top:8px; margin-left:3px; margin-right:10px;",
    
    /// Delay to reposition tabs after animations, 
    /// so tabs will be positioned based on their final position instead of an intermediate one.
    updateDelayForAnimationMs : 300,
    
    /// When tabs container is "moved" (top/left/right/bottom in Vivladi settings), a new one is created.
    /// This is the interval used to check if filter gui is attached to the current tabs container.
    filterUiCheckIntervalMs : 500,
    

    };

    ///
    ///
    ///
    var Facade = {

    getVivaldiVersion : function() {
    	return navigator.appVersion.match(/Vivaldi\/(.*)/)[1];
    },
    
    
    getTabContainer : function() {
    	return document.getElementById("tabs-container");
    },
    
    
    isTabContainerVertical : function() {
    	return Facade.getTabContainer().className.match(/left|right/) != null;
    },
    
    
    getInnerTabContainer : function() {
    	return Facade.getTabContainer().getElementsByClassName("tab-strip")[0];
    },
    
    
    getNewTabButton : function() {
    	return Facade.getTabContainer().getElementsByClassName("newtab")[0];
    },
    
    
    repositionNewTabButton : function( numVisibleTabs ) {
    	if( Facade.isTabContainerVertical() ) {
    		Facade.getNewTabButton().style.top = numVisibleTabs * Facade.getTabElementHeight() + "px";
    	}else {
    		Facade.getNewTabButton().style.left = numVisibleTabs * Facade.getTabElementWidth() + "px";
    	}
    },
    
    
    getTabElements : function() {
    	return Facade.getTabContainer().getElementsByClassName("tab-position");
    },
    
    
    getTabElementTitle : function( tabElement ) {
    	return tabElement.firstElementChild.title;
    },
    
    
    getTabElementPos : function( tabElement ) {
    	return {
    		x : parseInt( tabElement.style.left ),
    		y : parseInt( tabElement.style.top )
    	};
    },
    
    
    offsetTabElement : function( tabElement, x, y ) {
    	tabElement.style.transform = "translate(" + x + "px," + y + "px)";
    },
    
    
    getPageCount : function() {
    	return window.PageStore.getPages().size;
    },
    
    
    getPageTitle : function( pageIndex ) {
    	return window.PageStore.getPages().get(pageIndex).get("title");
    },
    
    
    getPageUrl : function( pageIndex ) {
    	return window.PageStore.getPages().get(pageIndex).get("url");
    },
    
    
    getTabElementHeight : function() {
    	return parseInt( Facade.getTabElements()[0].style.height );
    },
    
    
    getTabElementWidth : function() {
    	return parseInt( Facade.getTabElements()[0].style.width );
    },
    

    };

    ///
    ///
    ///
    var TabView = function( element ) {
    this.element = element;
    this.title = Facade.getTabElementTitle(element).toLowerCase();
    this.elementPos = Facade.getTabElementPos(element);
    }
    TabView.prototype = {

    dispatchMouseEvent : function( type, button, ctrlKey, shiftKey ) {
    	var evt = document.createEvent("MouseEvents");
    	evt.initMouseEvent(type, true, true, undefined, undefined, undefined, undefined, undefined, undefined, ctrlKey, undefined, shiftKey, undefined, button );
    	this.element.dispatchEvent(evt);
    },
    
    select : function() {
    	if( this.isSelected() ) return;
    	this.dispatchMouseEvent( "mousedown", 0, true );
    	this._selected = true;
    },
    
    
    deselect : function() {
    	if( ! this.isSelected() ) return;
    	this.dispatchMouseEvent( "mousedown", 0, true );
    	this._selected = false;
    },
    
    
    isSelected : function() {
    	if( this._selected !== undefined ) {
    		return this._selected;
    	}
    	return this.element.getElementsByClassName("marked").length > 0;
    },
    
    
    show : function() {
    	this.element.style.display = "";
    },
    
    
    hide : function() {
    	this.element.style.display = "none";
    },
    
    
    isVisible : function() {
    	return this.element.style.display != "none";
    },
    

    };

    ///
    ///
    ///
    var TabList = function() {
    ///
    /// Array<TabView> containing TabViews that match current filter.
    ///
    this.matchedTabViews = [];

    // Store unmatched views due to a Vivaldi's bug that adds active tab to selection when another tab receive ctrl+LMB.
    //TODO remove once bug fixed
    this.unmatchedTabViews = [];
    

    }
    TabList.prototype = {

    ///
    /// Returns a map (key = title:string, value = visible:bool).
    /// @private
    ///
    getVisibilityByTitleMap : function( filter ) {
    	var map = {};
    	for( var i=0,n=Facade.getPageCount(); i<n; i++ ) {
    		var title = Facade.getPageTitle(i).toLowerCase();
    		map[title] = this.matchesFilter( title, Facade.getPageUrl(i), filter );
    	}
    	// view vs model titles mismatch for "vivaldi://settings/startup" url (which isn't the real url btw)
    	map["startup"] = map["settings"];
    	return map;
    },
    
    
    ///
    /// @private
    ///
    matchesFilter : function ( title, url, filter ) {
    	title = title.replace( Settings.ignoredChars, '' );
    	url = url.replace( Settings.ignoredChars, '' );
    	filter = filter.replace( Settings.ignoredChars, '' );
    	var re = new RegExp(filter,'i');
    	return re.test(title) || re.test(url);
    },
    
    
    ///
    /// 
    ///
    filterTabViews : function( filter ) {
    	var visibilityMap = this.getVisibilityByTitleMap(filter);
    	var tabElements = Facade.getTabElements();
    	
    	for( var i=0,n=tabElements.length; i<n; i++ ) {
    		var tabView = new TabView( tabElements[i] );
    		if( filter=="" || visibilityMap[tabView.title] ) {
    			this.matchedTabViews.push(tabView);
    			tabView.show();
    		}else {
    			this.unmatchedTabViews.push(tabView); //TODO remove once Vivaldi bug fixed
    			tabView.hide();
    			tabView.deselect();
    		}
    	}
    	
    	/*
    	// minimize active page if it doesnt matches filter
    	// Note : not sure if it's a good idea...
    	if( this.matchedTabViews.length > 0 ) {
    		while( true ) {
    			var activePage = window.PageStore.getActivePage();
    			// do not use visibilityMap here because Vivaldi adds suffix to title if it cannot connect to the page => endless loop
    			var matches = this.matchesFilter( activePage.get("title"), activePage.get("url"), filter );
    			if(matches) {
    				break;
    			}else {
    				window.PageActions.minimizePage( activePage );
    				//TODO focus on filter's input
    			}
    		}
    	}
    	*/
    },
    
    ///
    /// @private
    ///
    sortTabViews : function( tabViews ) {
    	if( Facade.isTabContainerVertical() ) {
    		tabViews.sort( function(a,b) {
    			return a.elementPos.y - b.elementPos.y;
    		});
    	}
    	else {
    		tabViews.sort( function(a,b) {
    			return a.elementPos.x - b.elementPos.x;
    		});
    	}
    },
    
    ///
    /// 
    ///
    repositionTabViews : function() {
    	var tabElementHeight = Facade.getTabElementHeight();
    	var tabElementWidth = Facade.getTabElementWidth();
    	
    	this.sortTabViews( this.matchedTabViews );
    	
    	for( var i=0, n=this.matchedTabViews.length; i<n; i++ ) {
    		var tabView = this.matchedTabViews[i];
    		var offsetX = 0;
    		var offsetY = 0;
    		if( Facade.isTabContainerVertical() ) {
    			offsetY = i*tabElementHeight - tabView.elementPos.y;
    		}else {
    			offsetX = i*tabElementWidth - tabView.elementPos.x;
    		}
    		Facade.offsetTabElement( tabView.element, offsetX, offsetY );
    	}
    	
    	this.repositionNewTabButton();
    },
    
    
    repositionNewTabButton : function() {
    	Facade.repositionNewTabButton( this.matchedTabViews.length );
    },
    
    
    selectMatchedTabViews : function() {
    	for( var i=0,n=this.matchedTabViews.length; i<n; i++ ) {
    		this.matchedTabViews[i].select();
    	}
    	// deselected unmatched views due to a Vivaldi's bug that adds active tab to selection when another tab receive ctrl+LMB.
    	//TODO remove once bug fixed
    	for( var i=0,n=this.unmatchedTabViews.length; i<n; i++ ) {
    		this.unmatchedTabViews[i].deselect();
    	}
    },
    

    };

    ///
    ///
    ///
    var TabFilter = function() {
    this.createView();
    this.observer = new MutationObserver( this.handleMutations.bind(this) );

    window.addEventListener("resize", this.handleWindowResize.bind(this) );
    
    // leave timer running since changing tabs orientation creates a new container.
    setInterval( this.handleTabContainerCheckTimer.bind(this), Settings.filterUiCheckIntervalMs );
    

    };
    TabFilter.prototype = {

    handleTabContainerCheckTimer : function() {
    	var tabContainer = Facade.getTabContainer();
    	if( tabContainer && this.div.parentNode != tabContainer ) {
    		this.attachView();
    		this.observe();
    		// update on mouseup to reposition after drag/drop
    		tabContainer.addEventListener( "mouseup", this.handleMouseUp.bind(this) );
    	}
    },
    
    
    createView : function() {
    	var div = document.createElement("div");
    	div.style.cssText = Settings.divCss;
    	
    	var field = document.createElement("input");
    	field.spellcheck = false;
    	field.type = "text";
    	field.style.cssText = Settings.fieldCss;
    	field.placeholder = Settings.fieldPlaceholder;
    	field.onfocus = this.handleFieldFocus.bind(this);
    	field.oninput = this.update.bind(this);
    	div.appendChild(field);
    	
    	var checkbox = document.createElement("input");
    	checkbox.type = "checkbox";
    	checkbox.style.cssText = Settings.checkboxCss;
    	checkbox.title = Settings.checkboxTitle;
    	checkbox.checked = true;
    	checkbox.onclick = this.handleCheckboxClick.bind(this);
    	div.appendChild(checkbox);
    	
    	this.div = div;
    	this.field = field;
    	this.checkbox = checkbox;
    },
    
    
    handleWindowResize : function(evt) {
    	this.update();
    	setTimeout( this.update.bind(this), Settings.updateDelayForAnimationMs );
    },
    
    
    handleFieldFocus : function(evt) {
    	this.field.value = ""; // auto reset filter
    	this.update();
    },
    
    
    handleCheckboxClick : function(evt) {
    	var tabList = new TabList();
    	tabList.filterTabViews( this.field.value );
    	tabList.selectMatchedTabViews();
    	this.checkbox.checked = true;
    },
    
    
    attachView : function() {
    	var tabContainer = Facade.getTabContainer();
    	tabContainer.insertBefore( this.div, tabContainer.firstChild );
    },
    
    
    handleMouseUp : function() {
    	setTimeout( this.update.bind(this), Settings.updateDelayForAnimationMs );
    },
    
    
    handleMutations : function(mutations) {
    	if( mutations[0].addedNodes.length > 0 ) {
    		this.update();
    		setTimeout( this.update.bind(this), Settings.updateDelayForAnimationMs );
    	}
    	else {
    		var tabList = new TabList();
    		tabList.filterTabViews( this.field.value );
    		tabList.repositionNewTabButton();
    	}
    },
    
    
    observe : function() {
    	this.observer.disconnect();
    	this.observer.observe(
    		Facade.getInnerTabContainer(), {
    		attributes: false, 
    		childList: true, 
    		characterData: false 
    	});
    },
    
    
    update : function() {
    	var tabList = new TabList();
    	tabList.filterTabViews( this.field.value );
    	tabList.repositionTabViews();
    }
    

    };

    // Compatibility fixes for last stable version
    if( Facade.getVivaldiVersion() === "1.2.490.43" ) {

    Facade.getTabElementTitle = function( tabElement ) {
    	return tabElement.getElementsByClassName("title")[0].textContent;
    };
    
    
    Facade.getTabElementPos = function( tabElement ) {
    	var coordsString = tabElement.style.transform.match(/\(.*\)/)[0];
    	var matches = coordsString.match(/[\d\.]+/g);
    	return {
    		x:matches[0],
    		y:matches[1]
    	};
    };
    
    
    Facade.offsetTabElement = function( tabElement, x, y ) {
    	tabElement.style.left = x + "px";
    	tabElement.style.top = y + "px";
    };
    
    
    Facade.repositionNewTabButton = function( numVisibleTabs ) {
    	if( Facade.isTabContainerVertical() ) {
    		Facade.getInnerTabContainer().style.maxHeight = numVisibleTabs * Facade.getTabElementHeight() + "px";
    	}else {
    		Facade.getInnerTabContainer().style.maxWidth = numVisibleTabs * Facade.getTabElementWidth() + "px";
    	}
    };
    
    
    TabFilter.prototype.handleMutations = function(mutations) {
    	if( mutations[0].addedNodes.length > 0 ) {
    		this.update();
    		setTimeout( this.update.bind(this), Settings.updateDelayForAnimationMs );
    	}else {
    		this.update();	
    	}
    };
    

    }

    new TabFilter();

    }();
    [/code]



  • Hello again,

    Changed lot (most) of the code to get something more useful :

    • compatible with Vivendi 1.2.490.43 (last stable) and 1.3.519.25 (last snapshot).
    • handles selection flawlessly (using ctrl and/or shift + LMB).
    • filter by url now works for hibernated tabs too.
    • added a button to select all visible tabs, which can be handy to quickly bookmark and/or close all filtered tabs.
    • fixed a bunch of bugs + some minor improvements (full changelog is in code's header).

    I've edited the first message with new code.



  • Wow, cool. Thank you!


Log in to reply
 

Looks like your connection to Vivaldi Forum was lost, please wait while we try to reconnect.