Putting CodeMirror 6 in a ShadowRoot

Running CodeMirror in ShadowDOM ShadowRoot causes the styles not to be applied.

It is tricky to get the styles to apply because they are injected by CodeMirror’s code into the head of the document, from within the CM’s JavaScript code. The package does not include standalone .css files:

Shipping standalone files would make it easy to include the style in a ShadowRoot.

Here is a partial and incomplete way of finding and inserting the dynamic style:

	let cmStyle

	const findCMStyle = () => {
		const style = Array.from(document.head.querySelectorAll('style')).find(n => n.textContent?.includes('.ͼ1'))

		if (style) {
			cmStyle = style.cloneNode(true)
			// ... APPEND STYLE TO SHADOW ROOT ...
		} else requestAnimationFrame(findCMStyle)
	}

	findCMStyle()

However, when this code eventually finds the <style> element, the textContent only has part of the full style, so the style only partially works. For example, at this point, in my case, it is always missing the cm-lintRange and cm-lintRange-error style definitions, so red squiggly lines will not appear in the text.

It appears that the JS code that injects the style, does so piecemeal in some way. Let’s try to observe it.

If at the moment that the <style> is found we add a MutationObserver to try and catch the rest of the textContent by observing mutations, it does not work:

	let cmStyle

	const findCMStyle = () => {
		const style = Array.from(document.head.querySelectorAll('style')).find(n => n.textContent?.includes('.ͼ1'))

		if (style) {
			cmStyle = style.cloneNode(true)
			// ... APPEND STYLE TO SHADOW ROOT ...

			const observer = new MutationObserver(() => {
				console.log('STYLE CHANGED') // THIS NEVER HAPPENS
				cmStyle = style.cloneNode(true)
				// ... REPLACE STYLE IN SHADOW ROOT ...
			})
			observer.observe(style, {characterData: true, subtree: true})

			// ... DON'T FORGET TO CLEAN UP THE OBSERVER ...
		} else requestAnimationFrame(findCMStyle)
	}

	findCMStyle()

I’m not sure why the MutationObserver never fires. Maybe because the JS code is injecting styles via CSSOM and there’s an edge case MutationObserver doesn’t handle? If so, we should report this to Chrome issue tracker.

To work around MutationObserver not being able to detect the textContent change (note that one can see the textContent in the element inspector), using a setTimeout finally does the trick,

	let cmStyle

	const findCMStyle = () => {
		const style = Array.from(document.head.querySelectorAll('style')).find(n => n.textContent?.includes('.ͼ1'))

		if (style) {
			cmStyle = style.cloneNode(true)
			// ... APPEND STYLE TO SHADOW ROOT ...

			setTimeout(() => {
				cmStyle = style.cloneNode(true)
				// ... REPLACE STYLE IN SHADOW ROOT ...
			}, 1000)
		} else requestAnimationFrame(findCMStyle)
	}

	findCMStyle()

but this feels a little more hacky: suppose there’s a race condition for some reason, hopefully 1 whole second is always enough though. It is currently working for me, assuming 1 second is always good.


Can you please ship the .css within the package?

One way to do this is to simply ship the src/ folder with each package, then we can handle transpiling source code any way we want.

Another way to do it is to make a CSS-specific build to output just .css.

Oops, nevermind. I see the root option, after noticing that style-mod has the option. :smiley:

Hmm, when I try to use the root or parent options pointing to a ShadowRoot, I get a runtime error from EditorView:

TypeError: Cannot read properties of null (reading 'anchorNode')
    at DOMSelectionState.eq (index.js:239:42)
    at DOMObserver.readSelectionRange (index.js:5541:33)
    at new DOMObserver (index.js:5483:14)
    at new EditorView (index.js:6041:25)

Are you using @codemirror/view 6.0.1? Which browser does it happen with?