glacambre / firenvim

Embed Neovim in Chrome, Firefox & others.
GNU General Public License v3.0
4.7k stars 145 forks source link

Firenvim is over other items when unfocused #427

Open NilsIrl opened 4 years ago

NilsIrl commented 4 years ago

https://peertube.co.uk/videos/watch/993f6c52-cf25-4d4b-a8da-5490d8e63234 (https://www.codingame.com/)

Would a fix be that firenvim has the same z-index (or same + 1) as the textarea it is dealing about so that it stays just above it but below everything else that is above the textarea?

glacambre commented 4 years ago

I tried to fix this issue by applying the following patch to 5d6ab83df3f78b3678f9dffd2337d7e8d2338c65:

diff --git a/src/FirenvimElement.ts b/src/FirenvimElement.ts
index 99b3d66..8ac4bb0 100644
--- a/src/FirenvimElement.ts
+++ b/src/FirenvimElement.ts
@@ -2,7 +2,7 @@ import * as browser from "webextension-polyfill";
 import { isFirefox } from "./utils/utils";
 import { AbstractEditor } from "./editors/AbstractEditor";
 import { getEditor } from "./editors/editors";
-import { computeSelector } from "./utils/CSSUtils";
+import { computeSelector, computeZIndex } from "./utils/CSSUtils";

 export class FirenvimElement {

@@ -105,6 +105,26 @@ export class FirenvimElement {
         this.span.attachShadow({ mode: "closed" }).appendChild(this.iframe);
         this.getElement().ownerDocument.body.appendChild(this.span);

+        const onFocusChange = ((e: Event) => {
+            const focused = document.activeElement === this.span
+                            || document.activeElement === this.iframe;
+            browser.runtime.sendMessage({
+                args: {
+                    frameId: this.frameId,
+                    message: {
+                        args: [focused],
+                        funcName: ["setFocused"],
+                    }
+                },
+                funcName: ["messageFrame"],
+            });
+            this.setFrameWindowFocused(focused);
+        }).bind(this);
+        this.iframe.addEventListener("focus", onFocusChange);
+        this.iframe.addEventListener("blur", onFocusChange);
+        this.span.addEventListener("focus", onFocusChange);
+        this.span.addEventListener("blur", onFocusChange);
+
         this.focus();

         // We want to remove the frame from the page if the corresponding
@@ -165,6 +185,7 @@ export class FirenvimElement {
             this.getElement().removeEventListener("focus", refocus);
         }, 100);
         refocus();
+        this.setFrameWindowFocused(true);
     }

     focusOriginalElement (addListener: boolean) {
@@ -212,14 +233,13 @@ export class FirenvimElement {
     putEditorAtInputOrigin () {
         const rect = this.editor.getElement().getBoundingClientRect();
         // Save attributes
-        const posAttrs = ["left", "position", "top", "zIndex"];
+        const posAttrs = ["left", "position", "top"];
         const oldPosAttrs = posAttrs.map((attr: any) => this.iframe.style[attr]);

         // Assign new values
         this.iframe.style.left = `${rect.left + window.scrollX}px`;
         this.iframe.style.position = "absolute";
         this.iframe.style.top = `${rect.top + window.scrollY}px`;
-        this.iframe.style.zIndex = "2147483645";

         // Compare, to know whether the element moved or not
         const posChanged = !!posAttrs.find((attr: any, index) =>
@@ -253,6 +273,14 @@ export class FirenvimElement {
         return { dimChanged, newRect: rect };
     }

+    setFrameWindowFocused (focused: boolean) {
+        if (focused) {
+            this.iframe.style.zIndex = "2147483645";
+        } else {
+            this.iframe.style.zIndex = `${computeZIndex(this.getElement()) + 1}`;
+        }
+    }
+
     setPageElementContent (text: string) {
         this.editor.setContent(text);
         [
diff --git a/src/NeovimFrame.ts b/src/NeovimFrame.ts
index 80f1f86..c29adff 100644
--- a/src/NeovimFrame.ts
+++ b/src/NeovimFrame.ts
@@ -14,6 +14,13 @@ browser
 const connectionPromise = browser.runtime.sendMessage({ funcName: ["getNeovimInstance"] });
 const settingsPromise = browser.storage.local.get("globalSettings");

+function setFocusedStyle() {
+    document.documentElement.style.opacity = "1";
+}
+function setBluredStyle() {
+    document.documentElement.style.opacity = "0.5";
+}
+
 window.addEventListener("load", async () => {
     try {
         const host = document.getElementById("host") as HTMLPreElement;
@@ -67,6 +74,13 @@ window.addEventListener("load", async () => {
                 const nRows = Math.floor(height / cellHeight);
                 nvim.ui_try_resize_grid(getGridId(), nCols, nRows);
                 page.resizeEditor(nCols * cellWidth, nRows * cellHeight);
+            } else if (request.funcName[0] === "setFocused") {
+                if (request.args[0]) {
+                    setFocusedStyle();
+                    keyHandler.focus();
+                } else {
+                    setBluredStyle();
+                }
             }
         });

@@ -235,15 +249,15 @@ window.addEventListener("load", async () => {
             }
         });
         // Let users know when they focus/unfocus the frame
-        function setFocusedStyle() {
-            document.documentElement.style.opacity = "1";
-        }
-        function setBluredStyle() {
-            document.documentElement.style.opacity = "0.5";
-        }
-        window.addEventListener("focus", setFocusedStyle);
-        window.addEventListener("blur", setBluredStyle);
-        window.addEventListener("focus", () => keyHandler.focus());
+        window.addEventListener("focus", () => {
+            setFocusedStyle();
+            keyHandler.focus();
+            page.setFocused(true);
+        });
+        window.addEventListener("blur", () => {
+            setBluredStyle();
+            page.setFocused(false);
+        });
         keyHandler.focus();
         setTimeout(() => keyHandler.focus(), 10);
     } catch (e) {
diff --git a/src/page/functions.ts b/src/page/functions.ts
index 6ad552d..ab95898 100644
--- a/src/page/functions.ts
+++ b/src/page/functions.ts
@@ -99,5 +99,8 @@ export function getFunctions(global: IGlobalState) {
         setElementCursor: (frameId: number, line: number, column: number) => {
             return global.firenvimElems.get(frameId).setPageElementCursor(line, column);
         },
+        setFocused: (frameId: number, focused: boolean) => {
+            return global.firenvimElems.get(frameId).setFrameWindowFocused(focused);
+        },
     };
 }
diff --git a/src/utils/CSSUtils.ts b/src/utils/CSSUtils.ts
index 04fa54f..bbf6400 100644
--- a/src/utils/CSSUtils.ts
+++ b/src/utils/CSSUtils.ts
@@ -114,3 +114,12 @@ export function toCss(highlights: HighlightArray) {
         `.${toHighlightClassName(id)}{background: ${elem.background || bg};color:${elem.foreground || fg};font-style:${elem.italic ? "italic" : "normal"};font-weight:${elem.bold ? "bold" : "normal"};text-decoration-line:${(elem.undercurl || elem.underline) ? "underline" : (elem.strikethrough ? "line-through" : "none")};text-decoration-style:${elem.undercurl ? "wavy" : "solid"};}`
         , "");
 }
+
+export function computeZIndex(e: HTMLElement, base: number = 0): number {
+    if (e === undefined || e === null) {
+        return base;
+    }
+    const index = parseInt(window.getComputedStyle(e)
+        .getPropertyValue("z-index"), 10);
+    return computeZIndex(e.parentElement, base + (isNaN(index) ? 0 : index));
+}

While this correctly places the iframe on the z-index closest to that of the editor when Firenvim is unfocused, this doesn't actually fix the problem as z-indexes are actually relative within an element. This means that for the following document:

+-----------------------------------------------------------------------------+
| body: z-index 0                                                             |
| +-------------------------------------------------------------------------+ |
| | div: z-index 10                                                         | |
| | +---------------------------------------------------------------------+ | |
| | | p: z-index 1000                                                     | | |
| | +---------------------------------------------------------------------+ | |
| | +---------------------------------------------------------------------+ | |
| | | textarea: z-index 0                                                 | | |
| | +---------------------------------------------------------------------+ | |
| +-------------------------------------------------------------------------+ |
| +-------------------------------------------------------------------------+ |
| | firenvim iframe                                                         | |
| +-------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------+

Using a z-index of 9 will place the iframe behind the div, but using 10 or 11 will place it over both textarea and p.

This problem can only be solved by placing the iframe in div, right after textarea but we can't do that because this would disturb the page (lots of bad js rely on childElements, nextSibling, CSS relies on ~, :first, :nth-of-type…).

So what we would need is a way to insert elements in the DOM without it being seen by the page at all. This is in theory what the shadow dom lets you do, but since you can't have an element that contains both shadow and non-shadow elements, we would need to create and insert a new node in the DOM.

In conclusion, I don't think I can solve this for now :/.