codemirror / codemirror5

In-browser code editor (version 5, legacy)
http://codemirror.net/5/
MIT License
26.84k stars 4.97k forks source link

setCursor doesn't work well with `inputStyle: "contenteditable",` #6167

Closed A-312 closed 4 years ago

A-312 commented 4 years ago

Hi,

When I try to do editor.setCursor({line: 0, ch: 2}) with inputStyle: "contenteditable", setting, the cursor doesn't change this position but this work fine with inputStyle: "editor",.

I'm looking to make a fix/pr, if you have advice about fixing this issue, please feel free to comment.

Example with demo/abc.html :

<!doctype html>

<title>CodeMirror: Any Word Completion Demo</title>
<meta charset="utf-8"/>
<link rel=stylesheet href="../doc/docs.css">

<link rel="stylesheet" href="../lib/codemirror.css">
<link rel="stylesheet" href="../addon/hint/show-hint.css">
<script src="../lib/codemirror.js"></script>
<script src="../addon/hint/show-hint.js"></script>
<script src="../addon/hint/anyword-hint.js"></script>
<script src="../mode/javascript/javascript.js"></script>
<div id=nav>
  <a href="https://codemirror.net"><h1>CodeMirror</h1><img id=logo src="../doc/logo.png"></a>

  <ul>
    <li><a href="../index.html">Home</a>
    <li><a href="../doc/manual.html">Manual</a>
    <li><a href="https://github.com/codemirror/codemirror">Code</a>
  </ul>
  <ul>
    <li><a class=active href="#">Any Word Completion</a>
  </ul>
</div>

<article>
<h2>Native Spellchecker Demo</h2>
<form><textarea id="code" name="code">
Native abc tesst
</textarea></form>

<script>
CodeMirror.commands.autocomplete = function(cm) {
  cm.showHint({hint: CodeMirror.hint.anyword});
}
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
  inputStyle: "contenteditable",
  spellcheck: true,
});
</script>
</article>

(External linked issue : https://github.com/Ionaru/easy-markdown-editor/issues/160 & https://github.com/zestedesavoir/zds-site/issues/5652)

marijnh commented 4 years ago

Works for me in Chrome and Firefox (but since the code you show doesn't call setCursor, I can't be sure that I'm doing the same things that you're doing).

A-312 commented 4 years ago

Works for me in Chrome and Firefox (but since the code you show doesn't call setCursor, I can't be sure that I'm doing the same things that you're doing).

In your webconsole, when you try .setCursor, the cursor will don't move

A-312 commented 4 years ago
<article>
<h2>Native Spellchecker Demo</h2>
<p>Codemirror uses <code>Codemirror.inputStyle: `contenteditable`</code> with <code>spellcheck: true<code>.</p>
<form><textarea id="code" name="code">
Native abc tesst
</textarea></form>
<form><textarea id="code2" name="code2">
No abc, textareaInput
</textarea></form>

<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
  inputStyle: "contenteditable",
  spellcheck: true,
});

var editor2 = CodeMirror.fromTextArea(document.getElementById("code2"), {});
</script>
</article>

editor.setSelection({line:0, ch:2}, {line:0, ch:5}) doesn't work : image

editor2.setSelection({line:0, ch:2}, {line:0, ch:5}) works : image

A-312 commented 4 years ago

Looking for the bug

In dom.js, I add log and trace in range function because it calls the native/browser function to selection the text, I got (line 40):

if (document.createRange) range = function(node, start, end, endNode) {
  console.log("RANGE:")
  console.log(node, start, end, endNode)
  console.trace()
  let r = document.createRange()
  r.setEnd(endNode || node, end)
  r.setStart(node, start)
  return r
}

Output for: editor.setSelection(...)

Args = {line:0, ch:2}, {line:0, ch:5}

Output RANGE: "Native" 5 6 undefined

console.trace (click to open) ``` range @ codemirror.js:90 measureCharInner @ codemirror.js:2493 measureCharPrepared @ codemirror.js:2423 get @ codemirror.js:2654 cursorCoords @ codemirror.js:2666 scrollPosIntoView @ codemirror.js:3436 endOperation_finish @ codemirror.js:3891 endOperations @ codemirror.js:3810 (anonymous) @ codemirror.js:3793 finishOperation @ codemirror.js:2053 endOperation @ codemirror.js:3790 (anonymous) @ codemirror.js:3946 (anonymous) @ codemirror.js:9707 (anonymous) @ VM774:1 ```

Output for: editor2.setSelection(...)

Args = {line:0, ch:2}, {line:0, ch:5}

range function in dom.js is called 3 times with editor.setSelection(...)

console.trace (click to open) ``` editor2.setSelection({line:0, ch:2}, {line:0, ch:5}) codemirror.js:88 RANGE: codemirror.js:89 #text 0 1 undefined codemirror.js:90 console.trace range @ codemirror.js:90 measureCharInner @ codemirror.js:2493 measureCharPrepared @ codemirror.js:2423 measureChar @ codemirror.js:2372 charCoords @ codemirror.js:2631 coords @ codemirror.js:3201 (anonymous) @ codemirror.js:3214 iterateBidiSections @ codemirror.js:319 drawForLine @ codemirror.js:3212 drawSelectionRange @ codemirror.js:3253 prepareSelection @ codemirror.js:3155 TextareaInput.prepareSelection @ codemirror.js:9353 endOperation_R2 @ codemirror.js:3848 endOperations @ codemirror.js:3806 (anonymous) @ codemirror.js:3793 finishOperation @ codemirror.js:2053 endOperation @ codemirror.js:3790 (anonymous) @ codemirror.js:3946 (anonymous) @ codemirror.js:9707 (anonymous) @ VM783:1 codemirror.js:88 RANGE: codemirror.js:89 "abc" 1 2 undefined codemirror.js:90 console.trace range @ codemirror.js:90 measureCharInner @ codemirror.js:2493 measureCharPrepared @ codemirror.js:2423 measureChar @ codemirror.js:2372 charCoords @ codemirror.js:2631 coords @ codemirror.js:3201 (anonymous) @ codemirror.js:3215 iterateBidiSections @ codemirror.js:319 drawForLine @ codemirror.js:3212 drawSelectionRange @ codemirror.js:3253 prepareSelection @ codemirror.js:3155 TextareaInput.prepareSelection @ codemirror.js:9353 endOperation_R2 @ codemirror.js:3848 endOperations @ codemirror.js:3806 (anonymous) @ codemirror.js:3793 finishOperation @ codemirror.js:2053 endOperation @ codemirror.js:3790 (anonymous) @ codemirror.js:3946 (anonymous) @ codemirror.js:9707 (anonymous) @ VM783:1 codemirror.js:88 RANGE: codemirror.js:89 "abc" 2 3 undefined codemirror.js:90 console.trace range @ codemirror.js:90 measureCharInner @ codemirror.js:2493 measureCharPrepared @ codemirror.js:2423 get @ codemirror.js:2654 cursorCoords @ codemirror.js:2666 TextareaInput.prepareSelection @ codemirror.js:9357 endOperation_R2 @ codemirror.js:3848 endOperations @ codemirror.js:3806 (anonymous) @ codemirror.js:3793 finishOperation @ codemirror.js:2053 endOperation @ codemirror.js:3790 (anonymous) @ codemirror.js:3946 (anonymous) @ codemirror.js:9707 (anonymous) @ VM783:1 ```
A-312 commented 4 years ago

2nd looking

Depending of where is the cursor when I do editor.setSelection({line:0, ch:2}, {line:0, ch:5}) and if I do editor.focus() the selection can work:

  1. If my editor (CM) has already the focus, editor.setSelection will do nothing.
  2. If my editor (CM) does not have the focus, editor.setSelection will work if I use editor.focus() after.

I try to set the focus out with code: document.getElementsByTagName('A')[0].focus() and use editor.setSelection after, this will don't work.

Output for 2. focus out before setSelection()

If my editor (CM) does not have the focus, editor.setSelection will work if I use editor.focus() after.

editor.setSelection({line:0, ch:2}, {line:0, ch:5})

Output RANGE: "Native" 5 6 undefined

console.trace (click to open) ``` range @ codemirror.js:90 measureCharInner @ codemirror.js:2493 measureCharPrepared @ codemirror.js:2423 get @ codemirror.js:2654 cursorCoords @ codemirror.js:2666 scrollPosIntoView @ codemirror.js:3436 endOperation_finish @ codemirror.js:3891 endOperations @ codemirror.js:3810 (anonymous) @ codemirror.js:3793 finishOperation @ codemirror.js:2053 endOperation @ codemirror.js:3790 (anonymous) @ codemirror.js:3946 (anonymous) @ codemirror.js:9707 (anonymous) @ VM1819:1 ```

editor.focus()

Output RANGE: "Native" 2 5 "Native"

console.trace (click to open) ``` range @ codemirror.js:90 ContentEditableInput.showPrimarySelection @ codemirror.js:8858 ContentEditableInput.showSelection @ codemirror.js:8818 ContentEditableInput.focus @ codemirror.js:8909 focus @ codemirror.js:8210 (anonymous) @ VM1826:1 ```
A-312 commented 4 years ago

Looking how .focus() work

https://github.com/codemirror/CodeMirror/blob/28568ba037146b2d13857849650db94b8ec09385/src/input/ContentEditableInput.js#L196-L202

When I comment if (!this.selectionInEditor()), focus will refresh selection each time and getSelection on 1. or 2. will work if I do .focus() after.

Now, I think I know why editor2.getSelection work and not editor.getSelection, it seem showSelection is never called with editor.getSelection.

Looking in the refresh/render function after a change

Ok...

This is the render loop: https://github.com/codemirror/CodeMirror/blob/28568ba037146b2d13857849650db94b8ec09385/src/display/operations.js#L56-L68

showSelection (I mean cm.display.input.showSelection to his closest friends) is called in endOperation_W2 if op.selectionChanged is true.

https://github.com/codemirror/CodeMirror/blob/28568ba037146b2d13857849650db94b8ec09385/src/display/operations.js#L119-L120

In endOperation_R2 function :

https://github.com/codemirror/CodeMirror/blob/28568ba037146b2d13857849650db94b8ec09385/src/display/operations.js#L104-L105

op.selectionChanged is defined in startOperation:

https://github.com/codemirror/CodeMirror/blob/28568ba037146b2d13857849650db94b8ec09385/src/display/operations.js#L23-L42

Next step

I have to understand why startOperation is not called or why startOperation is called with selectionChanged is false.

Hack for my code (temporary solution)

Now I understood, i can make a hack (temporary solution) for my code:

I find:

editor.setSelection({line:0, ch:2}, {line:0, ch:5})
editor.display.input.showSelection(editor.display.input.prepareSelection(), true)
A-312 commented 4 years ago

3rd looking

Not usefull I saw on my last looking that `selectionChanged` is not `true`, I will looking why looking the *tree* of `setCursor` and `setSelection` ## Looking of setSelection declaration https://github.com/codemirror/CodeMirror/blob/28568ba037146b2d13857849650db94b8ec09385/src/model/Doc.js#L119-L124 Looking where `setSimpleSelection` is set... https://github.com/codemirror/CodeMirror/blob/28568ba037146b2d13857849650db94b8ec09385/src/model/Doc.js#L20 `setSimpleSelection` is set in `"./selection_updates.js"`. https://github.com/codemirror/CodeMirror/blob/28568ba037146b2d13857849650db94b8ec09385/src/model/selection_updates.js#L95-L99 `selectionChanged` is always true after setSelection, checked with : `console.log(doc.cm ? doc.cm.curOp.selectionChanged : 'nop')`

I have to understand why startOperation is not called or why startOperation is called with selectionChanged is false.

Ok... I did a mistake, I forget to watch the value of selectionChanged (I thought I did it).

Problem: showSelection

https://github.com/codemirror/CodeMirror/blob/28568ba037146b2d13857849650db94b8ec09385/src/display/operations.js#L118-L120

showSelection doesn't work when takeFocus is false. 😕

editor.setSelection({line:0, ch:2}, {line:0, ch:5}) works fine if I replace takeFocus by true.

Caused by: https://github.com/codemirror/CodeMirror/commit/b9e0c4cd0bab86a440cb10e3505bd6691bb0f001 linked to #3979 in #9 (https://github.com/notepadqq/CodeMirror/pull/9/commits/b9e0c4cd0bab86a440cb10e3505bd6691bb0f001)

A-312 commented 4 years ago

The condition op.focus == activeElt() seems that it will be always false because op.focus is boolean and activeElt() returns an HTMLElement.

HTMLElement == true
false
HTMLElement == false
false
document.body == true
false
document.body == false
false

activeElt() always return an HTMLElement. https://github.com/codemirror/CodeMirror/blob/28568ba037146b2d13857849650db94b8ec09385/src/util/dom.js#L67-L80

I think this code is not necessary, because it is already tested in L110 in showSelection:

https://github.com/codemirror/CodeMirror/blob/28568ba037146b2d13857849650db94b8ec09385/src/input/ContentEditableInput.js#L108-L112

marijnh commented 4 years ago

Ah, right, the issue only occurs when the editor has focus within the window, but the window doesn't itself have focus.

I looked at your patch but I think there's a much less invasive way to fix this. See attached patch. Does that work for you?

A-312 commented 4 years ago

My change is invasive but needed and it doesn't make depreciation and don't not changz the behavior.

I need to test but in first looking (i just reviewed not test), that doesn't sound perfect with no-focus. Without calling showPrimarySelection the selection will be not processing because the selection can appear without focus.

marijnh commented 4 years ago

I'd prefer that you actually test. It can be painful to give up one's patch, but I really much prefer the low-impact version of the fix.

A-312 commented 4 years ago

I try with:

abc.html (click to open) ```html CodeMirror: Native Spellchecker Demo

Native Spellchecker Demo

This editor uses Codemirror.inputStyle: 'contenteditable' and spellcheck: true.

Default Editor Mode

The native spellchecker (of the browser) does not run with Codemirror.inputStyle: 'textarea' (default value).

```

image2 2

Like I say, in contenteditableInput the selection appear only when the textarea have focus but in textareaInput the selection always appear (with or without focus). I think you would the same behavior depending of the Style.

A-312 commented 4 years ago

(I got a bug with editor2.focus(), it seems to not work, I mean I can't write directly after I got the focus with this function in editor2)

marijnh commented 4 years ago

Like I say, in contenteditableInput the selection appear only when the textarea have focus

That's just how the native selection works, so it is kind of implied in the decision to use the native selection.

A-312 commented 4 years ago

Can you publish your change on npm ?

marijnh commented 4 years ago

CodeMirror uses a monthly release cycle. There'll be a new release around the 20th of the month.

A-312 commented 4 years ago

@marijnh I tried your code on master of Easymde: My change works fine, your change seem to doesn't work, I let you try:

zds.html (click to open) ```html Example / Preview ```

My change (PR #6168) :

image2 2

Your commit/master branch :

I add a color change, to show you that it is been the last version of codemirror in git (not the old one).

image2 2

A-312 commented 4 years ago

On my PR, i did 4 commits :

image

Each commit is one step, you can try each commit step by step.

  1. f0eccc8e3e24e5220d5f26b4d6fca12fa1bbb903 don't change the behavior for user, I explained why, here. It is only code review to delete unused code.
  2. c8f0a0e4a79dae7d5b81e0e3a3c410f0c122fdf4 Demo page for documentation if people want to try (I can remove this commit if you want).
  3. 8c07543296063051d9c48c0d8e1968af1ea0a5b4 My fix.
  4. a2a32984771c232773159a6dca8ba92e58f00972 A new setting (I can remove this commit if you want).
A-312 commented 4 years ago

(If my change need to be enabled with option tell me I will make this changes).

marijnh commented 4 years ago

It is only code review to delete unused code.

That isn't actually unused code. The focus field in an operation will hold false or a DOM node.

Your fix seems to unconditionally mess with the DOM selection, which is not a good idea (when the user doesn't have the editor focused, but for some reason its content is changed, that'll disrupt the selection).

So, again, I don't want to merge these changes.

Running just CodeMirror, without any external code, I can really verify that the problem is gone with my patch. If can set up a demo without your codebase that still shows the problem, let me know, but for now I'm closing this.

A-312 commented 4 years ago

Running just CodeMirror, without any external code, I can really verify that the problem is gone with my patch. If can set up a demo without your codebase that still shows the problem, let me know, but for now I'm closing this.

@marijnh Please, I haven't finished yet, can you reopen ? I can prove it without easymde (I wanted to save some time with remaking an existing script). I will not speak again of easymde, here.

Why am I sure?

  1. The behavior of contenteditableInput and textareaInput are different ;
  2. I took the time to read your code, watch how it works ;
  3. The selection is not processed when we have no focus in contenteditableInput:

    Without calling showPrimarySelection the selection will be not processing because the selection can appear without focus.

My example with only codemirror:

var editor = CodeMirror.fromTextArea(document.getElementById('code'), {
  inputStyle: 'contenteditable',
  spellcheck: true
})

document.getElementById('icanproveit').addEventListener('click', function () {
  const cm = editor;
  const startPoint = cm.getCursor('start');
  const endPoint = cm.getCursor('end');
  cm.replaceSelection('**' + cm.getSelection() + '**');
  startPoint.ch += 2;
  endPoint.ch += 2;
  cm.setSelection(startPoint, endPoint)
  cm.focus();
})

My full page to test:

abc.html (click to open) ```html CodeMirror: Native Spellchecker Demo

Native Spellchecker Demo

This editor uses Codemirror.inputStyle: 'contenteditable' and spellcheck: true.

Default Editor Mode

The native spellchecker (of the browser) does not run with Codemirror.inputStyle: 'textarea' (default value).

```

Result:

work

doesn't work

marijnh commented 4 years ago

I see—that's a different issue from the one before, though. Attached patch seems to help with that.

A-312 commented 4 years ago

This work fine thanks you!