codemirror/codemirror5

Initializing inside a transformed CSS block invalidates CodeMirror's positioning assumptions.

Opened this issue ยท 17 comments

CodeMirror assumes that it is being initialized in a non-transformed state. These assumptions have bearings on a lot of math within the editor for handling layout positioning.

Yes, this is a known limitation. The editor assumes it is not rotated. I think in this case, simply calling .refresh() after you've finished rotating it back should fix the problem (it does when I do it from the console).

I'm going to operate under the assumption that initializing CodeMirror while transformed doesn't work in most cases.

  1. Would you like CodeMirror to support being correctly initialized while transformed?
  2. Is there a code-weight or code-complexity cost that makes it not worth addressing?

(I appreciate that there is a .refresh() workaround, but the "jump" when calling on completion of the animation is less than desirable.)

I'm not yet sure if I'm volunteering to do this, but I'm thinking about it. :)

Making initialization while transformed work would require making actual editing work while transformed. This would violate a bunch of assumptions (such as that lines stretch along the y axis), and though it would be cute to be able to edit at 45 degrees, I do not feel that this feature would be worth the added complexity -- which would be quite a lot.

Still, if you think you have a sane solution to the problem, feel free to propose it.

I'm going to document everything I learn about this here so that it exists somewhere other than in my head.

There is no way to detect a transformation using just getBoundingClientRect(). It has to be paired with a "canary" element to test the intrinsic attributes.

var canary = document.createElement('div');
canary.style.position = "absolute";
cm.appendChild(canary);

var rect;
canary.style.right = "0px";
canary.style.bottom = "0px";

var calculated = {};
calculated.height = canary.offsetTop;
calculated.width = canary.offsetLeft;

rect = canary.getBoundingClientRect();
calculated.right = rect.right;
calculated.bottom = rect.bottom;

canary.style.right = "auto";
canary.style.bottom = "auto";
canary.style.top = "0px";
canary.style.left = "0px";

rect = canary.getBoundingClientRect();
calculated.top = rect.top;
calculated.left = rect.left;

var parent = cm.getBoundingClientRect();

var properties = ['height', 'width', 'right', 'bottom', 'top', 'left'];
var transformed = properties.some(function(property) {
  return calculated[property] !== parent[property];
});

The alternative to this is to traverse all the way up the DOM tree:

var computedStyle;
var node = cm.parentNode;
var properties = ['transform', 'webkitTransform', 'mozTransform', 'oTransform', 'msTransform'];
var transformed = false;

if (getComputedStyle) {
  while (!transformed && cm.parentNode) {
    computedStyle = getComputedStyle(node);
    transformed = properties.some(function(property) {
      return computedStyle[property] && ~computedStyle[property].indexOf('matrix');
    });
    node = node.parentNode;
  }
}

We might be able to avoid actually processing everything through a matrix transformation if we are exceptionally clever with the first approach. Alternatively, running all positioning math through a matrix transformation would be guaranteed correct.

jsPerf says to use the second solution.

@schanzer If you're bored, follow along here. :)

The root cause is that the response returned from getBoundingClientRect() is not aware of transformations, it is based upon the bounding box of the element's location on the screen.

The strategy for CodeMirror to reach transform independence can therefore be accomplished in one of two ways:

  1. Replace all calls to getBoundingClientRect() with offset-based calculations in order to understand the intrinsic size (prior to transform).
  2. Create a new getIntrinsicBoundingClientRect() that returns information as if it weren't transformed and replace all calls to getBoundingClientRect() with it.

The second solution would theoretically be an almost drop-in solution while the first seems more reliable long-term but far more invasive.

@marijnh Do you have any preferences or thoughts?

Edit: Also need to account for getClientRects().

My main concern is performance. Something like doing multiple calls to getBoundingClientRect and messing with the element's style in between is definitely not going to work (it'll force multiple relayouts). Measuring of layout is already a major cost in CodeMirror. Also, there are several places where the fractional results returned by getBoundingClientRect (but not by offset properties) are required for correct operation.

Needing getBoundingClientRect's precision probably means that the first solution would only ever get us part of the way there and that the second solution would be necessary in every scenario. Since that is the case and the second solution is relatively non-invasive I'm going to take a swing at building getIntrinsicBoundingClientRect and getIntrinsicClientRects.

If we assume that the transformation is static then any performance hit would be on initialize and every other call can use the already-calculated transformation matrix. This would add one matrix multiplication operation to every call to getBoundingClientRect which, fully segregated from the the DOM, should be below our threshold of caring.

Without the assumption that the transformation is static, every call to getIntrinsicBoundingClientRect would require recalculating that matrix. This would trigger the DOM tree parent walk from my above comment (except always traversing to the root node), which will have some performance impact. These would still all be reads without interlaced DOM writes so it wouldn't trigger relayout, but it's still going to be heavier than simply saving off the transformation matrix.

If it all works my proposal would be that CodeMirror adopt getIntrinsic... with a static transformation matrix calculation by default and create an option to opt in to calculating the transformation matrix on every call.

Sound reasonable?

Only having this recomputed on refresh() would be perfectly okay -- that's how CodeMirror treats other CSS as well (changing the font size will screw up your editor until you refresh it).

another use-case: integrating codemirror into a reveal.js presentation for live-coding. Reveal.js automatically scales presentation based on the window size.

Thus, I think it is a pretty useful feature. Thanks a lot for looking into this!

For anyone that wants to display a transformed codemirror, here is a partial workaround:
reverse-transform the codemirror cursor by the inverse of the overall transform.

That is, if you have something like this that transforms some codemirrors:

$(element).css({
    transform: `scale(${x})`
});

Then reverse the effect of this on the codemirror cursor by scaling by 1/x:

$('.CodeMirror-cursors').css({
    transform: `scale(${1/x})`,
    transformOrigin: '0 0'
});

This will put the cursor in the right place again. However, the codemirror-sizer element for some reason still has issues, so the code and cursors will display fine, but the size of the code mirror box will be off. You'll get some blank space or a scroll bar that does nothing.
Setting min-width to 0 on the codemirror-sizer fixes that until .refresh() is called or the codemirror is focused or edited.

Hello,

Just giving my two cents on that issue since I experienced the same problem. Based on the answer from @robertstrauss, here is a code to fix the mouse cursor position and also user selected code bg/position:

.CodeMirror-cursors,
.CodeMirror-measure:nth-child(2) + div{
    transform:scale(1.1); /* Reverse scale from 0.9 */
    transform-origin: 0 0;
}

Hope it helps!

For what it is worth, using inputStyle: "contenteditable" works much better than inputStyle = "textarea" when a codemirror editor is inside of a CSS tranform.

inca commented

Still an issue with CodeMirror 6

Update: found relevant issue in CodeMirror 6. Tl;dr "not supported" ๐Ÿ˜’

kno10 commented

Note: this happens, e.g., with reveal.js when the window size changes #4189
and is quite annoying.

Codemirror 6 apparently can handle scale() but not more complex transformations.

Possible more robust workaround (based on @acf-extended, but using more reliable addressing of containers than jQuery) for CodeMirror 5, here for use with reveal.js:

Reveal.on( 'resize', (event) => {
  let scale="scale("+(1/event.scale)+")";
  document.getElementsByClassName('CodeMirror').forEach((x)=>{
    x.CodeMirror.display.cursorDiv.style.transform=scale;
    x.CodeMirror.display.cursorDiv.style.transformOrigin="0 0 0";
    x.CodeMirror.display.selectionDiv.style.transform=scale;
    x.CodeMirror.display.selectionDiv.style.transformOrigin="0 0 0";
})});

Multi-line selection appears to be still unreliable, it may assume a wrong line height due to the scaling.