Activate Tab On Hover
-
@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.
-
@barbudo2005 And sorry, but I don’t wanna mess with a third party extension, maybe someone else takes a look at it. This is off‐topic anyway, so you might wanna create a separate topic for it in https://forum.vivaldi.net/category/51/extensions (not a modification).
-
@luetage Thank you anyway. I understand.
-
Update for tab stacks:
// Activate Tab On Hover // https://forum.vivaldi.net/topic/50354/create-a-new-mod-mouseover-tab-select/4 // Activates tab on hover. { function activateTab(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 () { if (tab.parentNode.parentNode.classList.contains('is-substack')) { const down = document.createEvent('MouseEvents'); down.initEvent('mousedown', true, true); tab.dispatchEvent(down); const up = document.createEvent('MouseEvents'); up.initEvent('mouseup',true,true); tab.dispatchEvent(up); } else { 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 () { var trigger = (event) => activateTab(event, arguments[0]); arguments[0].addEventListener('mouseenter', trigger); }.bind(this, arguments[0])); } return appendChild.apply(this, arguments); } }
Ugly but necessary. The parent tab of two level tab stacks doesn’t have a proper number id, but a string with mixed characters, which makes the chrome.tabs method fail. Simulating click still works for this. This was brought to my attention by N3C2L on github, don’t know whether they have an account here.
Anyway, I would have liked updating my original solution here, but admins have disabled editing posts again…
-
@luetage I was going to ask you to update the Mod for tab stacks, and realize you already did it. Bravo!!!!
-
@luetage
In snapshot 2352.3 / 2355.3,
Hover does not work on stacked tabs. -
Same problem. Not working on all three variants of stacked tabs. Perhaps a simulated click on the open and close arrow is the cure. I am afraid I am not capable of coding this.
Brian