📝 Added entire config directory for backup purposes
This commit is contained in:
parent
4b38e59bb6
commit
9b946e2d14
11091 changed files with 1440953 additions and 0 deletions
|
|
@ -0,0 +1,509 @@
|
|||
const $ = id => document.getElementById(id);
|
||||
const bgExclusions = chrome.extension.getBackgroundPage().Exclusions;
|
||||
|
||||
// We have to use Settings from the background page here (not Settings, directly) to avoid a race condition for
|
||||
// the page popup. Specifically, we must ensure that the settings have been updated on the background page
|
||||
// *before* the popup closes. This ensures that any exclusion-rule changes are in place before the page
|
||||
// regains the focus.
|
||||
const bgSettings = chrome.extension.getBackgroundPage().Settings;
|
||||
|
||||
//
|
||||
// Class hierarchy for various types of option.
|
||||
// Call "fetch" after constructing an Option object, to fetch it from localStorage.
|
||||
class Option {
|
||||
// Base class for all option classes.
|
||||
// Abstract. Option does not define @populateElement or @readValueFromElement.
|
||||
constructor(field, onUpdated) {
|
||||
this.field = field;
|
||||
this.onUpdated = onUpdated;
|
||||
this.element = $(this.field);
|
||||
this.element.addEventListener("change", this.onUpdated);
|
||||
Option.all.push(this);
|
||||
}
|
||||
|
||||
// Fetch a setting from localStorage, remember the @previous value and populate the DOM element.
|
||||
// Return the fetched value.
|
||||
fetch() {
|
||||
this.populateElement(this.previous = bgSettings.get(this.field));
|
||||
return this.previous;
|
||||
}
|
||||
|
||||
// Write this option's new value back to localStorage, if necessary.
|
||||
save() {
|
||||
const value = this.readValueFromElement();
|
||||
if (JSON.stringify(value) !== JSON.stringify(this.previous))
|
||||
bgSettings.set(this.field, (this.previous = value));
|
||||
}
|
||||
|
||||
restoreToDefault() {
|
||||
bgSettings.clear(this.field);
|
||||
return this.fetch();
|
||||
}
|
||||
|
||||
static onSave(callback) {
|
||||
this.onSaveCallbacks.push(callback);
|
||||
}
|
||||
|
||||
// Static method.
|
||||
static saveOptions() {
|
||||
Option.all.map(option => option.save());
|
||||
this.onSaveCallbacks.map((callback) => callback());
|
||||
}
|
||||
}
|
||||
|
||||
// Abstract method; only implemented in sub-classes.
|
||||
// Populate the option's DOM element (@element) with the setting's current value.
|
||||
// populateElement: (value) -> DO_SOMETHING
|
||||
|
||||
// Abstract method; only implemented in sub-classes.
|
||||
// Extract the setting's new value from the option's DOM element (@element).
|
||||
// readValueFromElement: -> RETURN_SOMETHING
|
||||
|
||||
Option.all = []; // Static. Array of all options.
|
||||
Option.onSaveCallbacks = [];
|
||||
|
||||
class NumberOption extends Option {
|
||||
populateElement(value) { this.element.value = value; }
|
||||
readValueFromElement() { return parseFloat(this.element.value); }
|
||||
}
|
||||
|
||||
class TextOption extends Option {
|
||||
constructor(...args) {
|
||||
super(...Array.from(args || []));
|
||||
this.element.addEventListener("input", this.onUpdated);
|
||||
}
|
||||
populateElement(value) { this.element.value = value; }
|
||||
readValueFromElement() { return this.element.value.trim(); }
|
||||
}
|
||||
|
||||
class NonEmptyTextOption extends Option {
|
||||
constructor(...args) {
|
||||
super(...Array.from(args || []));
|
||||
this.element.addEventListener("input", this.onUpdated);
|
||||
}
|
||||
|
||||
populateElement(value) { this.element.value = value; }
|
||||
// If the new value is not empty, then return it. Otherwise, restore the default value.
|
||||
readValueFromElement() {
|
||||
const value = this.element.value.trim();
|
||||
if (value)
|
||||
return value;
|
||||
else
|
||||
return this.restoreToDefault();
|
||||
}
|
||||
}
|
||||
|
||||
class CheckBoxOption extends Option {
|
||||
populateElement(value) { this.element.checked = value; }
|
||||
readValueFromElement() { return this.element.checked; }
|
||||
}
|
||||
|
||||
class ExclusionRulesOption extends Option {
|
||||
constructor(...args) {
|
||||
super(...Array.from(args || []));
|
||||
$("exclusionAddButton").addEventListener("click", event => {
|
||||
this.addRule();
|
||||
});
|
||||
}
|
||||
|
||||
// Add a new rule, focus its pattern, scroll it into view, and return the newly-added element. On the
|
||||
// options page, there is no current URL, so there is no initial pattern. This is the default. On the popup
|
||||
// page (see ExclusionRulesOnPopupOption), the pattern is pre-populated based on the current tab's URL.
|
||||
addRule(pattern) {
|
||||
if (pattern == null)
|
||||
pattern = "";
|
||||
const element = this.appendRule({ pattern, passKeys: "" });
|
||||
this.getPattern(element).focus();
|
||||
const exclusionScrollBox = $("exclusionScrollBox");
|
||||
exclusionScrollBox.scrollTop = exclusionScrollBox.scrollHeight;
|
||||
this.onUpdated();
|
||||
return element;
|
||||
}
|
||||
|
||||
populateElement(rules) {
|
||||
// For the case of restoring a backup, we first have to remove existing rules.
|
||||
const exclusionRules = $("exclusionRules");
|
||||
while (exclusionRules.rows[1]) { exclusionRules.deleteRow(1); }
|
||||
for (let rule of rules)
|
||||
this.appendRule(rule);
|
||||
}
|
||||
|
||||
// Append a row for a new rule. Return the newly-added element.
|
||||
appendRule(rule) {
|
||||
let element;
|
||||
const content = document.querySelector('#exclusionRuleTemplate').content;
|
||||
const row = document.importNode(content, true);
|
||||
|
||||
for (let field of ["pattern", "passKeys"]) {
|
||||
element = row.querySelector(`.${field}`);
|
||||
element.value = rule[field];
|
||||
for (let event of [ "input", "change" ])
|
||||
element.addEventListener(event, this.onUpdated);
|
||||
}
|
||||
|
||||
this.getRemoveButton(row).addEventListener("click", event => {
|
||||
rule = event.target.parentNode.parentNode;
|
||||
rule.parentNode.removeChild(rule);
|
||||
this.onUpdated();
|
||||
});
|
||||
|
||||
this.element.appendChild(row);
|
||||
return this.element.children[this.element.children.length-1];
|
||||
}
|
||||
|
||||
readValueFromElement() {
|
||||
const rules =
|
||||
Array.from(this.element.getElementsByClassName("exclusionRuleTemplateInstance")).map((element) => ({
|
||||
pattern: this.getPattern(element).value.trim(),
|
||||
passKeys: this.getPassKeys(element).value.trim()
|
||||
}));
|
||||
return rules.filter(rule => rule.pattern);
|
||||
}
|
||||
|
||||
// Accessors for the three main sub-elements of an "exclusionRuleTemplateInstance".
|
||||
getPattern(element) { return element.querySelector(".pattern"); }
|
||||
getPassKeys(element) { return element.querySelector(".passKeys"); }
|
||||
getRemoveButton(element) { return element.querySelector(".exclusionRemoveButton"); }
|
||||
}
|
||||
|
||||
// ExclusionRulesOnPopupOption is ExclusionRulesOption, extended with some UI tweeks suitable for use in the
|
||||
// page popup. This also differs from ExclusionRulesOption in that, on the page popup, there is always a URL
|
||||
// (@url) associated with the current tab.
|
||||
class ExclusionRulesOnPopupOption extends ExclusionRulesOption {
|
||||
constructor(url, ...args) {
|
||||
super(...Array.from(args || []));
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
addRule() {
|
||||
const element = super.addRule(this.generateDefaultPattern());
|
||||
this.activatePatternWatcher(element);
|
||||
// ExclusionRulesOption.addRule()/super() has focused the pattern. Here, focus the passKeys instead;
|
||||
// because, in the popup, we already have a pattern, so the user is more likely to edit the passKeys.
|
||||
this.getPassKeys(element).focus();
|
||||
// Return element (for consistency with ExclusionRulesOption.addRule()).
|
||||
return element;
|
||||
}
|
||||
|
||||
populateElement(rules) {
|
||||
let element;
|
||||
super.populateElement(rules);
|
||||
const elements = this.element.getElementsByClassName("exclusionRuleTemplateInstance");
|
||||
for (element of Array.from(elements))
|
||||
this.activatePatternWatcher(element);
|
||||
|
||||
let haveMatch = false;
|
||||
for (element of Array.from(elements)) {
|
||||
const pattern = this.getPattern(element).value.trim();
|
||||
if (0 <= this.url.search(bgExclusions.RegexpCache.get(pattern))) {
|
||||
haveMatch = true;
|
||||
this.getPassKeys(element).focus();
|
||||
} else {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
}
|
||||
if (!haveMatch)
|
||||
return this.addRule();
|
||||
}
|
||||
|
||||
// Provide visual feedback (make it red) when a pattern does not match the current tab's URL.
|
||||
activatePatternWatcher(element) {
|
||||
const patternElement = element.children[0].firstChild;
|
||||
patternElement.addEventListener("keyup", () => {
|
||||
if (this.url.match(bgExclusions.RegexpCache.get(patternElement.value))) {
|
||||
patternElement.title = (patternElement.style.color = "");
|
||||
} else {
|
||||
patternElement.style.color = "red";
|
||||
patternElement.title = "Red text means that the pattern does not\nmatch the current URL.";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Generate a default exclusion-rule pattern from a URL. This is then used to pre-populate the pattern on
|
||||
// the page popup.
|
||||
generateDefaultPattern() {
|
||||
if (/^https?:\/\/./.test(this.url)) {
|
||||
// The common use case is to disable Vimium at the domain level.
|
||||
// Generate "https?://www.example.com/*" from "http://www.example.com/path/to/page.html".
|
||||
// Note: IPV6 host addresses will contain "[" and "]" (which must be escaped).
|
||||
const hostname = this.url.split("/",3).slice(1).join("/").replace("[", "\\[").replace("]", "\\]");
|
||||
return "https?:/" + hostname + "/*";
|
||||
} else if (/^[a-z]{3,}:\/\/./.test(this.url)) {
|
||||
// Anything else which seems to be a URL.
|
||||
return this.url.split("/",3).join("/") + "/*";
|
||||
} else {
|
||||
return this.url + "*";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Options = {
|
||||
exclusionRules: ExclusionRulesOption,
|
||||
filterLinkHints: CheckBoxOption,
|
||||
waitForEnterForFilteredHints: CheckBoxOption,
|
||||
hideHud: CheckBoxOption,
|
||||
keyMappings: TextOption,
|
||||
linkHintCharacters: NonEmptyTextOption,
|
||||
linkHintNumbers: NonEmptyTextOption,
|
||||
newTabUrl: NonEmptyTextOption,
|
||||
nextPatterns: NonEmptyTextOption,
|
||||
previousPatterns: NonEmptyTextOption,
|
||||
regexFindMode: CheckBoxOption,
|
||||
ignoreKeyboardLayout: CheckBoxOption,
|
||||
scrollStepSize: NumberOption,
|
||||
smoothScroll: CheckBoxOption,
|
||||
grabBackFocus: CheckBoxOption,
|
||||
searchEngines: TextOption,
|
||||
searchUrl: NonEmptyTextOption,
|
||||
userDefinedLinkHintCss: NonEmptyTextOption
|
||||
};
|
||||
|
||||
const initOptionsPage = function() {
|
||||
const onUpdated = function() {
|
||||
$("saveOptions").removeAttribute("disabled");
|
||||
$("saveOptions").textContent = "Save Changes";
|
||||
};
|
||||
|
||||
// Display either "linkHintNumbers" or "linkHintCharacters", depending upon "filterLinkHints".
|
||||
const maintainLinkHintsView = function() {
|
||||
const hide = el => el.style.display = "none";
|
||||
const show = el => el.style.display = "table-row";
|
||||
if ($("filterLinkHints").checked) {
|
||||
hide($("linkHintCharactersContainer"));
|
||||
show($("linkHintNumbersContainer"));
|
||||
show($("waitForEnterForFilteredHintsContainer"));
|
||||
} else {
|
||||
show($("linkHintCharactersContainer"));
|
||||
hide($("linkHintNumbersContainer"));
|
||||
hide($("waitForEnterForFilteredHintsContainer"));
|
||||
}
|
||||
};
|
||||
|
||||
const maintainAdvancedOptions = function() {
|
||||
if (bgSettings.get("optionsPage_showAdvancedOptions")) {
|
||||
$("advancedOptions").style.display = "table-row-group";
|
||||
return $("advancedOptionsButton").textContent = "Hide Advanced Options";
|
||||
} else {
|
||||
$("advancedOptions").style.display = "none";
|
||||
return $("advancedOptionsButton").textContent = "Show Advanced Options";
|
||||
}
|
||||
};
|
||||
maintainAdvancedOptions();
|
||||
|
||||
const toggleAdvancedOptions = function(event) {
|
||||
bgSettings.set("optionsPage_showAdvancedOptions", !bgSettings.get("optionsPage_showAdvancedOptions"));
|
||||
maintainAdvancedOptions();
|
||||
$("advancedOptionsButton").blur();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const activateHelpDialog = function() {
|
||||
HelpDialog.toggle({showAllCommandDetails: true});
|
||||
};
|
||||
|
||||
const saveOptions = function() {
|
||||
$("linkHintCharacters").value = $("linkHintCharacters").value.toLowerCase();
|
||||
Option.saveOptions();
|
||||
$("saveOptions").disabled = true;
|
||||
$("saveOptions").textContent = "Saved";
|
||||
};
|
||||
|
||||
$("saveOptions").addEventListener("click", saveOptions);
|
||||
$("advancedOptionsButton").addEventListener("click", toggleAdvancedOptions);
|
||||
$("showCommands").addEventListener("click", activateHelpDialog);
|
||||
$("filterLinkHints").addEventListener("click", maintainLinkHintsView);
|
||||
|
||||
for (let element of Array.from(document.getElementsByClassName("nonEmptyTextOption"))) {
|
||||
element.className = element.className + " example info";
|
||||
element.textContent = "Leave empty to reset this option.";
|
||||
}
|
||||
|
||||
window.onbeforeunload = function() {
|
||||
if (!$("saveOptions").disabled)
|
||||
return "You have unsaved changes to options.";
|
||||
};
|
||||
|
||||
document.addEventListener("keyup", function(event) {
|
||||
if (event.ctrlKey && (event.keyCode === 13)) {
|
||||
if (document && document.activeElement && document.activeElement.blur) {
|
||||
document.activeElement.blur();
|
||||
return saveOptions();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Populate options. The constructor adds each new object to "Option.all".
|
||||
for (let name of Object.keys(Options)) {
|
||||
const type = Options[name];
|
||||
const option = new type(name, onUpdated);
|
||||
option.fetch();
|
||||
}
|
||||
|
||||
maintainLinkHintsView();
|
||||
};
|
||||
|
||||
const initPopupPage = function() {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, function(...args) {
|
||||
const [tab] = Array.from(args[0]);
|
||||
let exclusions = null;
|
||||
document.getElementById("optionsLink").setAttribute("href", chrome.runtime.getURL("pages/options.html"));
|
||||
|
||||
const tabPorts = chrome.extension.getBackgroundPage().portsForTab[tab.id];
|
||||
if (!tabPorts || !(Object.keys(tabPorts).length > 0)) {
|
||||
// The browser has disabled Vimium on this page. Place a message explaining this into the popup.
|
||||
document.body.innerHTML = `\
|
||||
<div style="width: 400px; margin: 5px;">
|
||||
<p style="margin-bottom: 5px;">
|
||||
Vimium is not running on this page.
|
||||
</p>
|
||||
<p style="margin-bottom: 5px;">
|
||||
Your browser does not run web extensions like Vimium on certain pages,
|
||||
usually for security reasons.
|
||||
</p>
|
||||
<p>
|
||||
Unless your browser's developers change their policy, then unfortunately it is not possible to make Vimium (or any other
|
||||
web extension, for that matter) work on this page.
|
||||
</p>
|
||||
</div>\
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// As the active URL, we choose the most recently registered URL from a frame in the tab, or the tab's own
|
||||
// URL.
|
||||
const url = chrome.extension.getBackgroundPage().urlForTab[tab.id] || tab.url;
|
||||
|
||||
const updateState = function() {
|
||||
const rule = bgExclusions.getRule(url, exclusions.readValueFromElement());
|
||||
$("state").innerHTML = "Vimium will " +
|
||||
(rule && rule.passKeys ?
|
||||
`exclude <span class='code'>${rule.passKeys}</span>`
|
||||
: (rule ? "be disabled" : "be enabled"));
|
||||
};
|
||||
|
||||
const onUpdated = function() {
|
||||
$("helpText").innerHTML = "Type <strong>Ctrl-Enter</strong> to save and close.";
|
||||
$("saveOptions").removeAttribute("disabled");
|
||||
$("saveOptions").textContent = "Save Changes";
|
||||
if (exclusions)
|
||||
return updateState();
|
||||
};
|
||||
|
||||
const saveOptions = function() {
|
||||
Option.saveOptions();
|
||||
$("saveOptions").textContent = "Saved";
|
||||
$("saveOptions").disabled = true;
|
||||
};
|
||||
|
||||
$("saveOptions").addEventListener("click", saveOptions);
|
||||
|
||||
document.addEventListener("keyup", function(event) {
|
||||
if (event.ctrlKey && (event.keyCode === 13)) {
|
||||
saveOptions();
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Populate options. Just one, here.
|
||||
exclusions = new ExclusionRulesOnPopupOption(url, "exclusionRules", onUpdated);
|
||||
exclusions.fetch();
|
||||
|
||||
updateState();
|
||||
document.addEventListener("keyup", updateState);
|
||||
});
|
||||
|
||||
// Install version number.
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
$("versionNumber").textContent = manifest.version;
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// Initialization.
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
DomUtils.injectUserCss(); // Manually inject custom user styles.
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', chrome.extension.getURL('pages/exclusions.html'), true);
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
$("exclusionScrollBox").innerHTML = xhr.responseText;
|
||||
switch (location.pathname) {
|
||||
case "/pages/options.html": initOptionsPage(); break;
|
||||
case "/pages/popup.html": initPopupPage(); break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
|
||||
//
|
||||
// Backup and restore.
|
||||
if (global.DomUtils) { // global.DomUtils is not defined when running our tests.
|
||||
DomUtils.documentReady(function() {
|
||||
// Only initialize backup/restore on the options page (not the popup).
|
||||
if (location.pathname !== "/pages/options.html")
|
||||
return;
|
||||
|
||||
let restoreSettingsVersion = null;
|
||||
|
||||
const populateBackupLinkUrl = function() {
|
||||
const backup = {settingsVersion: bgSettings.get("settingsVersion")};
|
||||
for (let option of Array.from(Option.all))
|
||||
backup[option.field] = option.readValueFromElement();
|
||||
|
||||
// Create the blob in the background page so it isn't garbage collected when the page closes in FF.
|
||||
const bgWin = chrome.extension.getBackgroundPage();
|
||||
const blob = new bgWin.Blob([ JSON.stringify(backup, null, 2) + "\n" ]);
|
||||
$("backupLink").href = bgWin.URL.createObjectURL(blob);
|
||||
};
|
||||
|
||||
$("backupLink").addEventListener("mousedown", populateBackupLinkUrl, true);
|
||||
|
||||
$("chooseFile").addEventListener("change", function(event) {
|
||||
if (document.activeElement)
|
||||
document.activeElement.blur();
|
||||
|
||||
const files = event.target.files;
|
||||
if (files.length === 1) {
|
||||
const file = files[0];
|
||||
const reader = new FileReader;
|
||||
reader.readAsText(file);
|
||||
reader.onload = function(event) {
|
||||
let backup;
|
||||
try {
|
||||
backup = JSON.parse(reader.result);
|
||||
} catch (error) {
|
||||
alert("Failed to parse Vimium backup.");
|
||||
return;
|
||||
}
|
||||
|
||||
if ("settingsVersion" in backup)
|
||||
restoreSettingsVersion = backup["settingsVersion"];
|
||||
for (let option of Array.from(Option.all)) {
|
||||
if (option.field in backup) {
|
||||
option.populateElement(backup[option.field]);
|
||||
option.onUpdated();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Option.onSave(function() {
|
||||
// If we're restoring a backup, then restore the backed up settingsVersion.
|
||||
if (restoreSettingsVersion != null) {
|
||||
bgSettings.set("settingsVersion", restoreSettingsVersion);
|
||||
restoreSettingsVersion = null;
|
||||
}
|
||||
// Reset the restore-backup input.
|
||||
$("chooseFile").value = "";
|
||||
// We need to apply migrations in case we are restoring an old backup.
|
||||
bgSettings.applyMigrations();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Exported for use by our tests.
|
||||
global.Options = Options
|
||||
global.isVimiumOptionsPage = true
|
||||
Loading…
Add table
Add a link
Reference in a new issue