├── .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 |