12
votes

I'm trying to set up an Ace editor with only one single line of text.

The idea is to mimic the behaviour of an <input type="text"> box, but with syntax colouring: enter image description here

Currently if the user presses Enter while in the editor, it creates a new line: enter image description here

So my question is:

How can I set up Ace to allow one single line only, like a standard text input box?

Below is what I've tried so far, and the reasons why it didn't succeed.

  • Calling editor.undo() on change if e.lines.length > 1

    The problem is, change gets triggered before the actual change is applied in the deltas, so the undo() doesn't work here (or it concerns the previous delta)

  • Cancelling keypress if Event.which = 13

    It kind of works but is very dirty, and it does not handle the case where multiple-lines text is pasted, so we'd need to handle paste event as well - which would make this solution even dirtier. I'm also pretty confident there would be even more edge cases to take into account.

  • Trying to "empty" e in on("change", function(e) { ... })

    For instance, saying that e = {} in the callback function, provided that e is just a reference to the actual object. No effect whatsoever.

  • Trying to find a built-in parameter in Ace editor to do that

    No success in finding such a parameter yet...

4
e is a reference, so changing it wont affect the original event. Have you tried just modifying whichever property on e holds the new text?Kyeotic
Sounds like you should list for the Enter event on the element and cancel it, it is not dirty as you are wanting to stop Enter being used. On paste, just stripe the line separators out of the string and the end user will catch on pretty quick. I don't see much more edge cases than typing/pasting.SamV
Strip out newlines when loading the element; keypress handler seems right. Coincidentally I am doing something similar w/ a different editor component.Dave Newton
@DaveNewton that is what I end up doing in the fiddle. For some reason stopPropagation and preventDefault doesnt workvittore
Thanks to all. It appears that if change effectively gets triggered before the delta is inserted in the undo chain, it's nevertheless triggered after the change has been made into the editor - so @vittore 's solution works like a charmJivan

4 Answers

7
votes

For some reason neither e.preventDefault nor e.stopPropagation works in change event handler. But you can do find-replace.

See fiddle: http://jsfiddle.net/vittore/3rLfdtxb/

 var editor = ace.edit("editor");
 editor.setTheme("ace/theme/monokai");
 editor.getSession().setMode("ace/mode/javascript");
 editor.setFontSize(30)
 editor.getSession().on('change', function(e) {
    console.log(e)
    if (e.data.text.charCodeAt(0) === 10 && e.data.action == "insertText") {
      console.log('cancel event')
      //e.preventDefault() // doesnt work
      //e.stopPropagation()  // doesnt work
      editor.find(String.fromCharCode(10))
      editor.replaceAll(''); // this work
    }
 })

You can even remove if statement from handler and replace line break on any change, regardless.

When you find-replace in change, you got line from cursor to the end of line selected. In order to deselect it after that use :

editor.selection.clearSelection()
12
votes

you can use the following code to make editor behave similar to input type="text" (mostly taken from https://github.com/ajaxorg/ace/blob/v1.2.0/demo/kitchen-sink/layout.js#L103)

var el = document.getElementById("textbox")
var editor = ace.edit(el);
editor.setOptions({
    maxLines: 1, // make it 1 line
    autoScrollEditorIntoView: true,
    highlightActiveLine: false,
    printMargin: false,
    showGutter: false,
    mode: "ace/mode/javascript",
    theme: "ace/theme/tomorrow_night_eighties"
});
// remove newlines in pasted text
editor.on("paste", function(e) {
    e.text = e.text.replace(/[\r\n]+/g, " ");
});
// make mouse position clipping nicer
editor.renderer.screenToTextCoordinates = function(x, y) {
    var pos = this.pixelToScreenCoordinates(x, y);
    return this.session.screenToDocumentPosition(
        Math.min(this.session.getScreenLength() - 1, Math.max(pos.row, 0)),
        Math.max(pos.column, 0)
    );
};
// disable Enter Shift-Enter keys
editor.commands.bindKey("Enter|Shift-Enter", "null")
#textbox {
    font-size: 30px;
    border:solid 2px gray;
}
body{
   background: #161619;
   padding: 40px 20px
}
<script src="https://ajaxorg.github.io/ace-builds/src/ace.js"></script>


<div id=textbox>var a = 1</div>
0
votes

Both of the existing answer where very helpful in getting this working for me, but I still encountered some issues I had to address (some apparently due to api changes). This is the combination of both answers that is working for me.

Note, when you run these snippets or a jsFiddle for example, a CORS check will prevent the worker from loading so, just because the example "works" doesn't mean it will all work when integrated into your project.

Also note... this is react code using hooks, but the basic code should apply.

It also uses resizing code from Set width of ace editor instance according to the length of characters in it

const editDiv = useRef<HTMLDivElement>(null);

useEffect(() => {
    if (editing) {
      if (!editor && editDiv.current) {
        editDiv.current.textContent = value;
        const ed = ace.edit(editDiv.current);
        ed.setOptions({
          maxLines: 1,
          autoScrollEditorIntoView: true,
          highlightActiveLine: false,
          printMargin: false,
          showGutter: false,
          enableLiveAutocompletion: true,
          enableBasicAutocompletion: true,
          enableSnippets: false,
          mode: "ace/mode/javascript",
          theme: "ace/theme/tomorrow_night_eighties"
        });
        ed.commands.bindKey(
          "Up|Ctrl-P|Down|Ctrl-N|PageUp|PageDown",
          "null"
        );
        ed.commands.addCommand({
          name: "SaveOnEnter",
          bindKey: {
            win: "Enter",
            mac: "Enter",
            sender: "editor|cli"
          },
          exec: () => {
            setValue(ed.getValue());
            // Handle new value;
          }
        });
        ed.on("paste", e => {
          e.text = e.text.replace(/[\r\n]+/g, " ");
        });
        setEditor(ed);

        ed.getSession().on("change", e => {
          if (e.action == "insert" && ed.session.getValue().includes("\n")) {
            setTimeout(() => {
              // doing a replaceAll during a change event causes issues in the
              // worker, so we'll queue up the change
              ed.find(String.fromCharCode(10));
              ed.replaceAll("");
              ed.selection.clearSelection();
            }, 0);
          }
        });
        ed.renderer.on("beforeRender", (e, renderer) => {
          const rSession = renderer.session;
          const text = rSession.getLine(0);
          const charCount = rSession.$getStringScreenWidth(text)[0];
          const width =
            Math.max(charCount, 2) * renderer.characterWidth + // text size
            2 * renderer.$padding + // padding
            2 + // little extra for the cursor
            0; // add border width if needed

          renderer.container.style.width = width + "px";
          renderer.onResize(false, 0, width, renderer.$size.height);
        });
        ed.setWrapBehavioursEnabled(false);
        ed.session.setUseWrapMode(false);
        ed.focus();
      }
    } else {
      if (editor) {
        editor.renderer.on("beforeRender", () => {});
        editor.destroy();
        setEditor(undefined);
      }
    }
  }, [editing]);

  return <div ref={editDiv} style={{ width: "1em", height: "1.2em" }} />;

0
votes

Try provide maxLines set to 1 in the setOptions property. I use react-ace version 9.2.0. I use this as an optional/configurable behavior, with an exposed property on wrapper component: maxLines={this.props.singleLine ? 1 : undefined}