diff --git a/background_scripts/exclusions.js b/background_scripts/exclusions.js index aaa2444fe..ae2507e52 100644 --- a/background_scripts/exclusions.js +++ b/background_scripts/exclusions.js @@ -25,7 +25,6 @@ const ExclusionRegexpCache = { } }, }; - // Make RegexpCache, which is required on the page popup, accessible via the Exclusions object. const RegexpCache = ExclusionRegexpCache; @@ -37,15 +36,17 @@ function getRule(url, rules) { if (rules == null) { rules = Settings.get("exclusionRules"); } - const matchingRules = rules.filter((r) => - r.pattern && (url.search(ExclusionRegexpCache.get(r.pattern)) >= 0) + const matchingRules = rules.filter( + (r) => r.pattern && url.search(ExclusionRegexpCache.get(r.pattern)) >= 0, ); // An absolute exclusion rule (one with no passKeys) takes priority. for (const rule of matchingRules) { if (!rule.passKeys) return rule; } // Strip whitespace from all matching passKeys strings, and join them together. - const passKeys = matchingRules.map((r) => r.passKeys.split(/\s+/).join("")).join(""); + const passKeys = matchingRules + .map((r) => r.passKeys.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) { @@ -58,7 +59,7 @@ function getRule(url, rules) { export function isEnabledForUrl(url) { const rule = getRule(url); return { - isEnabledForUrl: !rule || (rule.passKeys.length > 0), + isEnabledForUrl: !rule || rule.passKeys.length > 0, passKeys: rule ? rule.passKeys : "", }; } @@ -74,5 +75,4 @@ function onSettingsUpdated() { // popup is closed. Do NOT store it/use it asynchronously. ExclusionRegexpCache.clear(); } - Settings.addEventListener("change", () => onSettingsUpdated()); diff --git a/content_scripts/link_hints.js b/content_scripts/link_hints.js index 033e830a4..62d2577a4 100644 --- a/content_scripts/link_hints.js +++ b/content_scripts/link_hints.js @@ -430,17 +430,7 @@ class LinkHintsMode { if (hasPopoverSupport) { this.containerEl.popover = "manual"; this.containerEl.showPopover(); - Object.assign(this.containerEl.style, { - top: 0, - left: 0, - position: "absolute", - // This display: block is required to override Github Enterprise's CSS circa 2024-04-01. See - // #4446. - display: "block", - width: "100%", - height: "100%", - overflow: "visible", - }); + this.containerEl.classList.add("vimiumPopover"); } this.setIndicator(); diff --git a/content_scripts/mode_visual.js b/content_scripts/mode_visual.js index c7a294c3d..ffb4353d1 100644 --- a/content_scripts/mode_visual.js +++ b/content_scripts/mode_visual.js @@ -20,7 +20,7 @@ class Movement { // or return undefined. getNextForwardCharacter() { const beforeText = this.selection.toString(); - if ((beforeText.length === 0) || (this.getDirection() === forward)) { + if (beforeText.length === 0 || this.getDirection() === forward) { this.selection.modify("extend", forward, character); const afterText = this.selection.toString(); if (beforeText !== afterText) { @@ -55,13 +55,15 @@ class Movement { // runMovement(...args) { // Normalize the various argument forms. - const [direction, granularity] = (typeof (args[0]) === "string") && (args.length === 1) + const [direction, granularity] = typeof args[0] === "string" && args.length === 1 ? args[0].trim().split(/\s+/) - : (args.length === 1 ? args[0] : args.slice(0, 2)); + : args.length === 1 + ? args[0] + : args.slice(0, 2); // Native word movements behave differently on Linux and Windows, see #1441. So we implement // some of them character-by-character. - if ((granularity === vimword) && (direction === forward)) { + if (granularity === vimword && direction === forward) { // Extend selection to the end of the 'vimword'. while (this.nextCharacterIsWordCharacter()) { if (this.extendByOneCharacter(forward) === 0) { @@ -69,7 +71,10 @@ class Movement { } } // Extend selection after the 'vimword' to position before next word. - while (this.getNextForwardCharacter() && !this.nextCharacterIsWordCharacter()) { + while ( + this.getNextForwardCharacter() && + !this.nextCharacterIsWordCharacter() + ) { if (this.extendByOneCharacter(forward) === 0) { return; } @@ -83,9 +88,12 @@ class Movement { // As above, we implement this character-by-character to get consistent behavior on Windows and // Linux. - if ((granularity === word) && (direction === forward)) { + if (granularity === word && direction === forward) { // Extend selection to the start of the next 'word' (non-word characters, e.g. whitespace). - while (this.getNextForwardCharacter() && !this.nextCharacterIsWordCharacter()) { + while ( + this.getNextForwardCharacter() && + !this.nextCharacterIsWordCharacter() + ) { if (this.extendByOneCharacter(forward) === 0) { return; } @@ -127,7 +135,10 @@ class Movement { range.collapse(direction === backward); this.setSelectionRange(range); const which = direction === forward ? "start" : "end"; - this.selection.extend(original[`${which}Container`], original[`${which}Offset`]); + this.selection.extend( + original[`${which}Container`], + original[`${which}Offset`], + ); } } @@ -211,7 +222,9 @@ class Movement { for (let i = 1, end = count; i < end; i++) this.runMovement(forward, line); this.runMovement(forward, lineboundary); // Include the next character if that character is a newline. - if (this.getNextForwardCharacter() === "\n") return this.runMovement(forward, character); + if (this.getNextForwardCharacter() === "\n") { + return this.runMovement(forward, character); + } } // Scroll the focus into view. @@ -232,7 +245,9 @@ class VisualMode extends KeyHandlerMode { if (options == null) { options = {}; } - this.movement = new Movement(options.alterMethod != null ? options.alterMethod : "extend"); + this.movement = new Movement( + options.alterMethod != null ? options.alterMethod : "extend", + ); this.selection = this.movement.selection; // Build the key mapping structure required by KeyHandlerMode. This only handles one- and @@ -245,45 +260,60 @@ class VisualMode extends KeyHandlerMode { } if (keys.length === 1) { keyMapping[keys] = { command: movement }; - } else { // keys.length == 2 + } else { + // keys.length == 2 if (keyMapping[keys[0]] == null) { keyMapping[keys[0]] = {}; } - Object.assign(keyMapping[keys[0]], { [keys[1]]: { command: movement } }); + Object.assign(keyMapping[keys[0]], { + [keys[1]]: { command: movement }, + }); } } // Aliases and complex bindings. Object.assign(keyMapping, { - "B": keyMapping.b, - "W": keyMapping.w, + B: keyMapping.b, + W: keyMapping.w, "": { command(count) { - return Scroller.scrollBy("y", count * Settings.get("scrollStepSize"), 1, false); + return Scroller.scrollBy( + "y", + count * Settings.get("scrollStepSize"), + 1, + false, + ); }, }, "": { command(count) { - return Scroller.scrollBy("y", -count * Settings.get("scrollStepSize"), 1, false); + return Scroller.scrollBy( + "y", + -count * Settings.get("scrollStepSize"), + 1, + false, + ); }, }, }); - super.init(Object.assign(options, { - name: options.name != null ? options.name : "visual", - indicator: options.indicator != null ? options.indicator : "Visual mode", - // Visual mode, visual-line mode and caret mode each displace each other. - singleton: "visual-mode-group", - exitOnEscape: true, - suppressAllKeyboardEvents: true, - keyMapping, - commandHandler: this.commandHandler.bind(this), - })); + super.init( + Object.assign(options, { + name: options.name != null ? options.name : "visual", + indicator: options.indicator != null ? options.indicator : "Visual mode", + // Visual mode, visual-line mode and caret mode each displace each other. + singleton: "visual-mode-group", + exitOnEscape: true, + suppressAllKeyboardEvents: true, + keyMapping, + commandHandler: this.commandHandler.bind(this), + }), + ); // If there was a range selection when the user lanuched visual mode, then we retain the // selection on exit. this.shouldRetainSelectionOnExit = this.options.userLaunchedMode && - (this.selection.type === "Range"); + this.selection.type === "Range"; this.onExit((event = null) => { // Retain any selection, regardless of how we exit. @@ -291,8 +321,10 @@ class VisualMode extends KeyHandlerMode { // This mimics vim: when leaving visual mode via Escape, collapse to focus, otherwise // collapse to anchor. } else if ( - event && (event.type === "keydown") && KeyboardUtils.isEscape(event) && - (this.name !== "caret") + event && + event.type === "keydown" && + KeyboardUtils.isEscape(event) && + this.name !== "caret" ) { this.movement.collapseSelectionToFocus(); } else { @@ -300,7 +332,10 @@ class VisualMode extends KeyHandlerMode { } // Don't leave the user in insert mode just because they happen to have selected an input. - if (document.activeElement && DomUtils.isEditable(document.activeElement)) { + if ( + document.activeElement && + DomUtils.isEditable(document.activeElement) + ) { if ((event != null ? event.type : undefined) !== "click") { return document.activeElement.blur(); } @@ -312,7 +347,12 @@ class VisualMode extends KeyHandlerMode { // Yank on . keypress: (event) => { if (event.key === "Enter") { - if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey) { + if ( + !event.metaKey && + !event.ctrlKey && + !event.altKey && + !event.shiftKey + ) { this.yank(); return this.suppressEvent; } @@ -331,18 +371,27 @@ class VisualMode extends KeyHandlerMode { // Establish or use the initial selection. If that's not possible, then enter caret mode. if (this.name !== "caret") { if (["Caret", "Range"].includes(this.selection.type)) { - let selectionRect = this.selection.getRangeAt(0).getBoundingClientRect(); + let selectionRect = this.selection + .getRangeAt(0) + .getBoundingClientRect(); if (globalThis.vimiumDomTestsAreRunning) { // We're running the DOM tests, where getBoundingClientRect() isn't available. if (!selectionRect) { - selectionRect = { top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0 }; + selectionRect = { + top: 0, + bottom: 0, + left: 0, + right: 0, + width: 0, + height: 0, + }; } } selectionRect = Rect.intersect( selectionRect, Rect.create(0, 0, globalThis.innerWidth, globalThis.innerHeight), ); - if ((selectionRect.height >= 0) && (selectionRect.width >= 0)) { + if (selectionRect.height >= 0 && selectionRect.width >= 0) { // The selection is visible in the current viewport. if (this.selection.type === "Caret") { // The caret is in the viewport. Make make it visible. @@ -356,7 +405,7 @@ class VisualMode extends KeyHandlerMode { } } - if ((this.selection.type !== "Range") && (this.name !== "caret")) { + if (this.selection.type !== "Range" && this.name !== "caret") { new CaretMode().init(); return HUD.show("No usable selection, entering caret mode...", 2500); } @@ -396,7 +445,7 @@ class VisualMode extends KeyHandlerMode { // The find was successfull. If we're in caret mode, then we should now have a selection, so we // can drop back into visual mode. - if ((this.name === "caret") && (this.selection.toString().length > 0)) { + if (this.name === "caret" && this.selection.toString().length > 0) { const mode = new VisualMode(); mode.init(); return mode; @@ -417,7 +466,10 @@ class VisualMode extends KeyHandlerMode { message = message.slice(0, 12) + "..."; } const plural = this.yankedText.length === 1 ? "" : "s"; - HUD.show(`Yanked ${this.yankedText.length} character${plural}: \"${message}\".`, 2500); + HUD.show( + `Yanked ${this.yankedText.length} character${plural}: \"${message}\".`, + 2500, + ); return this.yankedText; } @@ -425,33 +477,33 @@ class VisualMode extends KeyHandlerMode { // A movement can be either a string or a function. VisualMode.prototype.movements = { - "l": "forward character", - "h": "backward character", - "j": "forward line", - "k": "backward line", - "e": "forward word", - "b": "backward word", - "w": "forward vimword", + l: "forward character", + h: "backward character", + j: "forward line", + k: "backward line", + e: "forward word", + b: "backward word", + w: "forward vimword", ")": "forward sentence", "(": "backward sentence", "}": "forward paragraph", "{": "backward paragraph", - "0": "backward lineboundary", - "$": "forward lineboundary", - "G": "forward documentboundary", - "gg": "backward documentboundary", + 0: "backward lineboundary", + $: "forward lineboundary", + G: "forward documentboundary", + gg: "backward documentboundary", - "aw"(count) { + aw(count) { return this.movement.selectLexicalEntity(word, count); }, - "as"(count) { + as(count) { return this.movement.selectLexicalEntity(sentence, count); }, - "n"(count) { + n(count) { return this.find(count, false); }, - "N"(count) { + N(count) { return this.find(count, true); }, "/"() { @@ -459,37 +511,43 @@ VisualMode.prototype.movements = { return new FindMode({ returnToViewport: true }).onExit(() => new VisualMode().init()); }, - "y"() { + y() { return this.yank(); }, - "Y"(count) { + Y(count) { this.movement.selectLine(count); return this.yank(); }, - "p"() { - return chrome.runtime.sendMessage({ handler: "openUrlInCurrentTab", url: this.yank() }); + p() { + return chrome.runtime.sendMessage({ + handler: "openUrlInCurrentTab", + url: this.yank(), + }); }, - "P"() { - return chrome.runtime.sendMessage({ handler: "openUrlInNewTab", url: this.yank() }); + P() { + return chrome.runtime.sendMessage({ + handler: "openUrlInNewTab", + url: this.yank(), + }); }, - "v"() { + v() { return new VisualMode().init(); }, - "V"() { + V() { return new VisualLineMode().init(); }, - "c"() { + c() { // If we're already in caret mode, or if the selection looks the same as it would in caret mode, // then callapse to anchor (so that the caret-mode selection will seem unchanged). Otherwise, // we're in visual mode and the user has moved the focus, so collapse to that. - if ((this.name === "caret") || (this.selection.toString().length <= 1)) { + if (this.name === "caret" || this.selection.toString().length <= 1) { this.movement.collapseSelectionToAnchor(); } else { this.movement.collapseSelectionToFocus(); } return new CaretMode().init(); }, - "o"() { + o() { return this.movement.reverseSelection(); }, }; @@ -499,7 +557,12 @@ class VisualLineMode extends VisualMode { if (options == null) { options = {}; } - super.init(Object.assign(options, { name: "visual/line", indicator: "Visual mode (line)" })); + super.init( + Object.assign(options, { + name: "visual/line", + indicator: "Visual mode (line)", + }), + ); return this.extendSelection(); } @@ -517,7 +580,10 @@ class VisualLineMode extends VisualMode { if (this.selection.isCollapsed) { this.extendSelection(); const [direction, granularity] = command.split(" "); - if ((this.movement.getDirection() !== direction) && (granularity === "line")) { + if ( + this.movement.getDirection() !== direction && + granularity === "line" + ) { this.movement.reverseSelection(); } this.movement.runMovement(command); @@ -536,15 +602,17 @@ class VisualLineMode extends VisualMode { extendSelection() { const initialDirection = this.movement.getDirection(); - // TODO(philc): Reformat this to be a plain loop rather than a closure. - return (() => { - const result = []; - for (const direction of [initialDirection, this.movement.opposite[initialDirection]]) { - this.movement.runMovement(direction, lineboundary); - result.push(this.movement.reverseSelection()); - } - return result; - })(); + const result = []; + for ( + const direction of [ + initialDirection, + this.movement.opposite[initialDirection], + ] + ) { + this.movement.runMovement(direction, lineboundary); + result.push(this.movement.reverseSelection()); + } + return result; } } @@ -554,7 +622,11 @@ class CaretMode extends VisualMode { options = {}; } super.init( - Object.assign(options, { name: "caret", indicator: "Caret mode", alterMethod: "move" }), + Object.assign(options, { + name: "caret", + indicator: "Caret mode", + alterMethod: "move", + }), ); // Establish the initial caret. @@ -591,12 +663,18 @@ class CaretMode extends VisualMode { // try to find the start of the page's main textual content. establishInitialSelectionAnchor() { let node; - const nodes = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); + const nodes = document.createTreeWalker( + document.body, + NodeFilter.SHOW_TEXT, + ); while ((node = nodes.nextNode())) { // Don't choose short text nodes; they're likely to be part of a banner. - if ((node.nodeType === 3) && (50 <= node.data.trim().length)) { + if (node.nodeType === 3 && 50 <= node.data.trim().length) { const element = node.parentElement; - if (DomUtils.getVisibleClientRect(element) && !DomUtils.isEditable(element)) { + if ( + DomUtils.getVisibleClientRect(element) && + !DomUtils.isEditable(element) + ) { // Start at the offset of the first non-whitespace character. const offset = node.data.length - node.data.replace(/^\s+/, "").length; const range = document.createRange(); diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css index 9182d7255..0ebd46d98 100644 --- a/content_scripts/vimium.css +++ b/content_scripts/vimium.css @@ -71,7 +71,8 @@ tr.vimium-reset { z-index: 2147483647; } -thead.vimium-reset, tbody.vimium-reset { +thead.vimium-reset, +tbody.vimium-reset { display: table-header-group; } @@ -179,6 +180,16 @@ iframe.vimium-help-dialog-frame { z-index: 2147483647; } +.vimiumPopover { + top: 0 !important; + left: 0 !important; + position: absolute !important; + display: block !important; + width: 100% !important; + height: 100% !important; + overflow: visible !important; +} + iframe.vimium-hud-frame { background-color: transparent; padding: 0px; @@ -207,7 +218,8 @@ iframe.vomnibar-frame { display: block; position: fixed; - width: calc(80% + 20px); /* same adjustment as in pages/vomnibar_page.js */ + width: calc(80% + 20px); + /* same adjustment as in pages/vomnibar_page.js */ min-width: 400px; height: calc(100% - 70px); top: 70px; diff --git a/pages/options.js b/pages/options.js index 0be0734c3..249f932c0 100644 --- a/pages/options.js +++ b/pages/options.js @@ -45,20 +45,20 @@ const OptionsPage = { saveButton.addEventListener("click", () => this.saveOptions()); - this.getOptionEl("filterLinkHints").addEventListener( - "click", - () => this.maintainLinkHintsView(), + this.getOptionEl("filterLinkHints").addEventListener("click", () => + this.maintainLinkHintsView(), ); - document.querySelector("#download-backup").addEventListener( - "mousedown", - () => this.onDownloadBackupClicked(), - true, - ); - document.querySelector("#upload-backup").addEventListener( - "change", - () => this.onUploadBackupClicked(), - ); + document + .querySelector("#download-backup") + .addEventListener( + "mousedown", + () => this.onDownloadBackupClicked(), + true, + ); + document + .querySelector("#upload-backup") + .addEventListener("change", () => this.onUploadBackupClicked()); for (const el of document.querySelectorAll(".reset-link a")) { el.addEventListener("click", (event) => { @@ -95,9 +95,15 @@ const OptionsPage = { // Invoked when the user clicks the "reset" button next to an option's text field. resetInputValue(event) { const parentDiv = event.target.parentNode.parentNode; - console.assert(parentDiv?.tagName == "DIV", "Expected parent to be a div", event.target); - const input = parentDiv.querySelector("input") || parentDiv.querySelector("textarea"); + console.assert( + parentDiv?.tagName == "DIV", + "Expected parent to be a div", + event.target, + ); + const input = + parentDiv.querySelector("input") || parentDiv.querySelector("textarea"); const optionName = input.name; + const defaultValue = Settings.defaultOptions[optionName]; input.value = defaultValue; event.preventDefault(); @@ -151,7 +157,8 @@ const OptionsPage = { } } if (settings["linkHintCharacters"] != null) { - settings["linkHintCharacters"] = settings["linkHintCharacters"].toLowerCase(); + settings["linkHintCharacters"] = + settings["linkHintCharacters"].toLowerCase(); } settings["exclusionRules"] = ExclusionRulesEditor.getRules(); return settings; @@ -178,9 +185,11 @@ const OptionsPage = { // linkHintCharacters field. text = this.getOptionEl("linkHintCharacters").value.trim(); if (text != this.removeDuplicateChars(text)) { - results["linkHintCharacters"] = "This cannot contain duplicate characters."; + results["linkHintCharacters"] = + "This cannot contain duplicate characters."; } else if (text.length <= 1) { - results["linkHintCharacters"] = "This must be at least two characters long."; + results["linkHintCharacters"] = + "This must be at least two characters long."; } // linkHintNumbers field. @@ -222,10 +231,16 @@ const OptionsPage = { } // Some options can be hidden in the UI. If they have validation errors, force them to be shown. if (errors["linkHintCharacters"]) { - this.showElement(document.querySelector("#link-hint-characters-container"), true); + this.showElement( + document.querySelector("#link-hint-characters-container"), + true, + ); } if (errors["linkHintNumbers"]) { - this.showElement(document.querySelector("#link-hint-numbers-container"), true); + this.showElement( + document.querySelector("#link-hint-numbers-container"), + true, + ); } const hasErrors = Object.keys(errors).length > 0; return hasErrors; @@ -246,8 +261,10 @@ const OptionsPage = { async saveOptions() { const hasErrors = this.showValidationErrors(); if (hasErrors) { - // TODO(philc): If no fields with validation errors are in view, scroll one of them into view - // so it's clear what the issue is. + const error = document.querySelector(".validation-error"); + + error?.scrollIntoView({ smooth: true }); + return; } @@ -266,6 +283,7 @@ const OptionsPage = { maintainLinkHintsView() { const errors = this.getValidationErrors(); const isFilteredLinkhints = this.getOptionEl("filterLinkHints").checked; + this.showElement( document.querySelector("#link-hint-characters-container"), !isFilteredLinkhints || errors["linkHintCharacters"], @@ -283,7 +301,8 @@ const OptionsPage = { onDownloadBackupClicked() { const backup = Settings.pruneOutDefaultValues(this.getSettingsFromForm()); const settingsBlob = new Blob([JSON.stringify(backup, null, 2) + "\n"]); - document.querySelector("#download-backup").href = URL.createObjectURL(settingsBlob); + document.querySelector("#download-backup").href = + URL.createObjectURL(settingsBlob); }, onUploadBackupClicked() {