Skip to content

Commit

Permalink
Add custom prefix matching for dedup (closes #24)
Browse files Browse the repository at this point in the history
The hotkey entries in the options now have an additional field, Match Prefix,
that is required only when Deduplicate is checked. Any tab with a URL starting
with the Match Prefix is considered a match to this hotkey for deduplication
purposes, but the Target (which can the same or more specific than Match
Prefix) is the URL that will be opened if no match already exists.

Important note: the Match Prefix has to be usable as a Chrome "URL match
pattern" when just a "*" is added to the end of it, so I added a rudimentary
regex validation on its input element. For some reason (I'm guessing related to
Foundation), the default popups aren't appearing when the user tries to save
with an invalid Match Prefix, but the page simply focuses the first box with an
invalid input, which is very unclear. Much clearer notification of what's
invalid and how to fix it should be a part of #20.

Also, this commit removes the legacy code for loading and converting the old
format of options, as planned. Options will not load correctly for any user who
has not first saved their options with the previous commit's code. (But they
can just check out the previous commit, open and save their options, then
checkout this commit again.)
  • Loading branch information
jchang504 committed Jul 1, 2017
1 parent af875f6 commit c191ff7
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 166 deletions.
197 changes: 56 additions & 141 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,6 @@ var current_tab_index = 0;
// changes (this does NOT fire the active tab change listener).
var window_to_active_tab_map = {};

// Regex for domain and path matching.
// Javascript Regex allows you to organize the regex matching into various
// capture groups that you can access through indices of the returned value from
// the exec function call.
// Capture group 0: The full URL of the string
// Capture group 1: The full URL of the string except the protocol (ex:
// "https://") and the" www.".
// Capture group 2: The domain, which includes everything after where the "www."
// would be (if it's not there) and before the first "/" after the "www."
const DOMAIN_PATH_REGEX =
/^(?:http:\/\/|https:\/\/)?(?:www\.)?(([a-z0-9]+(?:[\-\.][a-z0-9]+)*\.[a-z]+)(?::[0-9]{1,5})?(?:\/.*)?)$/;
//The indices of the captured components of calling exec on the returned values
//from DOMAIN_PATH_REGEX
const DOMAIN_PATH_INDEX = 1;
const DOMAIN_INDEX = 2;


// Navigate to (make active) the specified tab (and focus its window, if the
// optional argument is provided).
function navigateToTab(tab_id, window_id) {
Expand All @@ -42,72 +25,12 @@ function navigateToTab(tab_id, window_id) {
// Create a new tab of the url (and navigate to it). Also used for tab search.
function createNewTab(url) {
chrome.tabs.query({[CURRENT_WINDOW]: true, [ACTIVE]: true},
function(tabs) {
function (tabs) {
LOG_INFO("Create new tab of: " + url);
chrome.tabs.create({[URL]: url});
}
);
}
// Returns whether the host and path of the two given URL's are the same
// If either of them are null, then return false.
function domainPathMatch(url1, url2){
if(url1 && url2){
var result1 = DOMAIN_PATH_REGEX.exec(url1);
var result2 = DOMAIN_PATH_REGEX.exec(url2);
if(result1 && result2){
var host_path1 = result1[DOMAIN_PATH_INDEX];
var host_path2 = result2[DOMAIN_PATH_INDEX];
return host_path1 === host_path2;
} else {
return false;
}
} else {
return false;
}
}

// Returns whether the domains of the two given URL's are the same.
// If either of them are null, then return false.
function domainMatch(url1, url2){
if(url1 && url2){
LOG_INFO("Comparing urls: " + url1 + " and " + url2);
var result1 = DOMAIN_PATH_REGEX.exec(url1);
var result2 = DOMAIN_PATH_REGEX.exec(url2);
if(result1 && result2){
var domain1 = result1[DOMAIN_INDEX];
var domain2 = result2[DOMAIN_INDEX];
LOG_INFO("with domains: " + domain1 + " and " + domain2);
return domain1 === domain2;
} else {
return false;
}
} else {
return false;
}
}

/*
* url: the string that includes the protocol and domain name of the site you
* want to go to (e.g. "https://www.google.com").
* deduplicate: true if you want to check for a duplicate and go to it if it
* exists instead of creating a new tab; false if you want to create a new tab
* regardless of pre-existing tabs.
*/
function openTab(url, deduplicate){
chrome.tabs.query({}, function(tabs){
if (deduplicate) {
for (const tab of tabs){
var tab_url = tab.url;
if (domainMatch(url, tab_url)) {
LOG_INFO("Switch active tab to: " + tab_url);
navigateToTab(tab.id, tab.windowId);
return;
}
}
}
createNewTab(url);
});
}

/* direction: -1 for left, 1 for right
* move: true for moving tab left/right, false for navigating left/right
Expand Down Expand Up @@ -162,10 +85,11 @@ function loadHotkeys() {
LOG_INFO("Load hotkeys");
hotkeys_map = {};
chrome.storage.sync.get({[HOTKEYS_KEY]: HOTKEYS_DEFAULT}, function(items) {
for (const hotkey_info of items[HOTKEYS_KEY]){
for (const hotkey_info of items[HOTKEYS_KEY]) {
hotkeys_map[hotkey_info[HOTKEY_KEY]] = {
[TARGET_KEY]: hotkey_info[TARGET_KEY],
[DEDUPLICATE_KEY]: hotkey_info[DEDUPLICATE_KEY]
[DEDUPLICATE_KEY]: hotkey_info[DEDUPLICATE_KEY],
[MATCH_PREFIX_KEY]: hotkey_info[MATCH_PREFIX_KEY]
};
}
});
Expand All @@ -177,8 +101,8 @@ function openTabSearch() {
createNewTab(SEARCH_URL);
}

// Navigate to the previous tab that was navigated to with KeepTabs. Useful for
// quick alt+tab style switching between two tabs.
// Navigate to the most recently active still-existing tab before the current
// tab. Useful for quick alt+tab style switching between two tabs.
function navigateToPreviousTab() {
LOG_INFO("Navigate to previous tab");
var current_tab_id = tab_history[current_tab_index];
Expand Down Expand Up @@ -233,59 +157,61 @@ chrome.tabs.onActivated.addListener(function (activeInfo) {
window_to_active_tab_map[activeInfo.windowId] = activeInfo.tabId;
});

function cycleTabs(domain, active_tab_id){
LOG_INFO("Cycling tabs with domain: " + domain + "from tab id: " +
active_tab_id);
chrome.tabs.query({},
function(tabs) {
var tab_index = 0;
while(tabs[tab_index].id != active_tab_id){
tab_index++;
}
LOG_INFO("Current tab index: " + tab_index);
LOG_INFO("Number of tabs: " + tabs.length);

let increment = 0;
let curr_tab;
let curr_index;
//I included a check on the increment counter just to make sure we
//don't end up in an infinite loop if something happens to the
//active tab (like it's closed or redirected) right before this
//function is called.
do {
increment++;
curr_index = (tab_index + increment) % tabs.length;
curr_tab = tabs[curr_index];
} while((! domainMatch(curr_tab.url, domain)) &&
increment < tabs.length);
LOG_INFO("New index: " + curr_index);
function openNewTab() {
chrome.tabs.create({});
}

navigateToTab(curr_tab.id, curr_tab.windowId);
// Handles any custom hotkey received (including invalid ones).
function handleCustomHotkey(hotkey) {
var normalized = hotkey.toLowerCase();
// If hotkey is valid...
if (hotkeys_map.hasOwnProperty(normalized)) {
LOG_INFO("Activate custom hotkey: " + normalized);
var hotkey_info = hotkeys_map[normalized];
var target = hotkey_info[TARGET_KEY];
// Only deduplicate if checked in options and not overridden by capital
// letter in hotkey.
var deduplicate = hotkey_info[DEDUPLICATE_KEY] && hotkey == normalized;
if (!deduplicate) {
LOG_INFO("Not deduplicating; create new tab of target: " + target);
createNewTab(target);
}
);

else {
var match_prefix = hotkey_info[MATCH_PREFIX_KEY];
switchToMatchIfExists(target, match_prefix);
}
}
}

function handleTabSwitch(hotkey_info, overrideDeduplicate){
var domain = hotkey_info[TARGET_KEY];
chrome.tabs.query({[CURRENT_WINDOW]: true, [ACTIVE]: true},
function(tabs) {
var active_tab = tabs[0];
var url = active_tab.url;
var needToCycle = domainMatch(domain, url);
if(needToCycle && !overrideDeduplicate){
cycleTabs(domain, active_tab.id);

} else {
openTab(domain, hotkey_info[DEDUPLICATE_KEY] &&
!overrideDeduplicate);
// Queries for tabs with URLs starting with the match_prefix and navigates to
// the first one, unless the current tab is already a match -- then it cycles
// to the next matching tab after the current one. If no matches exist, opens a
// new tab of target.
function switchToMatchIfExists(target, match_prefix) {
LOG_INFO("Query for tabs matching prefix: " + match_prefix);
chrome.tabs.query({[URL]: match_prefix + "*"}, function(matching_tabs) {
if (matching_tabs.length > 0) {
var current_tab_matches = false;
for (var i = 0; i < matching_tabs.length; i++) {
if (matching_tabs[i].id == tab_history[current_tab_index]) {
LOG_INFO("Current tab matches; cycle to next");
current_tab_matches = true;
var next_tab =
matching_tabs[(i + 1) % matching_tabs.length];
navigateToTab(next_tab.id, next_tab.windowId);
break;
}
}
if (!current_tab_matches) {
LOG_INFO("Navigate to first match");
navigateToTab(matching_tabs[0].id, matching_tabs[0].windowId);
}
}
);
}

function openNewTab(){
chrome.tabs.create({});
else {
LOG_INFO("No matches; create new tab of target: " + target);
createNewTab(target);
}
});
}

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
Expand Down Expand Up @@ -329,18 +255,7 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
openNewTab();
}
else {
var normalized = hotkey.toLowerCase();
var overrideDeduplicate = hotkey != normalized;

if (normalized in hotkeys_map) {
var hotkey_info = hotkeys_map[normalized];
var domain = hotkey_info[TARGET_KEY];

if (domain){
LOG_INFO("Handle tab switch for domain: " + domain);
handleTabSwitch(hotkey_info, overrideDeduplicate);
}
}
handleCustomHotkey(hotkey);
}
}
// Refresh options after edit.
Expand Down
8 changes: 8 additions & 0 deletions options.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,11 @@ body {
#hold_key_description {
margin-bottom: 10px;
}

input[name="target"], input[name="match_prefix"] {
width: 300px;
}

input[name="hotkey"] {
width: 50px;
}
6 changes: 6 additions & 0 deletions options.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ <h2>Customize hotkeys</h2>
through all matches). If not, or if there are no existing matching
tabs, KeepTabs will open a new tab of the <strong>Target</strong>.
</p>
<p><strong>Syntax note</strong>: If you want to match to just a bare
host with no specific path (e.g. "https://facebook.com"), your
<strong>Match Prefix</strong> <em>must</em> still have a "/" after the
host (e.g. "https://facebook.com<strong>/</strong>"). You will not be
able to save if any of your match prefixes doesn't fit this
requirement.</p>
<table id="hotkey_entry"><tbody>
<tr>
<th>Hotkey</th>
Expand Down
45 changes: 21 additions & 24 deletions options.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
// CSS selectors.
var HOLD_KEY_SELECTOR ='#hold_key';
var HOTKEY_ENTRYS_TABLE_SELECTOR ='#hotkey_entry > tbody';
var HOTKEY_ENTRY_ROWS_SELECTOR ='#hotkey_entry tr:not(:first-child)';
var HOTKEY_ENTRY_LAST_ROW_SELECTOR ='#hotkey_entry tr:last-child';
var HOTKEY_ENTRY_DELETE_SELECTOR ='button.delete';
var INPUTTABLE_ELEMENT_SELECTOR ='input,select';
var INPUT_TEXT_SELECTOR ='input[type="text"]';
var INPUT_TARGET_SELECTOR ='input[name="target"]';
var INPUT_HOTKEY_SELECTOR ='input[name="hotkey"]';
var INPUT_DEDUPLICATE_SELECTOR ='input[name="deduplicate"]';
var INPUT_MATCH_PREFIX_SELECTOR ='input[name="match_prefix"]';
var OPTIONS_FORM_SELECTOR ='#options';
var ADD_HOTKEY_ENTRY_BUTTON_SELECTOR ='#add_hotkey';
var SAVE_BUTTON_SELECTOR ='#save';
var CHECKED ='checked';
var DISABLED ='disabled';
var HOLD_KEY_SELECTOR = '#hold_key';
var HOTKEY_ENTRYS_TABLE_SELECTOR = '#hotkey_entry > tbody';
var HOTKEY_ENTRY_ROWS_SELECTOR = '#hotkey_entry tr:not(:first-child)';
var HOTKEY_ENTRY_LAST_ROW_SELECTOR = '#hotkey_entry tr:last-child';
var HOTKEY_ENTRY_DELETE_SELECTOR = 'button.delete';
var INPUTTABLE_ELEMENT_SELECTOR = 'input,select';
var INPUT_TEXT_SELECTOR = 'input[type="text"]';
var INPUT_HOTKEY_SELECTOR = 'input[name="hotkey"]';
var INPUT_TARGET_SELECTOR = 'input[name="target"]';
var INPUT_DEDUPLICATE_SELECTOR = 'input[name="deduplicate"]';
var INPUT_MATCH_PREFIX_SELECTOR = 'input[name="match_prefix"]';
var OPTIONS_FORM_SELECTOR = '#options';
var ADD_HOTKEY_ENTRY_BUTTON_SELECTOR = '#add_hotkey';
var SAVE_BUTTON_SELECTOR = '#save';
var CHECKED = 'checked';
var DISABLED = 'disabled';

// TODO: Make match_prefix input required when we start actually using this
// field.
// The pattern for match_prefix is a basic check that the prefix can be made a
// valid URL match pattern by adding "*" on the end. See
// https://developer.chrome.com/extensions/match_patterns.
var HOTKEY_ENTRY_HTML = ' \
<tr> \
<td><input required type="text" maxlength="5" name="hotkey"></td> \
<td><input required type="text" name="target"></td> \
<td><input type="checkbox" name="deduplicate"></td> \
<td><input type="text" name="match_prefix"></td> \
<td><input required type="text" name="match_prefix" \
pattern=".+://.+/.*"></td> \
<td><button class="delete">Delete</button></td> \
</tr> \
';
Expand Down Expand Up @@ -80,11 +82,6 @@ function restoreHotkeyEntrys(hotkeys) {
for (var i = 0; i < hotkeys.length; i++) {
addHotkeyEntry();
var jqHotkeyEntryRow = $(HOTKEY_ENTRY_LAST_ROW_SELECTOR);
// TODO: Remove after options transition.
if (hotkeys[i].hasOwnProperty(DOMAIN_KEY)) {
hotkeys[i][TARGET_KEY] = hotkeys[i][DOMAIN_KEY];
}
// TODO: End Remove section.
jqHotkeyEntryRow.find(INPUT_HOTKEY_SELECTOR).val(
hotkeys[i][HOTKEY_KEY]);
jqHotkeyEntryRow.find(INPUT_TARGET_SELECTOR).val(
Expand Down
1 change: 0 additions & 1 deletion shared_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ var HOTKEYS_KEY = "hotkeys";
var HOTKEYS_DEFAULT = [];
var HOTKEY_KEY = "hotkey";
var TARGET_KEY = "target";
var DOMAIN_KEY = "domain"; // TODO: Remove after transition to new options.
var DEDUPLICATE_KEY = "deduplicate";
var MATCH_PREFIX_KEY = "match_prefix";

Expand Down

0 comments on commit c191ff7

Please sign in to comment.