notes/.config/chromium/Default/Extensions/dbepggeogbaibhgnhhndojpepiihcmeb/1.67_0/pages/options.js

509 lines
18 KiB
JavaScript

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