4

I'm trying to implement the following scrolling mechanism:

  • Document has several Code Mirror editors
  • Scrolling on the body should be done regularly (default behaviour) even if cursor is on top of an editor
  • Scrolling inside an editor should only be done through drag scrolling (holding down mouse button and scrolling)
  • When scrolling through the body and the cursor moves inside an editor the editor's scroll should not be triggered since the mouse button is not being held down

I've been struggling with this. My best bet so far is setting the .CodeMirror-scroll class to unset !important when mouse button is up and revert it to the default value (scroll !important) when mouse button is down to allow scrolling. However, this seems to break and cause erratic scrolling behaviour (e.g., when mouse button is up the editors do not retain their last scroll value and are always reset to zero).

I've even tried using CodeMirror's API cm.scrollTo(x, y) function to force the scroll value on mouseup but this also does not work.

Here's a JSFiddle which shows the scrolling propagating from body to child editor when the cursor moves over. Also a GIF showing this.

Usually implementing drag scrolling is not an issue as I've done it in the past both by myself and by using this lib. However, I'm not able to manipulate the scroll event in the .CodeMirror-scroll class as it seems to be overriden by the lib, possibly due to the CSS class (snippet from the CSS source):

.CodeMirror-scroll {
  overflow: scroll !important; /* Things will break if this is overridden */
  /* 30px is the magic margin used to hide the element's real scrollbars */
  /* See overflow: hidden in .CodeMirror */
  margin-bottom: -30px; margin-right: -30px;
  padding-bottom: 30px;
  height: 100%;
  outline: none; /* Prevent dragging from highlighting the element */
  position: relative;
}

As by the author's comments, it seems changing overflow will break stuff, so I guess I need to find a solution which does not involve changing the CSS/style. I'll be most thankful for any help.

Sasha Fonseca
  • 2,257
  • 23
  • 41
  • 1
    Drag scrolling may not be a good idea for browsing code. How about this: clicking the editor to focus in it, and the scrolling only works in editor. Clicking outside the editor to blur, and the scrolling only works in body. – blackmiaool Mar 29 '17 at 04:19
  • Did my answer help you at all? Can I do anything to improve it and help you? – Just a student Apr 12 '17 at 14:27

1 Answers1

1

This is very tricky to get right, in particular because dragging in an editor should, I believe, select text. Overloading it for scrolling seems counterintuitive. Therefore, I propose the following.
This resembles blackmiacool's suggestion in the comments a lot. I only chose not to disable scrolling on the body.

First a demo, both here and on JSFiddle, for your convenience.

(function() {

  function isChildOf(el, parent) {
    do {
      el = el.parentNode;
    } while (el !== null && el !== parent && el !== document.body);
    return (el === parent);
  }

  var readOnlyCodeMirror = CodeMirror.fromTextArea(document.getElementById('codesnippet_readonly'), {
    mode: "javascript",
    theme: "default",
    lineNumbers: true,
    readOnly: true
  });

  var editableCodeMirror = CodeMirror.fromTextArea(document.getElementById('codesnippet_editable'), {
    mode: "javascript",
    theme: "default",
    lineNumbers: true
  });

  var activeEditor = null,
    newActiveEditor = null;

  for (let cm of document.querySelectorAll('.CodeMirror')) {
    let overlay = document.createElement('div');
    overlay.classList.add('cm__overlay');
    cm.insertBefore(overlay, cm.firstChild);
    overlay.addEventListener('click', function(event) {
      overlay.classList.add('cm__overlay--hidden');
      if (activeEditor === null) {
        activeEditor = cm;
      } else {
        newActiveEditor = cm;
      }
    });
  }

  document.body.addEventListener('click', function(event) {
    if (activeEditor !== null && !isChildOf(event.target, activeEditor)) {
      activeEditor.firstChild.classList.remove('cm__overlay--hidden');
      activeEditor = null;
      if (newActiveEditor !== null) {
        activeEditor = newActiveEditor;
        newActiveEditor = null;
      }
    }
  });

}());
.cm__overlay {
  position: absolute;
  z-index: 10;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
}

.cm__overlay--hidden {
  pointer-events: none;
  border: 1px solid red;
}
<h1>Using CodeMirror (readonly and editable code)</h1>

<p><a href="http://codemirror.net/mode/javascript/index.html">http://codemirror.net/mode/javascript/index.html</a></p>

<link rel="stylesheet" href="http://codemirror.net/lib/codemirror.css">
<script src="http://codemirror.net/lib/codemirror.js"></script>
<script src="http://codemirror.net/addon/edit/matchbrackets.js"></script>
<script src="http://codemirror.net/mode/javascript/javascript.js"></script>

<h2>Readonly</h2>

<div>
  <textarea rows="4" cols="50" id="codesnippet_readonly" name="codesnippet_readonly">
// Demo code (the actual new parser character stream implementation)

function StringStream(string) {
  this.pos = 0;
  this.string = string;
}

StringStream.prototype = {
  done: function() {return this.pos >= this.string.length;},
  peek: function() {return this.string.charAt(this.pos);},
  next: function() {
    if (this.pos &lt; this.string.length)
      return this.string.charAt(this.pos++);
  },
  eat: function(match) {
    var ch = this.string.charAt(this.pos);
    if (typeof match == "string") var ok = ch == match;
    else var ok = ch &amp;&amp; match.test ? match.test(ch) : match(ch);
    if (ok) {this.pos++; return ch;}
  },
  eatWhile: function(match) {
    var start = this.pos;
    while (this.eat(match));
    if (this.pos > start) return this.string.slice(start, this.pos);
  },
  backUp: function(n) {this.pos -= n;},
  column: function() {return this.pos;},
  eatSpace: function() {
    var start = this.pos;
    while (/\s/.test(this.string.charAt(this.pos))) this.pos++;
    return this.pos - start;
  },
  match: function(pattern, consume, caseInsensitive) {
    if (typeof pattern == "string") {
      function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
      if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) {
        if (consume !== false) this.pos += str.length;
        return true;
      }
    }
    else {
      var match = this.string.slice(this.pos).match(pattern);
      if (match &amp;&amp; consume !== false) this.pos += match[0].length;
      return match;
    }
  }
};
  </textarea>
</div>

<div>

  <h2>Editable</h2>

  <textarea rows="4" cols="50" name="codesnippet_editable" id="codesnippet_editable">
// Demo code (the actual new parser character stream implementation)

function StringStream(string) {
  this.pos = 0;
  this.string = string;
}

StringStream.prototype = {
  done: function() {return this.pos >= this.string.length;},
  peek: function() {return this.string.charAt(this.pos);},
  next: function() {
    if (this.pos &lt; this.string.length)
      return this.string.charAt(this.pos++);
  },
  eat: function(match) {
    var ch = this.string.charAt(this.pos);
    if (typeof match == "string") var ok = ch == match;
    else var ok = ch &amp;&amp; match.test ? match.test(ch) : match(ch);
    if (ok) {this.pos++; return ch;}
  },
  eatWhile: function(match) {
    var start = this.pos;
    while (this.eat(match));
    if (this.pos > start) return this.string.slice(start, this.pos);
  },
  backUp: function(n) {this.pos -= n;},
  column: function() {return this.pos;},
  eatSpace: function() {
    var start = this.pos;
    while (/\s/.test(this.string.charAt(this.pos))) this.pos++;
    return this.pos - start;
  },
  match: function(pattern, consume, caseInsensitive) {
    if (typeof pattern == "string") {
      function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
      if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) {
        if (consume !== false) this.pos += str.length;
        return true;
      }
    }
    else {
      var match = this.string.slice(this.pos).match(pattern);
      if (match &amp;&amp; consume !== false) this.pos += match[0].length;
      return match;
    }
  }
};
</textarea>

</div>

The idea is that we add an overlay div to every CodeMirror editor. This blocks scrolling from happening in that editor. It is a cheap and portable solution. Cancelling scroll events is much harder, hence this approach. It also allows us to do the following.

When users click an editor (thus, the overlay), we make the overlay ignore all pointer events. This enables scrolling, text selection, cursor movement, et cetera. For UX, it is good to indicate when an editor is focused. This can be done by highlighting the overlay! A red border is applied in the demo, you can of course do anything you like.

Then, when users click anywhere else, the active editor (if one is active) is deactivated and again ignores scroll events (and other pointer events). Because the border goes away, users will understand what happened and quickly learn that they first need to click an editor before being able to edit code in it, or select text for copy & pasting.

When you want to disable scrolling on the body while an editor is focused, check out this answer for example (there are many related questions and answers).

Community
  • 1
  • 1
Just a student
  • 10,560
  • 2
  • 41
  • 69