Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions background_scripts/exclusions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// This module manages manages the exclusion rule setting. An exclusion is an object with two
// attributes: pattern and passKeys. The exclusion rules are an array of such objects.
// This module manages manages the exclusion rule setting. An exclusion is an object with three
// attributes: pattern, passKeys and blockedKeys. The exclusion rules are an array of such objects.

const ExclusionRegexpCache = {
cache: {},
Expand Down Expand Up @@ -46,10 +46,16 @@ function getRule(url, rules) {
}
// Strip whitespace from all matching passKeys strings, and join them together.
const passKeys = matchingRules.map((r) => r.passKeys.split(/\s+/).join("")).join("");
const blockedKeys = matchingRules.map((r) => (r.blockedKeys ?? "").split(/\s+/).join("")).join(
"",
);
// TODO(philc): Remove this commented out code.
// passKeys = (rule.passKeys.split(/\s+/).join "" for rule in matchingRules).join ""
if (matchingRules.length > 0) {
return { passKeys: Utils.distinctCharacters(passKeys) };
return {
passKeys: Utils.distinctCharacters(passKeys),
blockedKeys: Utils.distinctCharacters(blockedKeys),
};
} else {
return null;
}
Expand All @@ -60,6 +66,7 @@ export function isEnabledForUrl(url) {
return {
isEnabledForUrl: !rule || (rule.passKeys.length > 0),
passKeys: rule ? rule.passKeys : "",
blockedKeys: rule ? rule.blockedKeys ?? "" : "",
};
}

Expand Down
13 changes: 13 additions & 0 deletions content_scripts/mode_key_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class KeyHandlerMode extends Mode {
this.passKeys = passKeys;
this.reset();
}
setBlockedKeys(blockedKeys) {
this.blockedKeys = blockedKeys;
this.reset();
}

// Only for tests.
setCommandHandler(commandHandler) {
Expand Down Expand Up @@ -71,6 +75,8 @@ class KeyHandlerMode extends Mode {
// preview popups. If the user types escape, issue a mouseout event here. See #3073.
HintCoordinator.mouseOutOfLastClickedElement();
return this.continueBubbling;
} else if (this.isBlockedKey(keyChar)) {
return this.suppressEvent;
} else if (this.isMappedKey(keyChar)) {
this.handleKeyChar(keyChar);
return this.suppressEvent;
Expand All @@ -92,6 +98,13 @@ class KeyHandlerMode extends Mode {
!this.isPassKey(keyChar);
}

isBlockedKey(keyChar) {
return this.isInResetState() &&
keyChar &&
this.blockedKeys &&
this.blockedKeys.includes(keyChar);
}

// This tests whether keyChar is a digit (and accounts for pass keys).
isCountKey(keyChar) {
return keyChar &&
Expand Down
1 change: 1 addition & 0 deletions content_scripts/vimium_frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ async function checkIfEnabledForUrl() {

if (normalMode == null) installModes();
normalMode.setPassKeys(response.passKeys);
normalMode.setBlockedKeys(response.blockedKeys);
// Hide the HUD if we're not enabled.
if (!isEnabledForUrl) HUD.hide(true, false);
}
Expand Down
52 changes: 37 additions & 15 deletions lib/keyboard_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,36 @@ const KeyboardUtils = {
}
},

getKeyFromCode(event) {
if (!event.code) {
return event.key != null ? event.key : ""; // Fall back to event.key (see #3099).
} else if (event.code.slice(0, 6) === "Numpad") {
// We cannot correctly emulate the numpad, so fall back to event.key; see #2626.
return event.key;
}

// The logic here is from the vim-like-key-notation project
// (https://github.com/lydell/vim-like-key-notation).
let key = event.code;
if (key.slice(0, 3) === "Key") key = key.slice(3);
// Translate some special keys to event.key-like strings and handle <Shift>.
if (this.enUsTranslations[key]) {
key = event.shiftKey ? this.enUsTranslations[key][1] : this.enUsTranslations[key][0];
} else if ((key.length === 1) && !event.shiftKey) {
key = key.toLowerCase();
}
return key;
},

shouldUseEventCodeFallback(event, key) {
if (event.code == null) return false;
if (event.isComposing || (event.keyCode === 229)) return true;
return (key != null) &&
(key.length === 1) &&
/^\p{Letter}$/u.test(key) &&
!/^\p{Script=Latin}$/u.test(key);
},

getKeyChar(event) {
let key;
const canUseEventKey = !Settings.get("ignoreKeyboardLayout") &&
Expand All @@ -41,22 +71,14 @@ const KeyboardUtils = {

if (canUseEventKey) {
key = event.key;
} else if (!event.code) {
key = event.key != null ? event.key : ""; // Fall back to event.key (see #3099).
} else if (event.code.slice(0, 6) === "Numpad") {
// We cannot correctly emulate the numpad, so fall back to event.key; see #2626.
key = event.key;
} else {
// The logic here is from the vim-like-key-notation project
// (https://github.com/lydell/vim-like-key-notation).
key = event.code;
if (key.slice(0, 3) === "Key") key = key.slice(3);
// Translate some special keys to event.key-like strings and handle <Shift>.
if (this.enUsTranslations[key]) {
key = event.shiftKey ? this.enUsTranslations[key][1] : this.enUsTranslations[key][0];
} else if ((key.length === 1) && !event.shiftKey) {
key = key.toLowerCase();
// IMEs can emit non-Latin letters (for example Korean Hangul) even while normal mode expects
// physical-key mappings. Use the existing event.code translation in those cases, but keep
// non-English Latin layouts like "é" working via event.key.
if (this.shouldUseEventCodeFallback(event, key)) {
key = this.getKeyFromCode(event);
}
} else {
key = this.getKeyFromCode(event);
}

// It appears that key is not always defined (see #2453).
Expand Down
1 change: 1 addition & 0 deletions lib/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ div > .vimiumHintMarker > .matchingCharacter {
exclusionRules: [
// Disable Vimium on Gmail.
{
blockedKeys: "",
passKeys: "",
pattern: "https?://mail.google.com/*",
},
Expand Down
4 changes: 4 additions & 0 deletions pages/action.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ <h1>
<span class="exclusion-header-text">Patterns matching the current page</span>
</td>
<td><span class="exclusion-header-text">Keys to exclude</span></td>
<td><span class="exclusion-header-text">Keys to block</span></td>
</tr>
</thead>
</table>
Expand Down Expand Up @@ -92,6 +93,9 @@ <h1>
<td>
<input type="text" name="passKeys" spellcheck="false" placeholder="All" />
</td>
<td>
<input type="text" name="blockedKeys" spellcheck="false" placeholder="None" />
</td>
<td>
<input type="button" class="remove" value="&#x2716;" />
</td>
Expand Down
15 changes: 10 additions & 5 deletions pages/exclusion_rules_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ const ExclusionRulesEditor = {
});
},

// - exclusionRules: the value obtained from settings, with the shape [{pattern, passKeys}].
// - exclusionRules: the value obtained from settings, with the shape
// [{pattern, passKeys, blockedKeys}].
setForm(exclusionRules = []) {
const rulesTable = document.querySelector("#exclusion-rules");
// Remove any previous rows.
Expand All @@ -20,14 +21,13 @@ const ExclusionRulesEditor = {
el.remove();
}

const rowTemplate = document.querySelector("#exclusion-rule-template").content;
for (const rule of exclusionRules) {
this.addRow(rule.pattern, rule.passKeys);
this.addRow(rule.pattern, rule.passKeys, rule.blockedKeys);
}
},

// `pattern` and `passKeys` are optional.
addRow(pattern, passKeys) {
// `pattern`, `passKeys`, and `blockedKeys` are optional.
addRow(pattern, passKeys, blockedKeys) {
const rulesTable = document.querySelector("#exclusion-rules");
const rowTemplate = document.querySelector("#exclusion-rule-template").content;
const rowEl = rowTemplate.cloneNode(true);
Expand All @@ -40,6 +40,10 @@ const ExclusionRulesEditor = {
keysEl.value = passKeys ?? "";
keysEl.addEventListener("input", () => this.dispatchEvent("input"));

const blockedKeysEl = rowEl.querySelector("[name=blockedKeys]");
blockedKeysEl.value = blockedKeys ?? "";
blockedKeysEl.addEventListener("input", () => this.dispatchEvent("input"));

rowEl.querySelector(".remove").addEventListener("click", (e) => {
e.target.closest("tr").remove();
this.dispatchEvent("input");
Expand All @@ -53,6 +57,7 @@ const ExclusionRulesEditor = {
const rules = rows
.map((el) => {
return {
blockedKeys: el.querySelector("[name=blockedKeys]").value.trim(),
// The ordering of these keys should match the order in defaultOptions in Settings.js.
passKeys: el.querySelector("[name=passKeys]").value.trim(),
pattern: el.querySelector("[name=pattern]").value.trim(),
Expand Down
1 change: 1 addition & 0 deletions pages/options.css
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ textarea:focus {

input[name="pattern"],
input[name="passKeys"],
input[name="blockedKeys"],
.exclusion-header-text {
width: 100%;
font-family: Consolas, "Liberation Mono", Courier, monospace;
Expand Down
6 changes: 6 additions & 0 deletions pages/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ <h2>Excluded URLs and keys</h2>
<tr>
<td><span class="exclusion-header-text">Patterns</span></td>
<td><span class="exclusion-header-text">Keys to exclude</span></td>
<td><span class="exclusion-header-text">Keys to block</span></td>
</tr>
</table>
</div>
Expand All @@ -32,6 +33,8 @@ <h2>Excluded URLs and keys</h2>
"Patterns" are URL regular expressions. <code>*</code> will match zero or more characters.
<br>
"Keys": Vimium will exclude these keys and pass them through to the page.
<br>
"Keys to block": Vimium will suppress these keys so the page never receives them.
</div>

<h2>Custom key mappings</h2>
Expand Down Expand Up @@ -273,6 +276,9 @@ <h2>Restore</h2>
<td>
<input type="text" name="passKeys" spellcheck="false" placeholder="All" />
</td>
<td>
<input type="text" name="blockedKeys" spellcheck="false" placeholder="None" />
</td>
<td>
<input type="button" class="remove" value="&#x2716;" />
</td>
Expand Down
24 changes: 15 additions & 9 deletions tests/unit_tests/exclusion_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ context("Excluded URLs and pass keys", () => {
setup(async () => {
await Settings.onLoaded();
await Settings.set("exclusionRules", [
{ pattern: "http*://mail.google.com/*", passKeys: "" },
{ pattern: "http*://www.facebook.com/*", passKeys: "abab" },
{ pattern: "http*://www.facebook.com/*", passKeys: "cdcd" },
{ pattern: "http*://www.bbc.com/*", passKeys: "" },
{ pattern: "http*://www.bbc.com/*", passKeys: "ab" },
{ pattern: "http*://www.example.com/*", passKeys: "a bb c bba a" },
{ pattern: "http*://www.duplicate.com/*", passKeys: "ace" },
{ pattern: "http*://www.duplicate.com/*", passKeys: "bdf" },
{ pattern: "http*://mail.google.com/*", passKeys: "", blockedKeys: "" },
{ pattern: "http*://www.facebook.com/*", passKeys: "abab", blockedKeys: "" },
{ pattern: "http*://www.facebook.com/*", passKeys: "cdcd", blockedKeys: " ff " },
{ pattern: "http*://www.bbc.com/*", passKeys: "", blockedKeys: "" },
{ pattern: "http*://www.bbc.com/*", passKeys: "ab", blockedKeys: "c" },
{ pattern: "http*://www.example.com/*", passKeys: "a bb c bba a", blockedKeys: " ff " },
{ pattern: "http*://www.duplicate.com/*", passKeys: "ace", blockedKeys: "xz" },
{ pattern: "http*://www.duplicate.com/*", passKeys: "bdf", blockedKeys: "zy" },
]);
});

Expand All @@ -30,41 +30,47 @@ context("Excluded URLs and pass keys", () => {
const rule = isEnabledForUrl({ url: "http://mail.google.com/calendar/page" });
assert.isFalse(rule.isEnabledForUrl);
assert.isFalse(rule.passKeys);
assert.equal("", rule.blockedKeys);
});

should("be disabled for excluded sites, one exclusion", () => {
const rule = isEnabledForUrl({ url: "http://www.bbc.com/calendar/page" });
assert.isFalse(rule.isEnabledForUrl);
assert.isFalse(rule.passKeys);
assert.equal("", rule.blockedKeys);
});

should("be enabled, but with pass keys", () => {
const rule = isEnabledForUrl({ url: "https://www.facebook.com/something" });
assert.isTrue(rule.isEnabledForUrl);
assert.equal(rule.passKeys, "abcd");
assert.equal(rule.blockedKeys, "f");
});

should("be enabled", () => {
const rule = isEnabledForUrl({ url: "http://www.twitter.com/pages" });
assert.isTrue(rule.isEnabledForUrl);
assert.isFalse(rule.passKeys);
assert.equal(rule.blockedKeys, "");
});

should("handle spaces and duplicates in passkeys", () => {
const rule = isEnabledForUrl({ url: "http://www.example.com/pages" });
assert.isTrue(rule.isEnabledForUrl);
assert.equal("abc", rule.passKeys);
assert.equal("f", rule.blockedKeys);
});

should("handle multiple passkeys rules", () => {
const rule = isEnabledForUrl({ url: "http://www.duplicate.com/pages" });
assert.isTrue(rule.isEnabledForUrl);
assert.equal("abcdef", rule.passKeys);
assert.equal("xyz", rule.blockedKeys);
});

should("be enabled when given malformed regular expressions", async () => {
await Settings.set("exclusionRules", [
{ pattern: "http*://www.bad-regexp.com/*[a-", passKeys: "" },
{ pattern: "http*://www.bad-regexp.com/*[a-", passKeys: "", blockedKeys: "" },
]);
const rule = isEnabledForUrl({ url: "http://www.bad-regexp.com/pages" });
assert.isTrue(rule.isEnabledForUrl);
Expand Down
54 changes: 54 additions & 0 deletions tests/unit_tests/keyboard_utils_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import "./test_helper.js";
import "../../lib/settings.js";
import "../../lib/keyboard_utils.js";

context("KeyboardUtils", () => {
setup(async () => {
await Settings.load();
});

teardown(async () => {
await chrome.storage.sync.clear();
Settings._settings = null;
});

should("use event.code for IME composition events", () => {
const keyChar = KeyboardUtils.getKeyChar({
key: "ㄹ",
code: "KeyF",
isComposing: true,
keyCode: 229,
});
assert.equal("f", keyChar);
});

should("use event.code for IME-style keydown events without isComposing", () => {
const keyChar = KeyboardUtils.getKeyChar({
key: "ㄹ",
code: "KeyF",
isComposing: false,
keyCode: 229,
});
assert.equal("f", keyChar);
});

should("use event.code for non-Latin letters even without composition flags", () => {
const keyChar = KeyboardUtils.getKeyChar({
key: "ㄹ",
code: "KeyF",
isComposing: false,
keyCode: 70,
});
assert.equal("f", keyChar);
});

should("preserve non-English Latin layouts outside IME composition", () => {
const keyChar = KeyboardUtils.getKeyChar({
key: "é",
code: "Digit2",
isComposing: false,
keyCode: 50,
});
assert.equal("é", keyChar);
});
});
Loading