monaco-editor icon indicating copy to clipboard operation
monaco-editor copied to clipboard

[Bug] Firefox 131: Selection issues when Monaco is in shadow dom

Open nate-bow-db opened this issue 1 year ago • 10 comments

Reproducible in vscode.dev or in VS Code Desktop?

  • [X] Not reproducible in vscode.dev or VS Code Desktop

Reproducible in the monaco editor playground?

Monaco Editor Playground Link

https://microsoft.github.io/monaco-editor/playground.html?source=v0.51.0#example-creating-the-editor-web-component

Monaco Editor Playground Code

customElements.define(
	"code-view-monaco",
	class CodeViewMonaco extends HTMLElement {
		_monacoEditor;
		/** @type HTMLElement */
		_editor;

		constructor() {
			super();

			const shadowRoot = this.attachShadow({ mode: "open" });

			// Copy over editor styles
			const styles = document.querySelectorAll(
				"link[rel='stylesheet'][data-name^='vs/']"
			);
			for (const style of styles) {
				shadowRoot.appendChild(style.cloneNode(true));
			}

			const template = /** @type HTMLTemplateElement */ (
				document.getElementById("editor-template")
			);
			shadowRoot.appendChild(template.content.cloneNode(true));

			this._editor = shadowRoot.querySelector("#container");
			this._monacoEditor = monaco.editor.create(this._editor, {
				automaticLayout: true,
				language: "html",

				value: `<div>Hello World</div>`,
			});
		}
	}
);
<template id="editor-template">
	<div
		id="container"
		style="overflow: hidden; width: 100%; height: 100%; position: absolute"
	></div>
</template>

<code-view-monaco></code-view-monaco>

Reproduction Steps

  • Monaco is mounted in the shadow dom of a custom element.
  • Browser is Firefox 131 (I used brew install firefox@developer-edition to install)

Actual (Problematic) Behavior

Text selection via mouse does not work correctly, and editing with an active selection has no effect.

https://github.com/user-attachments/assets/026ff3b6-2385-4a94-b7cc-e074b66ebf84

Expected Behavior

Text selection should work normally.

Additional Context

No response

nate-bow-db avatar Sep 12 '24 18:09 nate-bow-db

I did a bit of digging here and it seems related to Firefox updating its implementation of document.caretPositionFromPoint with respect to shadow dom. (Github, Bugzilla)

The updated implementation seems to respect the options parameter (CSSOM).

I made a codepen to demonstrate. If you open this in Firefox 131 you'll notice that clicking on the textarea (which is in the shadow-dom) produces different results depending on whether you include options["shadowRoots"]. Most notably:

If you don't include a reference to the shadow-root in the third parameter, the function will just return the shadow-root.

If you open the codepen in a lower version of Firefox, there's no difference in behavior, and it always returns the textarea even if the shadow-root parameter is omitted. I also noticed that Chrome seems to match FF 131. However, in Monaco Chrome doesn't use the caretPositionFromPoint, it uses caretRangeFromPoint which is not supported by FF.

The caretPositionFromPoint function is used in mouseTarget._doHitTestWithCaretPositionFromPoint here - seems that to continue using this, we'll need to pass in a third parameter with a reference to the shadow-root. It also seems adding it won't have any effect on lower versions of FF.

nate-bow-db avatar Sep 12 '24 22:09 nate-bow-db

I have the same problem, but after the Firefox update, I can't select anything. Not even incorrectly.

danielCCorreia avatar Oct 10 '24 20:10 danielCCorreia

Same for my project :(

evtaranov avatar Oct 10 '24 20:10 evtaranov

Hoping to make a PR to vscode fixing this issue, as we were able to resolve it by patching Monaco for our project. Here's the fix in case anyone has some free time - it's super simple it's just I've never made contributions to vscode so I'm putting it off until I have some free time to setup:

This line here:

const hitResult = document.caretPositionFromPoint(coords.clientX, coords.clientY);

Should instead be:

const shadowRoot = dom.getShadowRoot(ctx.viewDomNode);
const hitResult = shadowRoot ?
    document.caretPositionFromPoint(coords.clientX, coords.clientY, { shadowRoots: [ shadowRoot ] }) :
    document.caretPositionFromPoint(coords.clientX, coords.clientY);

nate-bow-db avatar Oct 11 '24 00:10 nate-bow-db

Having the same issue in the CrowdStrike Advanced Search interface. It is utilizing Monaco, and I cannot select any text. It is completely broken, but just in Firefox (running version 132.0.1)

mitch-n avatar Nov 11 '24 19:11 mitch-n

For anyone that wants to patch the minified version on the fly during a CI step, here is a code snipped that is based on this code (https://github.com/microsoft/monaco-editor/issues/3409#issuecomment-1862083015), which already patches another bug, that probably will never get fixed. All credit goes to @nate-bow-db for posting/finding the solution 😄

const fs = require('fs');

console.log("fix ./node_modules/monaco-editor/min/vs/editor/editor.main.js");
fs.readFile("./node_modules/monaco-editor/min/vs/editor/editor.main.js", function (err, buf) {
    let code = buf.toString();

    //fix for monaco editor (issue https://github.com/microsoft/monaco-editor/issues/3409)
    let rgx = /(.)=>{this\.viewHelper\.viewDomNode\.contains\((.)\.target\)/;
    let newcode = code.replace(rgx, '$1=>{this.viewHelper.viewDomNode.contains($1.composedPath()[0])');

    //fix for firefox 131+ issue (issue: https://github.com/microsoft/monaco-editor/issues/4679#issuecomment-2406284453)
    rgx = /=(.)\.getShadowRoot\((.)\.viewDomNode\)/;
    let matches = newcode.match(rgx);
    rgx = /static _doHitTestWithCaretPositionFromPoint\((.),(.)\)\{const (.)=document\.caretPositionFromPoint\((.).clientX,(.).clientY\);/;
    newcode = newcode.replace(rgx, "static _doHitTestWithCaretPositionFromPoint($1,$2){const shadowRoot=" + matches[1] + ".getShadowRoot($1.viewDomNode);const $3=shadowRoot?document.caretPositionFromPoint($2.clientX,$2.clientY,{shadowRoots:[shadowRoot]}):document.caretPositionFromPoint($2.clientX,$2.clientY);");
    if (code != newcode) {
        console.log("patched monaco editor");
        fs.writeFile("./node_modules/monaco-editor/min/vs/editor/editor.main.js", newcode, (err) => {
            if (err) console.log(err);
        });
    }
});

paresy avatar Nov 11 '24 20:11 paresy

@paresy

For anyone that wants to patch the minified version on the fly during a CI step, here is a code snipped that is based on this code (#3409 (comment)), which already patches another bug, that probably will never get fixed.

yeah, seems to be badly supported

jogibear9988 avatar Nov 11 '24 21:11 jogibear9988

see also: https://bugzilla.mozilla.org/show_bug.cgi?id=1927838

jogibear9988 avatar Nov 20 '24 10:11 jogibear9988

A quick fix for some cases can be:

class MonacoEditor extends HTMLElement {
  constructor() { super(); } get shadowRoot() { return this.d; }

  async connectedCallback() { if (this.connectedCallback.called) return; this.connectedCallback.called = true;
    this.style.display = 'contents';
    this.innerHTML = `<iframe style=width:100%;height:100%;border:0></iframe>`;
    this.f = this.querySelector('iframe');
    this.f.srcdoc = `
      <link rel=stylesheet href=monaco.css>
      <style>body { margin: 0; overflow: hidden; }</style>
      <main id=m style="width:100dvw;height:100dvh"></main>`.trim();
    await new Promise(r => this.f.onload = r);
    const base = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/';
    const doc = this.d = this.f.contentDocument;
    const                 script = doc.createElement('script');
    /**/                  script.src = `${base}${this.version}/min/vs/loader.min.js`;
    /**/  doc.body.append(script);
    await new Promise(r => script.onload = r);
    
    const win = this.w = this.f.contentWindow; this.d.m = win.m;
    win.require.config({ paths: { vs: `${base}${this.version}/min/vs` }});
    win.require(['vs/editor/editor.main'], () => {
      Object.assign(window, { monaco: win.monaco });
      this.editor = this.m = win.monaco.editor.create(win.m, this.config ?? {
        value: 'function hello() {\n  console.log("Hello, world!");  \n  retur "Hello";  \n}',
        language: 'javascript', theme: 'vs-dark',
        placeholder: 'Start coding!',
        automaticLayout: true,
      });
      ['keydown', 'keyup', 'keypress'].forEach(type => 
        win.addEventListener(type, e => ((e.ctrlKey || e.metaKey) && e.key === 'o' ? (e.preventDefault(), this) : this).dispatchEvent(new e.constructor(e.type, e)))
      );
      this.editorReady?.(); // subclass entry point
    });
  }

  get version() { return this.getAttribute('version') ?? '0.52.0'; }  

  get value()  { return this.editor?.getValue();  }
  set value(v) {        this.editor?.setValue(v); }
}

customElements.define('monaco-editor', MonacoEditor);

CetinSert avatar Nov 28 '24 02:11 CetinSert

I also noticed that text selection causes a JS error can't access property "offsetNode", hitResult is null to be thrown in Firefox (currently on v145), and this issue seems to be the root cause. I can reproduce on https://microsoft.github.io/monaco-editor/playground.html?source=v0.55.1#example-creating-the-editor-hello-world by just selecting all text in Monaco:

Image

silverwind avatar Nov 21 '25 18:11 silverwind

I have also noticed the same error on both playground and bundled editor, which happens when selecting text with mouse in the editor and the mouse moves out of the <div class="monaco-scrollable-element editor-scrollable vs" ...>

Uncaught Error: can't access property "offsetNode", i is null

erosman avatar Dec 13 '25 06:12 erosman