Skip to content

Commit

Permalink
Add firenvim#focus_next and firenvim#focus_prev
Browse files Browse the repository at this point in the history
Closes #1270 .
  • Loading branch information
glacambre committed Jan 9, 2022
1 parent d316afe commit 7f8c3c3
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 0 deletions.
12 changes: 12 additions & 0 deletions autoload/firenvim.vim
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ function! firenvim#focus_page() abort
call rpcnotify(firenvim#get_chan(), 'firenvim_focus_page')
endfunction

" Asks the browser extension to release focus from the frame and focus the
" next page element, matching the behavior of pressing `<Tab>` in the page.
function! firenvim#focus_next() abort
call rpcnotify(firenvim#get_chan(), 'firenvim_focus_next')
endfunction

" Asks the browser extension to release focus from the frame and focus the
" next page element, matching the behavior of pressing `<Tab>` in the page.
function! firenvim#focus_prev() abort
call rpcnotify(firenvim#get_chan(), 'firenvim_focus_prev')
endfunction

" Asks the browser extension to hide the firenvim frame
function! firenvim#hide_frame() abort
call rpcnotify(firenvim#get_chan(), 'firenvim_hide_frame')
Expand Down
9 changes: 9 additions & 0 deletions src/FirenvimElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ export class FirenvimElement {
this.span = elem
.ownerDocument
.createElementNS("http://www.w3.org/1999/xhtml", "span");
// Make non-focusable, as otherwise <Tab> and <S-Tab> in the page would
// focus the iframe at the end of the page instead of focusing the
// browser's UI. The only way to <Tab>-focus the frame is to
// <Tab>-focus the corresponding input element.
this.span.setAttribute("tabindex", "-1");
this.iframe = elem
.ownerDocument
.createElementNS("http://www.w3.org/1999/xhtml", "iframe") as HTMLIFrameElement;
Expand Down Expand Up @@ -360,6 +365,10 @@ export class FirenvimElement {
return p;
}

getOriginalElement () {
return this.originalElement;
}

getSelector () {
return computeSelector(this.getElement());
}
Expand Down
6 changes: 6 additions & 0 deletions src/Neovim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export async function neovim(
case "firenvim_focus_input":
lastLostFocus = performance.now();
return page.focusInput();
case "firenvim_focus_next":
lastLostFocus = performance.now();
return page.focusNext();
case "firenvim_focus_prev":
lastLostFocus = performance.now();
return page.focusPrev();
case "firenvim_hide_frame":
lastLostFocus = performance.now();
return page.hideEditor();
Expand Down
2 changes: 2 additions & 0 deletions src/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ class ThunderbirdPageEventEmitter extends PageEventEmitter {
async evalInPage(js: string) { return eval(js) }
async focusInput() { return Promise.resolve(); }
async focusPage() { return Promise.resolve(); }
async focusNext() { return Promise.resolve(); }
async focusPrev() { return Promise.resolve(); }
async getEditorInfo() { return [document.location.href, "", [1, 1], undefined] as [string, string, [number, number], string] }
async getElementContent() {
const details = await browser.runtime.sendMessage({ funcName: ["getOwnComposeDetails"], args: [] });
Expand Down
90 changes: 90 additions & 0 deletions src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,90 @@ export function getActiveContentFunctions(global: IGlobalState) {
};
}

function focusElementBeforeOrAfter(global: IGlobalState, frameId: number, i: 1 | -1) {
let firenvimElement;
if (frameId === undefined) {
firenvimElement = getFocusedElement(global.firenvimElems);
} else {
firenvimElement = global.firenvimElems.get(frameId);
}
const originalElement = firenvimElement.getOriginalElement();

const tabindex = (e: Element) => ((x => isNaN(x) ? 0 : x)(parseInt(e.getAttribute("tabindex"))));
const focusables = Array.from(document.querySelectorAll("input, select, textarea, button, object, [tabindex], [href]"))
.filter(e => e.getAttribute("tabindex") !== "-1")
.sort((e1, e2) => tabindex(e1) - tabindex(e2));

let index = focusables.indexOf(originalElement);
let elem: Element;
if (index === -1) {
// originalElement isn't in the list of focusables, so we have to
// figure out what the closest element is. We do this by iterating over
// all elements of the dom, accepting only originalElement and the
// elements that are focusable. Once we find originalElement, we select
// either the previous or next element depending on the value of i.
const treeWalker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_ELEMENT,
{
acceptNode: n => ((n === originalElement || focusables.indexOf((n as Element)) !== -1)
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT)
},
);
const firstNode = treeWalker.currentNode as Element;
let cur = firstNode;
let prev;
while (cur && cur !== originalElement) {
prev = cur;
cur = treeWalker.nextNode() as Element;
}
if (i > 0) {
elem = treeWalker.nextNode() as Element;
} else {
elem = prev;
}
// Sanity check, can't be exercised
/* istanbul ignore next */
if (!elem) {
elem = firstNode;
}
} else {
elem = focusables[(index + i + focusables.length) % focusables.length];
}

index = focusables.indexOf(elem);
// Sanity check, can't be exercised
/* istanbul ignore next */
if (index === -1) {
throw "Oh my, something went wrong!";
}

// Now that we know we have an element that is in the focusable element
// list, iterate over the list to find one that is visible.
let startedAt;
let style = getComputedStyle(elem);
while (startedAt !== index && (style.visibility !== "visible" || style.display === "none")) {
if (startedAt === undefined) {
startedAt = index;
}
index = (index + i + focusables.length) % focusables.length;
elem = focusables[index];
style = getComputedStyle(elem);
}

(document.activeElement as any).blur();
const sel = document.getSelection();
sel.removeAllRanges();
const range = document.createRange();
if (elem.ownerDocument.contains(elem)) {
range.setStart(elem, 0);
}
range.collapse(true);
(elem as HTMLElement).focus();
sel.addRange(range);
}

export function getNeovimFrameFunctions(global: IGlobalState) {
return {
evalInPage: (_: number, js: string) => executeInPage(js),
Expand All @@ -117,6 +201,12 @@ export function getNeovimFrameFunctions(global: IGlobalState) {
(document.activeElement as any).blur();
document.documentElement.focus();
},
focusNext: (frameId: number) => {
focusElementBeforeOrAfter(global, frameId, 1);
},
focusPrev: (frameId: number) => {
focusElementBeforeOrAfter(global, frameId, -1);
},
getEditorInfo: (frameId: number) => global
.firenvimElems
.get(frameId)
Expand Down
18 changes: 18 additions & 0 deletions tests/_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,24 @@ export const testFocusInput = retryTest(withLocalPage("simple.html", async (test
await server.pullCoverageData(frameSocket);
}));

const focusNextPrevTest = async (testTitle: string, server: any, driver: webdriver.WebDriver) => {
const [input, span, frameSocket] = await createFirenvimFor(server, driver, By.id("content-input"));
await sendKeys(driver, ":call firenvim#focus_next()".split("")
.concat(webdriver.Key.ENTER));
await driver.wait(async () => "" !== (await driver.switchTo().activeElement().getAttribute("id")), WAIT_DELAY, "focus_next did not change focused element");
expect(await driver.switchTo().activeElement().getAttribute("id")).toBe("after");
await driver.executeScript(`arguments[0].focus();`, input);
await driver.wait(async () => "after" !== (await driver.switchTo().activeElement().getAttribute("id")), WAIT_DELAY, "Page focus did not change");
await sendKeys(driver, ":call firenvim#focus_prev()".split("")
.concat(webdriver.Key.ENTER));
await driver.wait(async () => "" !== (await driver.switchTo().activeElement().getAttribute("id")), WAIT_DELAY, "focus_prev did not change focused element");
expect(await driver.switchTo().activeElement().getAttribute("id")).toBe("before");
await server.pullCoverageData(frameSocket);
}

export const testFocusNextPrev1 = retryTest(withLocalPage("focusnext.html", focusNextPrevTest));
export const testFocusNextPrev2 = retryTest(withLocalPage("focusnext2.html", focusNextPrevTest));

export const testEvalJs = retryTest(withLocalPage("simple.html", async (testTitle: string, server: any, driver: webdriver.WebDriver) => {
const backup = await readVimrc();
await writeVimrc(`
Expand Down
4 changes: 4 additions & 0 deletions tests/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
testEvalJs,
testFilenameSettings,
testFocusGainedLost,
testFocusNextPrev1,
testFocusNextPrev2,
testForceNvimify,
testGithubAutofill,
testGStartedByFirenvim,
Expand Down Expand Up @@ -166,6 +168,8 @@ describe("Chrome", () => {
t("Force nvimify", testForceNvimify);
t("Input focused after frame", testInputFocusedAfterLeave);
t("FocusInput", testFocusInput);
t("FocusNextPrev1", testFocusNextPrev1);
t("FocusNextPrev2", testFocusNextPrev2);
t("Dynamically created elements", testDynamicTextareas);
t("Dynamically created nested elements", testNestedDynamicTextareas);
t("Large buffers", testLargeBuffers);
Expand Down
4 changes: 4 additions & 0 deletions tests/firefox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
testEvalJs,
testFilenameSettings,
testFocusGainedLost,
testFocusNextPrev1,
testFocusNextPrev2,
testForceNvimify,
testGithubAutofill,
testGStartedByFirenvim,
Expand Down Expand Up @@ -139,6 +141,8 @@ describe("Firefox", () => {
t("Force nvimify", testForceNvimify);
t("Input focused after frame", testInputFocusedAfterLeave);
t("FocusInput", testFocusInput);
t("FocusNextPrev1", testFocusNextPrev1);
t("FocusNextPrev2", testFocusNextPrev2);
t("Dynamically created elements", testDynamicTextareas);
t("Dynamically created nested elements", testNestedDynamicTextareas);
t("Large buffers", testLargeBuffers);
Expand Down
20 changes: 20 additions & 0 deletions tests/pages/focusnext.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en" id="html">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>focus_next()/focus_prev()</title>
<style>
*:focused {
background: green;
}
</style>
</head>
<body id="body">
<a id="before" href="simple.html">before</a>
<a id="invisible" href="simple.html" style="display: none">invisible</a>
<textarea id="content-input" rows="20" cols="80"></textarea>
<a id="nope" href="simple.html" tabindex="-1">nope</a>
<a id="after" href="simple.html">after</a>
</body>
</html>
20 changes: 20 additions & 0 deletions tests/pages/focusnext2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en" id="html">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>2: focus_next()/focus_prev()</title>
<style>
*:focused {
background: green;
}
</style>
</head>
<body id="body">
<a id="before" href="simple.html">before</a>
<a id="invisible" href="simple.html" style="display: none">invisible</a>
<textarea id="content-input" rows="20" cols="80" tabindex="-1"></textarea>
<a id="nope" href="simple.html" tabindex="-1">nope</a>
<a id="after" href="simple.html">after</a>
</body>
</html>

0 comments on commit 7f8c3c3

Please sign in to comment.