Activate Tab On Hover
-
New fantastic mod by luetage: Activate Tab On Hover
I'm an absolute beginner. Nevertheless I cannot stop searching for a possibility to activate a tab by hovering the mouse pointer.
The Option is in Tab Mix Plus and in the Cent Browser as others posted already:
So is it even possible to develop a mod for this? I've searched some time already and found some stuff that seems to indicate in the direction. The expressions I've found which seem to match:
:hover mouseover selectTabOnMouseDown triggerHandler click
I've tried to read the code of Tab Mix Plus for hours now as I thought it must be in there somewhere but did not really succeed.
This seems to deal with it:
tabs.js // 2011-01-26 if (Tabmix.prefs.prefHasUserValue("mouseDownSelect")) { Tabmix.prefs.setBoolPref("selectTabOnMouseDown", Tabmix.prefs.getBoolPref("mouseDownSelect")); Tabmix.prefs.clearUserPref("mouseDownSelect"); }
2011-01-26 change mouseDownSelect to selectTabOnMouseDown pref("extensions.tabmix.mouseDownSelect", false); */ pref("extensions.tabmix.selectTabOnMouseDown", true); pref("extensions.tabmix.mouseOverSelect", false); pref("extensions.tabmix.mouseOverSelectDelay", 250); pref("extensions.tabmix.tabFlip", false); pref("extensions.tabmix.tabFlipDelay", 250); pref("extensions.tabmix.lockTabSizingOnClose", true);
Here is another script I've found from another source:
<script> $('.custom-button1').on('mouseover', function (evt) { $('.target-tab-link1').triggerHandler('click'); evt.preventDefault(); });
Copied from a forum member:
/* mouse over tab */ a:hover {
Interesting: https://vdw.github.io/Tabslet/#tab-2
$('.tabs').tabslet({ mouseevent: 'hover', attribute: 'href', animation: false });
https://api.jqueryui.com/tabs/ :
event Type: String Default: "click" The type of event that the tabs should react to in order to activate the tab. To activate on hover, use "mouseover". Code examples: Initialize the tabs with the event option specified: 1 2 3 $( ".selector" ).tabs({ event: "mouseover" }); Get or set the event option, after initialization: 1 2 3 4 5 // Getter var event = $( ".selector" ).tabs( "option", "event" ); // Setter $( ".selector" ).tabs( "option", "event", "mouseover" );
--
ModEdit: Title
-
Can you explain to me the opening of this topic?
You have already submitted the same topic in this category 4 days ago.
Trigger a click by hovering mouse pointer over a tab [most wanted] / Vivaldi Browser / Customizations & Extensions / Modifications | Vivaldi Forum
Why don't you continue in the first one? -
That mod isn’t hard to do theoretically. But Vivaldi doesn’t allow triggering single clicks easily. On buttons we still seem to be able to do it with pointerup and ‐down events. Didn’t get this to work on tabs. What works is closing tab on hover ^^
Anyway, I'd try and be less reliant on the mouse, it’s the slowest way to get around.
-
Ok, got it to work. This was more painful than necessary, you owe me a beer lol
// Activate Tab On Hover // version 2024.9.0 // https://forum.vivaldi.net/post/395460 // Activates tab on hover. (function activateTab() { "use strict" function hover(e, tab) { if ( !tab.parentNode.classList.contains("active") && !e.shiftKey && !e.ctrlKey ) { tab.addEventListener("mouseleave", function () { clearTimeout(wait); tab.removeEventListener("mouseleave", tab); }); wait = setTimeout(function () { const id = Number(tab.parentNode.parentNode.id.replace(/^\D+/g, "")); chrome.tabs.update(id, { active: true, highlighted: true }); }, delay); } } let wait; const delay = 300; //pick a time in milliseconds let appendChild = Element.prototype.appendChild; Element.prototype.appendChild = function () { if ( arguments[0].tagName === "DIV" && arguments[0].classList.contains("tab-header") ) { setTimeout( function () { const trigger = (event) => hover(event, arguments[0]); arguments[0].addEventListener("mouseenter", trigger); }.bind(this, arguments[0]) ); } return appendChild.apply(this, arguments); }; })();
Updates:
2021/01/29 – Tabs don’t activate on selection (shift/ctrl)
2021/03/14 – Two‐level tab stacking fix
2021/08/02 – Simulating mouse events broke, reverting to original/simple version
2022.10.0 – Latest version from Github
2024.9.0 – Fix for Vivaldi version 6.10 and later -
@luetage
You're my heroe. Thank you so much for creating this feature. I owe you a gofundme.One problem: it's not yet working for me.
If I understand right this is a .js file and under hooks.
Does this interfere with the other hooks?I will disable the css files and other hooks to check this.
-
@maximwaldow It’s not a hook, it's a normal javascript mod you have to refer to in
browser.html
. See pinned topics. -
@luetage
that's what I did and still not working:
To clarify: mouse over another tab and wait to activate.Edited browser.html and inserted line and saved:
<script src="custom.js"></script><!DOCTYPE html> <html> <head> <!-- Keep the styling in sync with ./window.html --> <meta charset="UTF-8" /> <title>Vivaldi</title> <link rel="stylesheet" href="style/common.css" /> <link rel="stylesheet" href="chrome://vivaldi-data/css-mods/css" /> <style> body { background-color: #d2d2d2; background-image: url('resources/vivaldi-splash-icon.svg'); background-size: 16%; background-position: center; background-repeat: no-repeat; } @media (prefers-color-scheme: dark) { body { background-color: #2d2d2d; } } </style> </head> <body> <div id="app" /> <script src="background-common-bundle.js"></script> <script src="vendor-bundle.js"></script> <script src="settings-bundle.js"></script> <script src="urlbar-bundle.js"></script> <script src="components-bundle.js"></script> <script src="jdhooks.js"></script> <script src="bundle.js"></script> <script src="custom.js"></script> </body> </html>
Named your code custom.js and put in the folder vivaldi
{ function selectTab(tab) { tab.addEventListener('mouseleave', function () { clearTimeout(wait); }) wait = setTimeout(function () { const tid = tab.parentNode.id; const id = Number(tid.replace( /^\D+/g, '')); chrome.tabs.update(id,{active: true, highlighted: true}); }, delay) } var wait; const delay = 300; //pick a time in milliseconds var appendChild = Element.prototype.appendChild; Element.prototype.appendChild = function () { if (arguments[0].tagName === 'DIV') { setTimeout(function() { if (arguments[0].classList.contains('tab-header')) { arguments[0].addEventListener('mouseenter', selectTab.bind(arguments[0], arguments[0])); } }.bind(this, arguments[0])); } return appendChild.apply(this, arguments); } } ◊ github
-
@maximwaldow You copied my signature and put it into the code. Not sure whether this breaks it. Anyway, you can take a look yourself by inspecting UI and opening the console tab.
-
@luetage
Working! That did the trick. You made my day.luetage for president!
I've changed the title in the opening thread and mentioned your name as you are the owner of this mod. Is this ok? Otherwise I change the title.
Or I delete my thread totally and you can announce your invention in your own thread.I changed the delay from 300 to 350.
-
@luetage - interesting mod - but gives me problems as i have the tabbar under the address bar and when i go to the address bar it can activate a tab as i cross the tabbar
can solve that by increasing the delay but that sort of defeats the object of the mod
i have a number of locked tabs on the left and its those that i cross - is there any difference in the code between a locked tab and an open one - if so is it possible for the mod to not activate locked tabs
-
@maximwaldow I don't need a new topic, I track mods on github.
mod updated, simplified it a bit -
@adacom A delay of 300ms should be enough to cross tabs vertically, it’s the magic number
Anyway, if you think you don’t wanna trigger it on pinned tabs that’s easily doable.{ function activateTab(tab) { if (!tab.parentNode.classList.contains('active') && !tab.parentNode.classList.contains('pinned')) { tab.addEventListener('mouseleave', function () { clearTimeout(wait); tab.removeEventListener('mouseleave', tab); }) wait = setTimeout(function () { const id = Number(tab.parentNode.id.replace( /^\D+/g, '')); chrome.tabs.update(id, {active: true, highlighted: true}); }, delay) } } var wait; const delay = 300; //pick a time in milliseconds var appendChild = Element.prototype.appendChild; Element.prototype.appendChild = function () { if (arguments[0].tagName === 'DIV' && arguments[0].classList.contains('tab-header')) { setTimeout(function () { arguments[0].addEventListener('mouseenter', activateTab.bind(arguments[0], arguments[0])); }.bind(this, arguments[0])); } return appendChild.apply(this, arguments); } }
-
@luetage - excellent - thanks
-
@luetage
Updated thread with your new mod title from github.
Tried your code from github which did not work at first (delay value 300 was missing). After you seemed to have edited it the updated script works flawlessly.If you change your mind concerning this thread, just tell me. Until then I leave everything as it is already.
Now with all these mods Vivaldi feels a bit like the old Firefox (52.9 ESR with the .xpi addons) which is great.
Changed the delay to 250 like in obsolete Firefox. -
@luetage Thank you very much from me, too. It was also for me the last missing feature in Vivaldi that makes it as comfortable to use as Firefox 56 with TMP.
-
@luetage I am using your fantastic Mod Activate tab on hover, but is very difficult (almost imposible) to select tabs.
Could you add an option to select tabs by using ALT+click.
Thank you very much in advance.
-
@barbudo2005 Didn’t think about this at all and I don’t use the mod, but it’s a good point. Alt‐Click won’t help, the tab selection disappears as soon as a tab is going active. A workaround is to “Include Active Tab in Initial Selection” and then select the first tab in your wanted selection and from below the tab, with shift already pressed, hover fast above the end of your selection, click and leave. That’s no good for selecting a bunch of individual tabs though. Another way is increasing the delay, but a long delay is annoying during normal usage. I believe I know how to code a better workaround for the issue…
-
@luetage Thank you for your fast answer. I use Otto tabs for auto stack tabs by domain:
https://github.com/borsini/chrome-otto-tabs (the developer is not available)So I only need it when I do a search and open various links from it.
I change the delay to 1000 and that solve the problem. Thanks.I would like to ask you another favor with Otto tabs.
When I open two search and various links from both, Otto tabs make a stack with both search (parents) and the children stay apart. Not good.
Is there a simple form in Javascript to block the domain (host) of the search so not to make a stack with this host? With regex for example.
This are the 3 JS in the extension:
tabs_helpers.js
const isVivaldiTab = (object) => { return object && 'extData' in object; }; function uuidv4() { return ("" + 1e7 + -1e3 + -4e3 + -8e3 + -1e11) .replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4) .toString(16)); } const promiseSerial = (funcs) => funcs.reduce((promise, func) => promise.then(result => func().then(Array.prototype.concat.bind(result))), Promise.resolve([])); /* Chrome Tabs promise wrappers */ export const chromeTabsMovePromise = (id, index) => (new Promise(function (resolve, reject) { console.log("moving tab to index : ", index); chrome.tabs.move(id, { index }, () => resolve()); })); const chromeTabsUpdatePromise = (tabId, data) => (new Promise(function (resolve, reject) { console.log("updating tab", tabId, data); chrome.tabs.update(tabId, data, (t) => resolve()); })); const chromeTabsRemovePromise = (tabId) => (new Promise(function (resolve, reject) { console.log("removing tab", tabId); chrome.tabs.remove(tabId, () => { console.log("removed"); resolve(); }); })); const chromeTabsQueryPromise = (query) => (new Promise(function (resolve, reject) { console.log("querying tabs ", query); chrome.tabs.query(query, resolve); })); /********************/ export const applyRulesForTab = (tab, config) => { console.log("apply otto rules for tab", tab); promiseSerial([ () => removeDuplicates(tab, config, chromeTabsQueryPromise, chromeTabsRemovePromise), () => trimTabs(tab, config, chromeTabsQueryPromise, chromeTabsRemovePromise), () => moveSameUrlHost(tab, config, chromeTabsQueryPromise, regroupTabsPromise(chromeTabsMovePromise), moveVivaldi(chromeTabsUpdatePromise, uuidv4)), ]); }; export const regroupTabsPromise = (chromeTabsMovePromise) => (tabs) => { console.log("bla"); if (tabs.length < 2) { console.log("no need to move", tabs.length, "tabs"); return Promise.resolve(tabs); } console.log("move tabs...", tabs); const firstIndex = tabs[0].index; const movePromises = tabs .filter(t => t.id !== undefined) .map(((t, index) => () => chromeTabsMovePromise(t.id, firstIndex + index))); return promiseSerial(movePromises) .then(() => Promise.resolve(tabs)); }; const moveVivaldi = (updatePromise, getUUID) => (tabs) => { return groupVivaldiTabsPromise(tabs, updatePromise, uuidv4); }; const getQueryToGroup = (url, config) => { if (config.group.type === 'SUB_DOMAIN') { const hostname = url.hostname.startsWith("www.") ? url.hostname.substring(4) : url.hostname; const domains = hostname.split("."); return "*://*." + (domains.length > 2 ? domains.slice(1) : domains).join(".") + "/*"; } else { return "*://" + url.hostname + "/*"; } }; export const moveSameUrlHost = (tab, config, queryPromise, regroupTabsPromise, groupVivaldiTabsPromise) => (new Promise(function (resolve, reject) { if (!config.group.isActivated || !tab.url) { resolve(); return; } console.log("Group tabs..."); const hostQuery = getQueryToGroup(new URL(tab.url), config); queryPromise({ url: hostQuery, currentWindow: true, pinned: false }) .then(regroupTabsPromise) .then(tabs => { if (isVivaldiTab(tab)) { //Vivaldi stacking feature is supported return groupVivaldiTabsPromise(tabs); } else { return Promise.resolve(); } }) .then(() => resolve()); })); export const groupVivaldiTabsPromise = (tabs, updatePromise, getUUID) => { console.log("group vivaldi tabs..."); const tabsToExtData = tabs .filter(t => t.id !== undefined) .reduce((old, curr) => { var data = {}; try { data = JSON.parse(curr.extData); } catch (e) { } return Object.assign({}, old, { [curr.id]: data }); }, {}); var groupIdToUse = null; if (tabs.length > 1) { const existingGroupId = Object.values(tabsToExtData) .map(d => d.group) .find(g => g !== undefined); groupIdToUse = existingGroupId ? existingGroupId : getUUID(); console.log("group id to use", groupIdToUse); } else { console.log("only one tab, remove it's group id"); } const updatePromises = Object.keys(tabsToExtData).map(t => parseInt(t)).map((tabId => { const newExtData = tabsToExtData[tabId]; newExtData.group = groupIdToUse; return () => updatePromise(tabId, { extData: JSON.stringify(newExtData) }); })); return promiseSerial(updatePromises); }; export const removeDuplicates = (tab, config, queryPromise, removePromise) => (new Promise((resolve) => { if (!config.duplicates.isActivated) { resolve(); return; } console.log("Removing duplicates..."); queryPromise({ currentWindow: true, pinned: false }) .then(allTabs => { const tabs = allTabs.filter(t => t.id !== undefined && t.url == tab.url && t.id != tab.id); console.log(tabs.length, "identical tabs to tab id ", tab.id, " : ", tabs); if (tabs.length > 0) { console.log("remove tabs ", tabs); return promiseSerial(tabs.map(t => () => removePromise(t.id))); } else { return Promise.resolve(); } }) .then(resolve); })); export const trimTabs = (tab, config, queryPromise, removePromise) => (new Promise((resolve) => { if (!config.host.isActivated || !tab.url) { resolve(); return; } console.log("Trimming tabs..."); const url = new URL(tab.url); var hostQuery = url.origin + "/*"; queryPromise({ url: hostQuery, currentWindow: true, pinned: false }) .then(tabs => { console.log(tabs.length, "tabs with same host"); if (tabs.length > config.host.maxTabsAllowed) { const toRemove = tabs .filter(t => t.id !== undefined && t.id != tab.id) .slice(0, tabs.length - config.host.maxTabsAllowed + 1); console.log("trim tab ", toRemove); return promiseSerial(toRemove.map(t => () => removePromise(t.id))); } else { return Promise.resolve(); } }) .then(resolve); }));
popup.js
function $$(id) { return document.querySelector(id); } document.addEventListener('DOMContentLoaded', function () { var duplicates = $$("input[name=duplicates]"); var group = $$("input[name=group]"); var group_domain = $$("#group_domain"); var host = $$("input[name=host]"); const askForConfig = () => { chrome.runtime.sendMessage('', 'GET_CONFIG', function (config) { console.log("display configuration", config); duplicates.checked = config.duplicates.isActivated; group.checked = config.group.isActivated; group_domain.options[config.group.type === 'FULL_DOMAIN' ? 0 : 1].selected = true; host.checked = config.host.isActivated; }); }; chrome.runtime.onMessage.addListener((message, _, sendResponse) => { if (message == 'NEW_CONFIG') { console.log("new configuration to display"); askForConfig(); } }); askForConfig(); const onUIChanged = () => { const conf = { duplicates: { isActivated: duplicates.checked, }, group: { isActivated: group.checked, type: group_domain.selectedOptions[0].value === 'full_domain' ? 'FULL_DOMAIN' : 'SUB_DOMAIN' }, host: { isActivated: host.checked, maxTabsAllowed: 5, } }; chrome.runtime.sendMessage('', conf); }; group_domain.addEventListener('input', onUIChanged); document.querySelectorAll('input').forEach(s => s.addEventListener('change', onUIChanged)); });
backgroud.js
import { applyRulesForTab } from './tabs_helpers.js'; var config = { duplicates: { isActivated: true, }, group: { isActivated: true, type: "FULL_DOMAIN" }, host: { isActivated: true, maxTabsAllowed: 5, } }; chrome.runtime.onMessage.addListener(function (message, _, sendResponse) { if (message == 'GET_CONFIG') { console.log('get config received'); sendResponse(config); } else { console.log('new config', message); config = message; //Sync config to chrome storage chrome.storage.sync.set({ 'config': config }, function () { console.log('Config has been synced', config); }); } }); chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { if (changeInfo.url) { applyRulesForTab(tab, config); } }); chrome.storage.sync.get(['config'], function (result) { const conf = result.config; console.log('Retrieved config is', conf); if (conf !== undefined) { config = result.config; chrome.runtime.sendMessage('', 'NEW_CONFIG'); } });
Again thank you very much in advance.
-
@barbudo2005 1000ms is stupid. I updated my solution post with the new version. When you have the shift or the ctrl key pressed the function will not trigger anymore, that means you can select both in bulk and specific without having to think about it.
-
@luetage Thank you very much.