Search code examples
javascriptjqueryhtmlcodemirroralt

Insert Text at Caret on CodeMirror Textarea


I am working on a Python editor and would like to add a feature to insert text at the position of the caret on a CodeMirror textarea.

There is a series of pictures which can be clicked. When one is clicked, the alt attribute of that picture gets saved, and then when you click again inside the textarea it gets copied to your mouse position (a demo fiddle: https://jsfiddle.net/t0k7yp7n/1/)

Here is a script for the text insertion part:

selected = '';

$('.insert').click(function() {
    console.log($(this).attr('alt'));
    selected = $(this).attr('alt');
});

$('#textbox').click(function() {
    insertAtCaret('textbox', selected)
        // Clear the selection so it isn't copied repeatedly
    selected = '';
});

function insertAtCaret(areaId, text) {
    var txtarea = document.getElementById(areaId);
    var scrollPos = txtarea.scrollTop;
    var strPos = 0;
    var br = ((txtarea.selectionStart || txtarea.selectionStart == '0') ?
        "ff" : (document.selection ? "ie" : false));
    if (br == "ie") {
        txtarea.focus();
        var range = document.selection.createRange();
        range.moveStart('character', -txtarea.value.length);
        strPos = range.text.length;
    } else if (br == "ff") strPos = txtarea.selectionStart;

    var front = (txtarea.value).substring(0, strPos);
    var back = (txtarea.value).substring(strPos, txtarea.value.length);
    txtarea.value = front + text + back;
    strPos = strPos + text.length;
    if (br == "ie") {
        txtarea.focus();
        var range = document.selection.createRange();
        range.moveStart('character', -txtarea.value.length);
        range.moveStart('character', strPos);
        range.moveEnd('character', 0);
        range.select();
    } else if (br == "ff") {
        txtarea.selectionStart = strPos;
        txtarea.selectionEnd = strPos;
        txtarea.focus();
    }
    txtarea.scrollTop = scrollPos;
}

And here is the CodeMirror textarea part:

var editor;

//<![CDATA[
window.onload = function() {
    editor = CodeMirror.fromTextArea(document.getElementById('textbox'), {
      mode: {
        name: "python",
        version: 2,
        singleLineStringErrors: false
      },
      lineNumbers: true,
      indentUnit: 4
    });
  } //]]>
<!DOCTYPE html>
<html>

<head>

  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js" type="text/javascript"></script>
  <script src="http://www.skulpt.org/static/skulpt.min.js" type="text/javascript"></script>
  <script src="http://www.skulpt.org/static/skulpt-stdlib.js" type="text/javascript"></script>
  <script src="https://www.cs.princeton.edu/~dp6/CodeMirror/lib/codemirror.js" type="text/javascript"></script>
  <script src="https://www.cs.princeton.edu/~dp6/CodeMirror/mode/python/python.js" type="text/javascript"></script>
  <script src="skulpt-codemirror.js" type="text/javascript"></script>
  <script src="load-save-py.js" type="text/javascript"></script>
  <script src="insert.js" type="text/javascript"></script>
  <link href="https://www.cs.princeton.edu/~dp6/CodeMirror/lib/codemirror.css" rel="stylesheet" type="text/css">
  <title>Python Editor</title>
</head>

<body>
  Filename:
  <input id="inputFileNameToSaveAs">
  <button onclick="saveTextAsFile()">Save</button>
  <br>
  <input type="file" id="fileToLoad">
  <button onclick="loadFileAsText()">Open</button>
  <br>
  <br>
  <a href="#!">
    <img class="insert" alt="#1">
    <img class="insert" alt="#2">
    <img class="insert" alt="#3">
  </a>
  <br>
  <br>
  <textarea id="textbox" name="textbox"></textarea>
  <br>
  <button onclick="runit()" type="button">Run</button>
  <pre id="dynamicframe"></pre>
  <div id="canvas"></div>
</body>

</html>

When I put them together in one file, though, when I click the pictures their alts do not copy over to the textarea. Why is this and how do I fix it?


Solution

  • When using CodeMirror, your <textarea /> will be visually replaced by an editor provided by CodeMirror and most of your code relative to your <textarea /> won't be usable as is.

    What's going on on the background is that your actual <textarea /> will first be marked with a style display: none;. Not displayed, no event binded on the <textarea /> will actually trigger. Then, CodeMirror will actually add his own code to the DOM to display a new editor at the position of your <textarea /> which is now not displayed.

    For example, the HTML code for a newly initialized CodeMirror editor with the string 'Hello World' written in it would looks like:

    <div class="CodeMirror-lines">
        <div style="position: relative; outline: none;">
            <div class="CodeMirror-measure">
                <div style="width: 50px; height: 50px; overflow-x: scroll;"></div>
            </div>
            <div class="CodeMirror-measure"></div>
            <div style="position: relative; z-index: 1;"></div>
            <div class="CodeMirror-cursors">
                <div class="CodeMirror-cursor" style="left: 74px; top: 0px; height: 13px;">&nbsp;</div>
            </div>
            <div class="CodeMirror-code">
                <div style="position: relative;">
                    <div class="CodeMirror-gutter-wrapper" style="position: absolute; left: -29px;">
                        <div class="CodeMirror-linenumber CodeMirror-gutter-elt" style="left: 0px; width: 20px;">1</div>
                    </div><pre><span style="padding-right: 0.1px;"><span class="cm-variable">Hello</span> <span class="cm-variable">World</span></span></pre></div>
            </div>
        </div>
    </div>
    

    Your <textarea /> is no longer being used.

    CodeMirror provides natively a programming API which can be used to do what you want. Basically, the steps required are:

    • Check when the editor is focused.
    • When focused, check if a picture has previously been clicked and if a selected text is available (the alt of the image).
    • If yes, insert the selected text (the alt of the image) at the current position.

    The JavaScript code associated to these steps would looks like:

    // Listen to the editor focus events.
    editor.on('focus', function () {
      // Only insert if a value has been previously selected.
      if (selected.length > 0) {
        // Fetch the current CodeMirror document.
        var doc = editor.getDoc();
    
        // Insert the text at the cursor position.
        doc.replaceSelection(selected);
    
        // Clear the selection so it isn't copied repeatedly.
        selected = '';
      }
    });
    

    You can check a working example on this JSFiddle.