代码镜像 - 仅在编辑器内实现拖动滚动(常规滚动正文)

时间:2017-03-26 11:35:10

标签: javascript jquery css scroll codemirror

我正在尝试实现以下滚动机制:

  • Document有几个Code Mirror编辑器
  • 即使光标位于编辑器顶部,也应定期滚动正文(默认行为)
  • 只能通过拖动滚动(按住鼠标按钮并滚动)来滚动编辑器。
  • 当滚动身体并且光标在编辑器内移动时,由于没有按下鼠标按钮,因此不应触发编辑器的滚动

我一直在努力解决这个问题。到目前为止,我最好的选择是在鼠标按钮启动时将.CodeMirror-scroll类设置为unset !important,并在鼠标按钮向下时将其恢复为默认值(scroll !important)以允许滚动。但是,这似乎会破坏并导致不稳定的滚动行为(例如,当鼠标按钮启动时,编辑器不会保留其最后的滚动值并始终重置为零。)

我甚至尝试使用CodeMirror的API cm.scrollTo(x, y)函数强制mouseup上的滚动值,但这也不起作用。

这是一个JSFiddle,它显示了当光标移过时从主体传播到子编辑器的滚动。另外一个GIF显示了这一点。

通常实现拖动滚动不是问题,因为过去我自己和使用this lib都是这样做的。但是,我无法操纵scroll类中的.CodeMirror-scroll事件,因为它似乎被lib覆盖,可能是由于CSS类(来自CSS源代码段):

.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;
}

正如作者的评论一样,似乎改变overflow会破坏内容,所以我想我需要找到一个不涉及改变CSS /风格的解决方案。我会非常感谢任何帮助。

1 个答案:

答案 0 :(得分:1)

这是非常棘手的,特别是因为在编辑器中拖动应该选择文本。重载它以进行滚动似乎违反直觉。因此,我提出以下建议 这在评论中很像blackmiacoolsuggestion。我只选择不禁用身体上的滚动。

为了您的方便,首先在此处和JSFiddle进行演示。

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

我们的想法是为每个CodeMirror编辑器添加一个覆盖div。这会阻止滚动在该编辑器中发生。这是一种便宜且便携的解决方案。取消滚动事件要困难得多,因此采用这种方法。它还允许我们执行以下操作。

当用户点击编辑器(因此,叠加层)时,我们会使叠加层忽略所有pointer events。这使得滚动,文本选择,光标移动等等成为可能。对于UX,最好指出编辑器何时聚焦。这可以通过突出显示叠加来完成!在演示中应用了红色边框,您当然可以做任何您喜欢的事情。

然后,当用户单击其他任何位置时,活动编辑器(如果一个处于活动状态)将被停用,并再次忽略滚动事件(和其他指针事件)。由于边界消失,用户将了解发生的情况并快速了解他们首先需要点击编辑器才能编辑其中的代码,或选择文本进行复制和放大。粘贴。

如果您想在编辑器聚焦时禁用正文滚动,请查看this answer例如(有许多相关问题和答案)。