【问题标题】:Code Mirror - Implementing drag scrolling inside editors only (regular scrolling for body)代码镜像 - 仅在编辑器内实现拖动滚动(正文的常规滚动)
【发布时间】:2017-03-26 11:35:10
【问题描述】:

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

  • 文档有多个代码镜像编辑器
  • 即使光标位于编辑器顶部,也应定期滚动正文(默认行为)
  • 在编辑器内滚动只能通过拖动滚动(按住鼠标按钮并滚动)来完成
  • 当滚动正文并且光标在编辑器内移动时,不应触发编辑器的滚动,因为没有按住鼠标按钮

我一直在为此苦苦挣扎。到目前为止,我最好的选择是在鼠标按钮向上时将.CodeMirror-scroll 类设置为unset !important,并在鼠标按钮向下以允许滚动时将其恢复为默认值(scroll !important)。但是,这似乎会中断并导致不稳定的滚动行为(例如,当鼠标按钮向上时,编辑器不会保留其最后的滚动值并且总是重置为零)。

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

这是一个JSFiddle,它显示了当光标移动时从正文到子编辑器的滚动传播。还有一个 GIF 显示这个。

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

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

正如作者的 cmets 所说,更改 overflow 似乎会破坏一些东西,所以我想我需要找到一个不涉及更改 CSS/样式的解决方案。如有任何帮助,我将不胜感激。

【问题讨论】:

  • 拖动滚动可能不是浏览代码的好主意。怎么样:点击编辑器聚焦,滚动只在编辑器中有效。在编辑器外点击进行模糊,滚动只在正文中起作用。
  • 我的回答对你有帮助吗?我可以做些什么来改进它并帮助你吗?

标签: javascript jquery css scroll codemirror


【解决方案1】:

这很难做到正确,特别是因为我相信拖动编辑器应该选择文本。为滚动而重载它似乎违反直觉。因此,我提出以下建议。
这很像 cmets 中的blackmiacoolsuggestion。我只是选择不禁用 body 上的滚动。

为了您的方便,首先在此处和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 (有许多相关问题和答案)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-01-21
    • 1970-01-01
    • 2020-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-16
    相关资源
    最近更新 更多