2019.1.22 简书官方已经加入了同步滚动的功能。
但是官方实现的时候,优先的是顶端对齐,也就是编辑区和预览区在顶部一定是同步的。本文的实现是优先底部对齐,也就是如果滑动到最后,预览和编辑是一定会对齐的。我觉得优先底部对齐更实用,因为写作的过程中在文末添加内容的场景更多,这个时候需要对齐。
如你所见,右侧的预览并不能实时地与左侧的输入进行同步。
这是很气的。
哎呦这是最气的。
所以我写了一个脚本,点击此处安装(需要先行安装浏览器用户脚本扩展)。
经过评论区小伙伴的反馈,这里还真遇到一个坑:
一开始的版本做的比较简单,当输入框接收到滚动事件后,就直接向预览框也发送这个滚动事件。
但是当浏览器开启平滑滚动时,一次鼠标滚轮的滚动会被浏览器分解成多段来处理,以达到平滑的效果。
平滑滚动的情况下,被分解的滚动事件的头一段的像素移动非常小,我们的脚本却会把这个小滚动事件也照旧作用在预览框上。问题来了,此时预览框接收到滚动事件后,又通过我们这个脚本向输入框上发了一个小滚动事件,相当于“反弹”了一下。也就是说,如果没有这个“反弹”回来的滚动事件影响,预览框本应该继续进行接下来的平滑滚动分解动作,但是却收到了来自我们脚本的小滚动事件,就忽视了接下来的平滑滚动分解动作,所以每次滚动只滚动了很小的距离,看起来像是被卡住了。
所以问题在于考虑的太简单,事件发生了来回相互触发。我们的脚本无法区别哪些是人产生的滚动事件,哪些是脚本发出的滚动事件,导致事件来回反弹,影响正常工作。
解决办法是手动的截断由我们的脚本所产生的滚动事件。步骤如下:
假如发生了编辑框的滚动事件计 mainFlag
为 true,发生了预览框的滚动事件计 preFlag
为 true。
- 用户滚动编辑框,触发滚动事件,
mainFlag
计为true
,触发脚本改变预览框位置。 - 上一步骤预览框位置改变又触发了预览框滚动事件,
preFlag
计为true
,此时检测发现mainFlag
也为true
,证明这次预览框滚动事件是由脚本产生的事件,应该被截断。(如果不截断,这次滚动事件又会触发更改编辑框的动作,引起 bug。) - 把两个标志位都恢复为
false
。
形象的说就是,由两个标志位来做“配对”,“配对”抵消下一个事件。
1.3
版代码如下:
// ==UserScript==
// @name 简书 Markdown 预览同步滚动
// @name:en Jianshu MD AUTO Scroll
// @namespace https://github.com/BlindingDark/JianshuMDAutoScroll
// @include *://www.jianshu.com/writer*
// @version 1.3
// @description:en jianshu Markdown preview AUTO scroll
// @description 给简书的在线 Markdown 编辑器增加输入预览同步滚动的功能
// @author BlindingDark
// @grant none
// @require https://cdn.bootcss.com/jquery/3.2.1/jquery.js
// ==/UserScript==
(function() {
'use strict';
var spSwitchMain; // 切换的那个按钮所在的窗体
var txtMain; // 输入框
var spPreview; // 预览框
const SWITCH_FEATURE = 'a.fa.fa-columns';
const EXPAND_FEATURE = 'a.fa.fa-expand';
const COMPRESS_FEATURE = 'a.fa.fa-compress';
function getInput() {
return $('#arthur-editor');
}
function getPreview() {
return getInput().closest("div").parent().next();
}
function scrollEvent(){
txtMain = getInput()[0];
spPreview = getPreview()[0];
if(txtMain == undefined) {
return;
}
if(spPreview == undefined) {
return;
}
let mainFlag = false; // 抵消两个滚动事件之间互相触发
let preFlag = false; // 如果两个 flag 都为 true,证明是反弹过来的事件引起的
function scrolling(who){
if(who == 'pre'){
preFlag = true;
if (mainFlag === true){ // 抵消两个滚动事件之间互相触发
mainFlag = false;
preFlag = false;
return;
}
txtMain.scrollTop = Math.round((spPreview.scrollTop + spPreview.clientHeight) * txtMain.scrollHeight / spPreview.scrollHeight - txtMain.clientHeight);
return;
}
if(who == 'main'){
mainFlag = true;
if (preFlag === true){ // 抵消两个滚动事件之间互相触发
mainFlag = false;
preFlag = false;
return;
}
spPreview.scrollTop = Math.round((txtMain.scrollTop + txtMain.clientHeight) * spPreview.scrollHeight / txtMain.scrollHeight - spPreview.clientHeight);
return;
}
}
function mainOnscroll(){
scrolling('main');
}
function preOnscroll(){
scrolling('pre');
}
getInput().on('scroll', () => mainOnscroll());
getPreview().on('scroll', () => preOnscroll());
}
function cycle() {
scrollEvent();
$(EXPAND_FEATURE).on('click', scrollEvent);
$(COMPRESS_FEATURE).on('click', scrollEvent);
$(SWITCH_FEATURE).on("click", scrollEvent);
window.setTimeout(cycle, 1000);
}
cycle();
})();
有兴趣的同学可以直接前往 Github 改进此脚本。