From b28750f256d6fbab1406c6c00155ea14eef5c600 Mon Sep 17 00:00:00 2001
From: "Cheng-Han, Wu" <jackymaxj@gmail.com>
Date: Thu, 26 May 2016 13:17:00 +0800
Subject: [PATCH] Update to improve syncscroll performance and add toggle for
 sync scrolling

---
 public/css/index.css    |  11 +++
 public/js/index.js      |  63 +++++++------
 public/js/syncscroll.js | 194 ++++++++++++++++++----------------------
 3 files changed, 132 insertions(+), 136 deletions(-)

diff --git a/public/css/index.css b/public/css/index.css
index aaf84a041..1b9fb5d4e 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -112,6 +112,17 @@ body {
     background-color: white;
     box-shadow: 5px 0px 10px #e7e7e7;
 }
+.ui-edit-area .ui-sync-toggle {
+    width: 42px;
+    height: 42px;
+    padding: 3px 1px 0 0;
+    border-radius: 50%;
+    border-color: #e7e7e7;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+}
 .ui-view-area {
     /*overflow-y: scroll;*/
     -webkit-overflow-scrolling: touch;
diff --git a/public/js/index.js b/public/js/index.js
index b4f912bb4..29e229799 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -552,7 +552,11 @@ var ui = {
         codemirrorScroll: $(".ui-edit-area .CodeMirror .CodeMirror-scroll"),
         codemirrorSizer: $(".ui-edit-area .CodeMirror .CodeMirror-sizer"),
         codemirrorSizerInner: $(".ui-edit-area .CodeMirror .CodeMirror-sizer > div"),
-        markdown: $(".ui-view-area .markdown-body")
+        markdown: $(".ui-view-area .markdown-body"),
+        resize: {
+            handle: $('.ui-resizable-handle'),
+            syncToggle: $('.ui-sync-toggle')
+        }
     },
     modal: {
         snippetImportProjects: $("#snippetImportModalProjects"),
@@ -705,30 +709,6 @@ $(window).error(function () {
     //setNeedRefresh();
 });
 
-//when page hash change
-window.onhashchange = locationHashChanged;
-
-function locationHashChanged(e) {
-    e.stopPropagation();
-    e.preventDefault();
-    if (currentMode != modeType.both) {
-        return;
-    }
-    var hashtarget = $("[id$='" + location.hash.substr(1) + "']");
-    if (hashtarget.length > 0) {
-        var linenumber = hashtarget.attr('data-startline');
-        if (linenumber) {
-            editor.setOption('viewportMargin', Infinity);
-            editor.setOption('viewportMargin', viewportMargin);
-            var t = editor.charCoords({
-                line: linenumber,
-                ch: 0
-            }, "local").top;
-            editor.scrollTo(null, t - defaultTextHeight * 1.2);
-        }
-    }
-}
-
 var windowResizeDebounce = 200;
 var windowResize = _.debounce(windowResizeInner, windowResizeDebounce);
 
@@ -814,10 +794,38 @@ function checkEditorStyle() {
         handles: 'e',
         maxWidth: $(window).width() * 0.7,
         minWidth: $(window).width() * 0.2,
+        resize: function (e) {
+            ui.area.resize.syncToggle.stop(true, true).show();
+        },
         stop: function (e) {
             lastEditorWidth = ui.area.edit.width();
         }
     });
+    if (!ui.area.resize.handle.length) {
+        ui.area.resize.handle = $('.ui-resizable-handle');
+    }
+    if (!ui.area.resize.syncToggle.length) {
+        ui.area.resize.syncToggle = $('<button class="btn btn-lg btn-default ui-sync-toggle" title="Toggle sync scrolling"><i class="fa fa-link fa-fw"></i></button>');
+        ui.area.resize.syncToggle.click(function () {
+            syncscroll = !syncscroll;
+            checkSyncToggle();
+        });
+        ui.area.resize.handle.append(ui.area.resize.syncToggle);
+        ui.area.resize.syncToggle.hide();
+        ui.area.resize.handle.hover(function () {
+            ui.area.resize.syncToggle.stop(true, true).delay(200).fadeIn(100);
+        }, function () {
+            ui.area.resize.syncToggle.stop(true, true).delay(300).fadeOut(300);
+        });
+    }
+}
+
+function checkSyncToggle() {
+    if (syncscroll) {
+        ui.area.resize.syncToggle.find('i').removeClass('fa-unlink').addClass('fa-link');
+    } else {
+        ui.area.resize.syncToggle.find('i').removeClass('fa-link').addClass('fa-unlink');
+    }
 }
 
 function checkEditorScrollbar() {
@@ -984,10 +992,10 @@ function changeMode(type) {
             ui.area.edit.css('width', lastEditorWidth + 'px');
         else
             ui.area.edit.css('width', '');
-        ui.area.edit.find('.ui-resizable-handle').show();
+        ui.area.resize.handle.show();
     } else {
         ui.area.edit.css('width', '');
-        ui.area.edit.find('.ui-resizable-handle').hide();
+        ui.area.resize.handle.hide();
     }
 
     windowResizeInner();
@@ -995,6 +1003,7 @@ function changeMode(type) {
     restoreInfo();
 
     if (lastMode == modeType.view && currentMode == modeType.both) {
+        preventSyncScrollToView = true;
         syncScrollToEdit();
     }
 
diff --git a/public/js/syncscroll.js b/public/js/syncscroll.js
index b52e2bfab..348af2d1c 100644
--- a/public/js/syncscroll.js
+++ b/public/js/syncscroll.js
@@ -105,10 +105,13 @@ md.use(window.markdownitContainer, 'info', { render: renderContainer });
 md.use(window.markdownitContainer, 'warning', { render: renderContainer });
 md.use(window.markdownitContainer, 'danger', { render: renderContainer });
 
-var preventSyncScroll = false;
+var syncscroll = true;
+
+var preventSyncScrollToEdit = false;
+var preventSyncScrollToView = false;
 
 var editScrollThrottle = 1;
-var viewScrollThrottle = 20;
+var viewScrollThrottle = 10;
 var buildMapThrottle = 100;
 
 var viewScrolling = false;
@@ -126,71 +129,6 @@ if (editor.getOption('scrollbarStyle') === 'native') {
 }
 ui.area.view.on('scroll', _.throttle(syncScrollToEdit, viewScrollThrottle));
 
-var preventViewScroll = false;
-
-function syncScrollToEdit(e) {
-    if (currentMode != modeType.both) return;
-    if (preventViewScroll) {
-        if (typeof preventViewScroll === 'number') {
-            preventViewScroll--;
-        } else {
-            preventViewScroll = false;
-        }
-        return;
-    }
-    if (!scrollMap || !lineHeightMap) {
-        buildMap(true);
-        return;
-    }
-    if (editScrolling) return;
-    var scrollTop = ui.area.view[0].scrollTop;
-    var lineIndex = 0;
-    for (var i = 0, l = scrollMap.length; i < l; i++) {
-        if (scrollMap[i] > scrollTop) {
-            break;
-        } else {
-            lineIndex = i;
-        }
-    }
-    var lineNo = 0;
-    var lineDiff = 0;
-    for (var i = 0, l = lineHeightMap.length; i < l; i++) {
-        if (lineHeightMap[i] > lineIndex) {
-            break;
-        } else {
-            lineNo = lineHeightMap[i];
-            lineDiff = lineHeightMap[i + 1] - lineNo;
-        }
-    }
-    
-    var scrollInfo = editor.getScrollInfo();
-    var textHeight = editor.defaultTextHeight();
-    var posTo = 0;
-    var topDiffPercent = 0;
-    var posToNextDiff = 0;
-    var preLastLineHeight = scrollInfo.height - scrollInfo.clientHeight - textHeight;
-    var preLastLineNo = Math.round(preLastLineHeight / textHeight);
-    
-    if (scrollInfo.height > scrollInfo.clientHeight && lineNo >= preLastLineNo) {
-        posTo = preLastLineHeight;
-        topDiffPercent = (scrollTop - scrollMap[preLastLineNo]) / (viewBottom - scrollMap[preLastLineNo]);
-        posToNextDiff = Math.ceil(textHeight * topDiffPercent);
-    } else {
-        posTo = lineNo * textHeight;
-        topDiffPercent = (scrollTop - scrollMap[lineNo]) / (scrollMap[lineNo + lineDiff] - scrollMap[lineNo]);
-        posToNextDiff = Math.ceil(textHeight * lineDiff * topDiffPercent);
-    }
-    
-    editor.scrollTo(0, posTo + posToNextDiff);
-    preventSyncScroll = true;
-    
-    viewScrolling = true;
-    clearTimeout(viewScrollingTimer);
-    viewScrollingTimer = setTimeout(function () {
-        viewScrolling = false;
-    }, viewScrollingDelay);
-}
-
 var scrollMap, lineHeightMap, viewTop, viewBottom;
 
 viewAjaxCallback = clearMap;
@@ -279,60 +217,98 @@ function buildMapInner(syncBack) {
     scrollMap = _scrollMap;
     lineHeightMap = _lineHeightMap;
 
-    if (loaded && syncBack)
+    if (loaded && syncBack) {
         syncScrollToView();
+        syncScrollToEdit();
+    }
 }
 
-function getPartByEditorLineNo(lineNo) {
-    var part = null;
-    ui.area.markdown.find('.part').each(function (n, el) {
-        if (part) return;
-        var $el = $(el),
-            t = $el.data('startline') - 1,
-            f = $el.data('endline') - 1;
-        if (t === '' || f === '') {
-            return;
-        }
-        if (lineNo >= t && lineNo <= f) {
-            part = $el;
-        }
-    });
-    if (part)
-        return {
-            startline: part.data('startline') - 1,
-            endline: part.data('endline') - 1,
-            linediff: Math.abs(part.data('endline') - part.data('startline')) + 1,
-            element: part
-        };
-    else
-        return null;
-}
 
-function getEditorLineNoByTop(top) {
-    for (var i = 0; i < lineHeightMap.length; i++)
-        if (lineHeightMap[i] * editor.defaultTextHeight() > top)
-            return i;
-    return null;
-}
-
-function syncScrollToView(event, _lineNo) {
-    if (currentMode != modeType.both) return;
-    if (preventSyncScroll) {
-        if (typeof preventSyncScroll === 'number') {
-            preventSyncScroll--;
+function syncScrollToEdit(e) {
+    if (currentMode != modeType.both || !syncscroll) return;
+    if (preventSyncScrollToEdit) {
+        if (typeof preventSyncScrollToEdit === 'number') {
+            preventSyncScrollToEdit--;
         } else {
-            preventSyncScroll = false;
+            preventSyncScrollToEdit = false;
         }
         return;
     }
-    var lineNo, posTo;
-    var scrollInfo = editor.getScrollInfo();
     if (!scrollMap || !lineHeightMap) {
         buildMap(true);
         return;
     }
+    if (editScrolling) return;
+    
+    var scrollTop = ui.area.view[0].scrollTop;
+    var lineIndex = 0;
+    for (var i = 0, l = scrollMap.length; i < l; i++) {
+        if (scrollMap[i] > scrollTop) {
+            break;
+        } else {
+            lineIndex = i;
+        }
+    }
+    var lineNo = 0;
+    var lineDiff = 0;
+    for (var i = 0, l = lineHeightMap.length; i < l; i++) {
+        if (lineHeightMap[i] > lineIndex) {
+            break;
+        } else {
+            lineNo = lineHeightMap[i];
+            lineDiff = lineHeightMap[i + 1] - lineNo;
+        }
+    }
+    
+    var posTo = 0;
+    var topDiffPercent = 0;
+    var posToNextDiff = 0;
+    var scrollInfo = editor.getScrollInfo();
+    var textHeight = editor.defaultTextHeight();
+    var preLastLineHeight = scrollInfo.height - scrollInfo.clientHeight - textHeight;
+    var preLastLineNo = Math.round(preLastLineHeight / textHeight);
+    var preLastLinePos = scrollMap[preLastLineNo];
+    
+    if (scrollInfo.height > scrollInfo.clientHeight && scrollTop >= preLastLinePos) {
+        posTo = preLastLineHeight;
+        topDiffPercent = (scrollTop - preLastLinePos) / (viewBottom - preLastLinePos);
+        posToNextDiff = Math.ceil(textHeight * topDiffPercent);
+    } else {
+        posTo = lineNo * textHeight;
+        topDiffPercent = (scrollTop - scrollMap[lineNo]) / (scrollMap[lineNo + lineDiff] - scrollMap[lineNo]);
+        posToNextDiff = Math.ceil(textHeight * lineDiff * topDiffPercent);
+    }
+    
+    editor.scrollTo(0, posTo + posToNextDiff);
+    preventSyncScrollToView = true;
+    
+    viewScrolling = true;
+    clearTimeout(viewScrollingTimer);
+    viewScrollingTimer = setTimeout(function () {
+        viewScrolling = false;
+    }, viewScrollingDelay);
+}
+
+function syncScrollToView(event, _lineNo) {
+    if (currentMode != modeType.both || !syncscroll) return;
+    if (preventSyncScrollToView) {
+        if (typeof preventSyncScrollToView === 'number') {
+            preventSyncScrollToView--;
+        } else {
+            preventSyncScrollToView = false;
+        }
+        return;
+    }
+    if (!scrollMap || !lineHeightMap) {
+        buildMap(true);
+        return;
+    }
+    if (viewScrolling) return;
+    
     if (!_lineNo) {
+        var lineNo, posTo;
         var topDiffPercent, posToNextDiff;
+        var scrollInfo = editor.getScrollInfo();
         var textHeight = editor.defaultTextHeight();
         lineNo = Math.floor(scrollInfo.top / textHeight);
         // if reach the last line, will start lerp to the bottom
@@ -349,11 +325,11 @@ function syncScrollToView(event, _lineNo) {
             posTo += Math.floor(posToNextDiff);
         }
     } else {
-        if (viewScrolling) return;
         posTo = scrollMap[lineHeightMap[_lineNo]];
     }
+    
     ui.area.view.stop(true, true).scrollTop(posTo);
-    preventViewScroll = true;
+    preventSyncScrollToEdit = true;
     
     editScrolling = true;
     clearTimeout(editScrollingTimer);