Hedge
Hedge

Reputation: 16768

Make side-by-side markdown preview scroll with its editor

I've got a Markdown editor on the left side and its HTML preview on the right side.

How do I make the preview scroll to the same location as the editor.

Upvotes: 4

Views: 3489

Answers (4)

mb21
mb21

Reputation: 39373

I've implemented this for PanWriter.

The gist from this commit:

var editor = ... // a CodeMirror editor instance
var frameWindow = document.querySelector('iframe').contentWindow; // my preview frame
var scrollMap;

editor.on("scroll", function() {
  if (!scrollMap) {
    buildScrollMap(editor, 10);
  }
  frameWindow.scrollTo(0, scrollMap[scrollTop]);
});

function buildScrollMap(editor, editorOffset) {
  // scrollMap maps source-editor-line-offsets to preview-element-offsets
  // (offset is the number of vertical pixels from the top)
  scrollMap = [];
  scrollMap[0] = 0;

  // lineOffsets[i] holds top-offset of line i in the source editor
  var lineOffsets = [undefined, 0]
    , knownLineOffsets = []
    , offsetSum = 0
    ;
  editor.eachLine( function(line) {
    offsetSum += line.height;
    lineOffsets.push(offsetSum);
  });

  var lastEl;
  frameWindow.document.querySelectorAll('body > [data-source-line]').forEach( function(el){
    // for each element in the preview with source annotation
    var line = parseInt(el.getAttribute('data-source-line'), 10)
      , lineOffset = lineOffsets[line]
      ;
    // fill in the target offset for the corresponding editor line
    scrollMap[lineOffset] = el.offsetTop - editorOffset;
    knownLineOffsets.push(lineOffset)

    lastEl = el;
  });
  if (lastEl) {
    scrollMap[offsetSum] = lastEl.offsetTop + lastEl.offsetHeight;
    knownLineOffsets.push(offsetSum);
  }

  // fill in the blanks by interpolating between the two closest known line offsets
  var j = 0;
  for (var i=1; i < offsetSum; i++) {
    if (scrollMap[i] === undefined) {
      var a = knownLineOffsets[j]
        , b = knownLineOffsets[j + 1]
        ;
      scrollMap[i] = Math.round(( scrollMap[b]*(i - a) + scrollMap[a]*(b - i) ) / (b - a));
    } else {
      j++;
    }
  }
}

For this to work, you need source-line annotations on your HTML output (use e.g. markdown-it-source-map).

Of course, you'll also have to do it the other way around (when you scroll the preview, make the editor scroll) and look out for edge-cases/offset, depending on your layout. But this is the basic algorithm.

And you probably want to wrap this in something like _.throttle.

Upvotes: 1

martincarlin87
martincarlin87

Reputation: 11052

Try this:

var $elements = $('textarea');

var sync = function(e){
    var $other = $elements.not(this).off('scroll'), other = $other.get(0);
    var percentage = this.scrollTop / (this.scrollHeight - this.offsetHeight);
    other.scrollTop = percentage * (other.scrollHeight - other.offsetHeight);
    setTimeout( function(){ $other.on('scroll', sync ); },10);
}

$elements.on( 'scroll', sync);

Fiddle: http://jsfiddle.net/b75KZ/5/

Although, I'm not sure if it would be a textarea that you'd want on the right, perhaps a div to show the rendered html?

If so just change the element in the html and the selector in the jQuery to var $elements = $('textarea, div#html'); and ensure you have the id attribute set for the div.

Also, if you have multiple textareas on the page and want to be more specific just change the selector to var $elements = $('textarea#markdown, div#html'); and update the markup accordingly, e.g.

<textarea id="markdown">...</textarea>
<div id="html">...</div>

Upvotes: 2

jmgross
jmgross

Reputation: 2336

The Remarkable library use a complex scroll sync for the demo : https://github.com/jonschlinkert/remarkable/blob/dev/demo/assets/index.js#L213

Upvotes: 6

Kevin Simper
Kevin Simper

Reputation: 1697

The example you linked to just align the scrollbars to the same height, it does not seem to do any intelligent matching of elements.

So a good start is just to align scrollbars.

Upvotes: 1

Related Questions