├── .gitignore ├── LICENSE ├── README.md ├── destroy.psd ├── destroy128.png ├── destroy19.png ├── destroy38.png ├── destroy48.png ├── destroyer.js ├── manifest.json ├── popup.html ├── popup.jhtml ├── popup.js ├── raindrop.min.js ├── templates.js └── xtab.sketch /.gitignore: -------------------------------------------------------------------------------- 1 | xtab.zip 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Craig Campbell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **PLEASE NOTE** Version 1.0.0 and later of this extension will no longer be open source and will be developed in private. The version 0.3.0 code here will remain licensed under the MIT license, and this repo will still be used to track issues and feature suggestions. 2 | 3 | # xTab 4 | 5 | 6 | 7 | Chrome extension for limiting the total number of tabs you can have open at the same time. 8 | 9 | ## Install it here: 10 | 11 | https://chrome.google.com/webstore/detail/xtab/amddgdnlkmohapieeekfknakgdnpbleb 12 | 13 | ## Enterprise use 14 | 15 | For enterprise users who are looking to lock specific settings using a group policy for a large installation please see https://xtab.app for more information. 16 | 17 | ### What’s new in version 1.0.0 18 | 19 | - Rearchitected to support additional functionality and custom builds. 20 | - Updated the pop up UI to make it simpler to understand what the different options do 21 | - Added an option to prevent tabs that are playing audio from being closed by the extension (enabled by default) 22 | - Added an option to recycle existing tabs. This means that instead of closing an old tab it will reuse the old tab and move it into the new position. This allows you to use the back button to get back to a tab that has been closed (disabled by default) 23 | - Added a page that shows up in place of a new tab if you have the extension set to block new tabs from opening (enabled by default) 24 | - Added an option to block tabs from opening without showing a page (disabled by default) 25 | - Added new xTab logo to popup menu 26 | - Updated extension icon 27 | - Updated extension logo 28 | - Improved handling of tabs you have never been to 29 | - Decreased amount of time until a tab is considered active to prevent cases where sometimes a tab doesn’t get closed even though you have been to it 30 | - Reduced number of options in tab limit dropdown 31 | -------------------------------------------------------------------------------- /destroy.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/xtab/a6b32866f4a43cb3fd06e13b5d6331d6df583dba/destroy.psd -------------------------------------------------------------------------------- /destroy128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/xtab/a6b32866f4a43cb3fd06e13b5d6331d6df583dba/destroy128.png -------------------------------------------------------------------------------- /destroy19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/xtab/a6b32866f4a43cb3fd06e13b5d6331d6df583dba/destroy19.png -------------------------------------------------------------------------------- /destroy38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/xtab/a6b32866f4a43cb3fd06e13b5d6331d6df583dba/destroy38.png -------------------------------------------------------------------------------- /destroy48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/xtab/a6b32866f4a43cb3fd06e13b5d6331d6df583dba/destroy48.png -------------------------------------------------------------------------------- /destroyer.js: -------------------------------------------------------------------------------- 1 | /* globals $, chrome */ 2 | var usedOn = {}; 3 | var openedOn = {}; 4 | var accessed = {}; 5 | var activeTabId; 6 | var timeout; 7 | var activeInterval = 2500; 8 | 9 | function _debug() { 10 | // console.log.apply(console, arguments); 11 | } 12 | 13 | function _getMax() { 14 | return parseInt(localStorage.max || 20); 15 | } 16 | 17 | function _getAlgo() { 18 | return localStorage.algo || 'used'; 19 | } 20 | 21 | function _markActive(tabId) { 22 | _debug('marked active', tabId); 23 | usedOn[tabId] = new Date().getTime(); 24 | accessed[tabId] += 1; 25 | } 26 | 27 | function _handleTabActivated(data) { 28 | var tabId = data.tabId; 29 | activeTabId = tabId; 30 | _debug('activated', tabId); 31 | 32 | clearTimeout(timeout); 33 | 34 | // after 3 seconds mark this tab as active 35 | // this is so if you are quickly switching tabs 36 | // they are not considered active 37 | timeout = setTimeout(function() { 38 | _markActive(tabId); 39 | }, activeInterval); 40 | } 41 | 42 | function _handleTabRemoved(tabId) { 43 | clearTimeout(timeout); 44 | 45 | _debug('removed', tabId); 46 | delete usedOn[tabId]; 47 | delete openedOn[tabId]; 48 | delete accessed[tabId]; 49 | } 50 | 51 | function _handleTabReplaced(newTabId, oldTabId) { 52 | if (usedOn[oldTabId]) { 53 | usedOn[newTabId] = usedOn[oldTabId]; 54 | } 55 | 56 | if (openedOn[oldTabId]) { 57 | openedOn[newTabId] = openedOn[oldTabId]; 58 | } 59 | 60 | if (accessed[oldTabId]) { 61 | accessed[newTabId] = accessed[oldTabId]; 62 | } 63 | 64 | delete usedOn[oldTabId]; 65 | delete openedOn[oldTabId]; 66 | delete accessed[oldTabId]; 67 | } 68 | 69 | function _removeTab(tabId) { 70 | _debug('_removeTab', tabId); 71 | if (tabId) { 72 | chrome.tabs.remove(tabId, function() {}); 73 | // _handleTabRemoved(tabId); 74 | } 75 | } 76 | 77 | function _getLowestIn(data, tabs) { 78 | var lowest; 79 | var lowestIndex; 80 | var tabId; 81 | var value; 82 | for (var i = 0; i < tabs.length; i++) { 83 | tabId = tabs[i].id; 84 | 85 | // never close the currently active tab 86 | if (tabId === activeTabId) { 87 | continue; 88 | } 89 | 90 | // if you have never been to this tab then skip it 91 | if (!usedOn.hasOwnProperty(tabId) || !data.hasOwnProperty(tabId)) { 92 | continue; 93 | } 94 | 95 | value = data[tabId] || 0; 96 | 97 | if (lowest === undefined) { 98 | lowest = value; 99 | } 100 | 101 | if (value <= lowest) { 102 | lowestIndex = i; 103 | lowest = value; 104 | } 105 | } 106 | 107 | return lowestIndex; 108 | } 109 | 110 | function _removeLeastAccessed(tabs) { 111 | var removeTabIndex = _getLowestIn(accessed, tabs); 112 | if (removeTabIndex >= 0) { 113 | _removeTab(tabs[removeTabIndex].id); 114 | tabs.splice(removeTabIndex, 1); 115 | } 116 | return tabs; 117 | } 118 | 119 | function _removeOldest(tabs) { 120 | var removeTabIndex = _getLowestIn(openedOn, tabs); 121 | if (removeTabIndex >= 0) { 122 | _removeTab(tabs[removeTabIndex].id); 123 | tabs.splice(removeTabIndex, 1); 124 | } 125 | return tabs; 126 | } 127 | 128 | function _removeLeastRecentlyUsed(tabs) { 129 | var removeTabIndex = _getLowestIn(usedOn, tabs); 130 | if (removeTabIndex >= 0) { 131 | _removeTab(tabs[removeTabIndex].id); 132 | tabs.splice(removeTabIndex, 1); 133 | } 134 | return tabs; 135 | } 136 | 137 | function _removeTabs(tabs) { 138 | var length = tabs.length; 139 | _debug('there are', tabs.length, 'tabs open'); 140 | _debug('max is', _getMax()); 141 | while (length >= _getMax()) { 142 | _debug('removing a tab with length', length); 143 | switch (_getAlgo()) { 144 | case 'oldest': 145 | tabs = _removeOldest(tabs); 146 | break; 147 | case 'accessed': 148 | tabs = _removeLeastAccessed(tabs); 149 | break; 150 | default: 151 | tabs = _removeLeastRecentlyUsed(tabs); 152 | break; 153 | } 154 | length -= 1; 155 | } 156 | } 157 | 158 | function _handleTabAdded(data) { 159 | var tabId = data.id || data; 160 | 161 | _debug('added', tabId); 162 | 163 | // find tab to remove 164 | chrome.tabs.query({currentWindow: true}, function(tabs) { 165 | tabs = tabs.filter(function(tab) { 166 | return !tab.pinned && tab.id != tabId; 167 | }); 168 | 169 | _debug('Total tabs', tabs.length); 170 | _debug('Max tabs', _getMax()); 171 | 172 | if (tabs.length >= _getMax()) { 173 | 174 | // If this is set to block just immediately remove this tab before 175 | // even adding info about it 176 | if (_getAlgo() === 'block') { 177 | _removeTab(tabId); 178 | return; 179 | } 180 | 181 | _removeTabs(tabs); 182 | } 183 | 184 | openedOn[tabId] = new Date().getTime(); 185 | accessed[tabId] = 0; 186 | }); 187 | } 188 | 189 | function _bindEvents() { 190 | chrome.tabs.onActivated.addListener(_handleTabActivated); 191 | chrome.tabs.onCreated.addListener(_handleTabAdded); 192 | chrome.tabs.onAttached.addListener(_handleTabAdded); 193 | chrome.tabs.onRemoved.addListener(_handleTabRemoved); 194 | chrome.tabs.onDetached.addListener(_handleTabRemoved); 195 | chrome.tabs.onReplaced.addListener(_handleTabReplaced); 196 | } 197 | 198 | function _init() { 199 | 200 | // on startup loop through all existing tabs and set them to active 201 | // this is only needed so that if you first install the extension 202 | // or bring a bunch of tabs in on startup it will work 203 | // 204 | // setting the time to their tab id ensures they will be closed in 205 | // the order they were opened in and there is no way to figure 206 | // out what time a tab was opened from chrome apis 207 | chrome.tabs.query({}, function(tabs) { 208 | for (var i = 0; i < tabs.length; i++) { 209 | if (!usedOn.hasOwnProperty(tabs[i].id)) { 210 | openedOn[tabs[i].id] = tabs[i].id; 211 | usedOn[tabs[i].id] = tabs[i].id; 212 | accessed[tabs[i].id] = 0; 213 | } 214 | } 215 | 216 | _bindEvents(); 217 | }); 218 | } 219 | 220 | $.ready(_init); 221 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "xTab", 4 | "author": "Craig Campbell", 5 | "version": "0.3.0", 6 | "description": "Limit maximum number of open tabs", 7 | "icons": { 8 | "16": "destroy19.png", 9 | "48": "destroy48.png", 10 | "128": "destroy128.png" 11 | }, 12 | "permissions": ["tabs"], 13 | "browser_action": { 14 | "default_title": "xTab", 15 | "default_icon": "destroy38.png", 16 | "default_popup": "popup.html" 17 | }, 18 | "background": { 19 | "scripts": ["raindrop.min.js", "destroyer.js"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /popup.jhtml: -------------------------------------------------------------------------------- 1 |

xTab

2 | 3 |
4 |

Maximum Tabs:

5 | 17 | 18 | 19 | 20 |

Close existing tab:

21 | 22 | 23 | 24 |

Block new tab:

25 | 26 |
27 | 28 | 31 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | /* global $, Aftershave */ 2 | function _handleChange() { 3 | var input = $('#input-max'); 4 | if (this.value === 'other') { 5 | input.show(); 6 | input.trigger('focus'); 7 | return; 8 | } 9 | 10 | localStorage.max = this.value; 11 | input.hide(); 12 | } 13 | 14 | function _saveMax() { 15 | var input = $('#input-max'); 16 | localStorage.max = input.val(); 17 | } 18 | 19 | function _saveAlgo() { 20 | localStorage.algo = this.value; 21 | } 22 | 23 | function _run() { 24 | var options = { 25 | 10: 10, 26 | 15: 15, 27 | 20: 20, 28 | 25: 25, 29 | 30: 30, 30 | 35: 35, 31 | 40: 40, 32 | 45: 45, 33 | 50: 50 34 | }; 35 | 36 | var algo = localStorage.algo || 'used'; 37 | var max = parseInt(localStorage.max || 20); 38 | $('body').html(Aftershave.render('popup', {options: options, algo: algo, max: max})); 39 | } 40 | 41 | $.ready(function() { 42 | $(document).on('change', 'select', _handleChange); 43 | $(document).on('change', '#input-max', _saveMax); 44 | $(document).on('change', 'input[type=radio]', _saveAlgo); 45 | _run(); 46 | }); 47 | -------------------------------------------------------------------------------- /raindrop.min.js: -------------------------------------------------------------------------------- 1 | /* raindrop v0.2.4 */'use strict';(function(){function a(e,b){if(!(this instanceof a)){if("string"===typeof e){if(!document.querySelectorAll)return new a;var g=(b?b[0]:document).querySelectorAll(e);if(1!==g.length)return new a(g);e=g[0]}return e&&e.__rid?d[e.__rid]:new a(e)}if(!e)return this.length=0,this;c++;if(e.nodeType||e===window)e.__rid=c,e={"0":e,length:1};this.id=c;f(this,e);this.length=e.length;d[c]=this}function f(e,a){for(var d in a)a.hasOwnProperty(d)&&(e[d]=a[d]);return e}var c=0,b,d={};a.ready=function(e){var a; 2 | document.addEventListener("DOMContentLoaded",a=function(){document.removeEventListener("DOMContentLoaded",a,!1);e()},!1)};a.extend=f;a.matches=function(e,a){var d;b||(e.matches&&(b=e.matches),e.webkitMatchesSelector&&(b=e.webkitMatchesSelector),e.mozMatchesSelector&&(b=e.mozMatchesSelector),e.msMatchesSelector&&(b=e.msMatchesSelector),e.oMatchesSelector&&(b=e.oMatchesSelector),b||(b=function(){}));d=b;return d.call(e,a)};a.prototype.each=function(e){for(var a=0;a ";return l+='