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

Open
opened 2014-04-06 23:13:03 +02:00 by marijnh · 27 comments
marijnh commented 2014-04-06 23:13:03 +02:00 (Migrated from gitlab.com)

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.

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.
marijnh commented 2014-04-06 23:42:50 +02:00 (Migrated from gitlab.com)

mentioned in issue #1

mentioned in issue #1
marijnh commented 2014-04-07 11:39:15 +02:00 (Migrated from gitlab.com)

mentioned in issue #2444

mentioned in issue #2444
marijnh commented 2014-04-07 11:45:56 +02:00 (Migrated from gitlab.com)

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).

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).
marijnh commented 2014-04-07 16:16:49 +02:00 (Migrated from gitlab.com)

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. :)

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. :)
marijnh commented 2014-04-07 16:20:36 +02:00 (Migrated from gitlab.com)

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.

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.
marijnh commented 2014-04-07 16:26:20 +02:00 (Migrated from gitlab.com)

changed title from {-.codemirrorparent { transform: rotateY(180deg); } at initi-}ali{-z-}at{-ion results in incorrect initial cursor position-}. to {+Initializing inside a transformed CSS block inv+}ali{+d+}at{+es CodeMirror's positioning assumptions+}.

changed title from **{-`.codemirrorparent { transform: rotateY(180deg); }` at initi-}ali{-z-}at{-ion results in incorrect initial cursor position-}.** to **{+Initializing inside a transformed CSS block inv+}ali{+d+}at{+es CodeMirror's positioning assumptions+}.**
marijnh commented 2014-04-09 05:55:23 +02:00 (Migrated from gitlab.com)

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.

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. ``` js 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: ``` js 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.](http://jsperf.com/transformedcheck)
marijnh commented 2014-04-09 06:12:30 +02:00 (Migrated from gitlab.com)

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

`@schanzer` If you're bored, follow along here. :)
marijnh commented 2014-04-09 18:11:29 +02:00 (Migrated from gitlab.com)

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().

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()`.
marijnh commented 2014-04-09 18:19:57 +02:00 (Migrated from gitlab.com)

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.

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.
marijnh commented 2014-04-09 19:12:55 +02:00 (Migrated from gitlab.com)

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?

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?
marijnh commented 2014-04-09 22:36:12 +02:00 (Migrated from gitlab.com)

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).

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).
marijnh commented 2014-05-13 08:46:11 +02:00 (Migrated from gitlab.com)

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!

another use-case: integrating codemirror into a [reveal.js](https://github.com/hakimel/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!
marijnh commented 2014-06-24 21:03:40 +02:00 (Migrated from gitlab.com)

mentioned in issue #2660

mentioned in issue #2660
marijnh commented 2016-02-16 17:02:11 +01:00 (Migrated from gitlab.com)

mentioned in issue #3827

mentioned in issue #3827
marijnh commented 2018-01-20 22:56:53 +01:00 (Migrated from gitlab.com)

mentioned in issue #5199

mentioned in issue #5199
marijnh commented 2021-01-18 18:39:33 +01:00 (Migrated from gitlab.com)

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.

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: ```javascript $(element).css({ transform: `scale(${x})` }); ``` Then reverse the effect of this on the codemirror cursor by scaling by 1/x: ```javascript $('.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.
marijnh commented 2021-01-27 02:58:45 +01:00 (Migrated from gitlab.com)

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!

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: ```css .CodeMirror-cursors, .CodeMirror-measure:nth-child(2) + div{ transform:scale(1.1); /* Reverse scale from 0.9 */ transform-origin: 0 0; } ``` Hope it helps!
ghost1 commented 2021-11-13 14:11:06 +01:00 (Migrated from gitlab.com)

mentioned in issue #1793

mentioned in issue #1793
marijnh commented 2022-04-07 20:10:01 +02:00 (Migrated from gitlab.com)

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

For what it is worth, using `inputStyle: "contenteditable"` works much better than `inputStyle = "textarea"` when a codemirror editor is inside of a CSS tranform.
marijnh commented 2022-08-19 16:12:05 +02:00 (Migrated from gitlab.com)

Still an issue with CodeMirror 6

Update: found relevant issue in CodeMirror 6. Tl;dr "not supported" 😒

Still an issue with CodeMirror 6 Update: found [relevant issue](https://github.com/codemirror/dev/issues/324) in CodeMirror 6. Tl;dr "not supported" 😒
marijnh commented 2023-10-24 02:45:49 +02:00 (Migrated from gitlab.com)

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

Note: this happens, e.g., with reveal.js when the window size changes #4189 and is quite annoying.
marijnh commented 2023-11-02 22:46:27 +01:00 (Migrated from gitlab.com)

mentioned in issue #53

mentioned in issue #53
marijnh commented 2023-12-01 21:22:58 +01:00 (Migrated from gitlab.com)

mentioned in issue #93

mentioned in issue #93
marijnh commented 2024-02-13 22:18:31 +01:00 (Migrated from gitlab.com)

mentioned in issue #730

mentioned in issue #730
marijnh commented 2024-02-28 22:46:58 +01:00 (Migrated from gitlab.com)

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

Codemirror 6 apparently can handle scale() but not more complex transformations.
marijnh commented 2024-02-28 23:20:21 +01:00 (Migrated from gitlab.com)

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.

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: ```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.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
codemirror/codemirror5#2443
No description provided.