From 4b0ca55eb79e963523eb6c8197825e9e8ae904e2 Mon Sep 17 00:00:00 2001 From: Wu Cheng-Han Date: Mon, 4 May 2015 15:53:29 +0800 Subject: First commit, version 0.2.7 --- public/js/index.js | 940 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 940 insertions(+) create mode 100644 public/js/index.js (limited to 'public/js/index.js') diff --git a/public/js/index.js b/public/js/index.js new file mode 100644 index 00000000..73b4e594 --- /dev/null +++ b/public/js/index.js @@ -0,0 +1,940 @@ +//constant vars +//settings +var debug = false; +var version = '0.2.7'; +var doneTypingDelay = 400; +var finishChangeDelay = 400; +var cursorActivityDelay = 50; +var syncScrollDelay = 50; +var scrollAnimatePeriod = 100; +var cursorAnimatePeriod = 100; +var modeType = { + edit: {}, + view: {}, + both: {} +} +var statusType = { + connected: { + msg: "CONNECTED", + label: "label-warning", + fa: "fa-wifi" + }, + online: { + msg: "ONLINE: ", + label: "label-primary", + fa: "fa-users" + }, + offline: { + msg: "OFFLINE", + label: "label-danger", + fa: "fa-plug" + } +} +var defaultMode = modeType.both; + +//global vars +var loaded = false; +var isDirty = false; +var editShown = false; +var visibleXS = false; +var visibleSM = false; +var visibleMD = false; +var visibleLG = false; +var isTouchDevice = 'ontouchstart' in document.documentElement; +var currentMode = defaultMode; +var currentStatus = statusType.offline; +var lastInfo = { + needRestore: false, + cursor: null, + scroll: null, + edit: { + scroll: { + left: null, + top: null + }, + cursor: { + line: null, + ch: null + } + }, + view: { + scroll: { + left: null, + top: null + } + }, + history: null +}; + +//editor settings +var editor = CodeMirror.fromTextArea(document.getElementById("textit"), { + mode: 'gfm', + viewportMargin: 20, + styleActiveLine: true, + lineNumbers: true, + lineWrapping: true, + theme: "monokai", + autofocus: true, + inputStyle: "textarea", + matchBrackets: true, + autoCloseBrackets: true, + matchTags: { + bothTags: true + }, + autoCloseTags: true, + foldGutter: true, + gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], + extraKeys: { + "Enter": "newlineAndIndentContinueMarkdownList" + }, + readOnly: true +}); + +//ui vars +var ui = { + spinner: $(".ui-spinner"), + content: $(".ui-content"), + toolbar: { + shortStatus: $(".ui-short-status"), + status: $(".ui-status"), + new: $(".ui-new"), + pretty: $(".ui-pretty"), + download: { + markdown: $(".ui-download-markdown") + }, + save: { + dropbox: $(".ui-save-dropbox") + }, + import: { + dropbox: $(".ui-import-dropbox"), + clipboard: $(".ui-import-clipboard") + }, + mode: $(".ui-mode"), + edit: $(".ui-edit"), + view: $(".ui-view"), + both: $(".ui-both") + }, + area: { + edit: $(".ui-edit-area"), + view: $(".ui-view-area"), + codemirror: $(".ui-edit-area .CodeMirror"), + markdown: $(".ui-view-area .markdown-body") + } +}; + +//page actions +var opts = { + lines: 11, // The number of lines to draw + length: 20, // The length of each line + width: 2, // The line thickness + radius: 30, // The radius of the inner circle + corners: 0, // Corner roundness (0..1) + rotate: 0, // The rotation offset + direction: 1, // 1: clockwise, -1: counterclockwise + color: '#000', // #rgb or #rrggbb or array of colors + speed: 1.1, // Rounds per second + trail: 60, // Afterglow percentage + shadow: false, // Whether to render a shadow + hwaccel: true, // Whether to use hardware acceleration + className: 'spinner', // The CSS class to assign to the spinner + zIndex: 2e9, // The z-index (defaults to 2000000000) + top: '50%', // Top position relative to parent + left: '50%' // Left position relative to parent +}; +var spinner = new Spinner(opts).spin(ui.spinner[0]); +//when page ready +$(document).ready(function () { + checkResponsive(); + changeMode(currentMode); + /* we need this only on touch devices */ + if (isTouchDevice) { + /* cache dom references */ + var $body = jQuery('body'); + + /* bind events */ + $(document) + .on('focus', 'textarea, input', function() { + $body.addClass('fixfixed'); + }) + .on('blur', 'textarea, input', function() { + $body.removeClass('fixfixed'); + }); + } +}); +//when page resize +$(window).resize(function () { + checkResponsive(); +}); +//768-792px have a gap +function checkResponsive() { + visibleXS = $(".visible-xs").is(":visible"); + visibleSM = $(".visible-sm").is(":visible"); + visibleMD = $(".visible-md").is(":visible"); + visibleLG = $(".visible-lg").is(":visible"); + if (visibleXS && currentMode == modeType.both) + if (editor.hasFocus()) + changeMode(modeType.edit); + else + changeMode(modeType.view); +} + +function showStatus(type, num) { + currentStatus = type; + var shortStatus = ui.toolbar.shortStatus; + var status = ui.toolbar.status; + var label = $(''); + var fa = $(''); + var msg = ""; + var shortMsg = ""; + + shortStatus.html(""); + status.html(""); + + switch (currentStatus) { + case statusType.connected: + label.addClass(statusType.connected.label); + fa.addClass(statusType.connected.fa); + msg = statusType.connected.msg; + break; + case statusType.online: + label.addClass(statusType.online.label); + fa.addClass(statusType.online.fa); + shortMsg = " " + num; + msg = statusType.online.msg + num; + break; + case statusType.offline: + label.addClass(statusType.offline.label); + fa.addClass(statusType.offline.fa); + msg = statusType.offline.msg; + break; + } + + label.append(fa); + var shortLabel = label.clone(); + + shortLabel.append(" " + shortMsg); + shortStatus.append(shortLabel); + + label.append(" " + msg); + status.append(label); +} + +function toggleMode() { + switch(currentMode) { + case modeType.edit: + changeMode(modeType.view); + break; + case modeType.view: + changeMode(modeType.edit); + break; + case modeType.both: + changeMode(modeType.view); + break; + } +} + +function changeMode(type) { + saveInfo(); + if (type) + currentMode = type; + var responsiveClass = "col-lg-6 col-md-6 col-sm-6"; + var scrollClass = "ui-scrollable"; + ui.area.codemirror.removeClass(scrollClass); + ui.area.edit.removeClass(responsiveClass); + ui.area.view.removeClass(scrollClass); + ui.area.view.removeClass(responsiveClass); + switch (currentMode) { + case modeType.edit: + ui.area.edit.show(); + ui.area.view.hide(); + if (!editShown) { + editor.refresh(); + editShown = true; + } + break; + case modeType.view: + ui.area.edit.hide(); + ui.area.view.show(); + break; + case modeType.both: + ui.area.codemirror.addClass(scrollClass); + ui.area.edit.addClass(responsiveClass).show(); + ui.area.view.addClass(scrollClass); + ui.area.view.addClass(responsiveClass).show(); + break; + } + if (currentMode != modeType.view && visibleLG) { + editor.focus(); + editor.refresh(); + } else { + editor.getInputField().blur(); + } + if (changeMode != modeType.edit) + updateView(); + restoreInfo(); + + ui.toolbar.both.removeClass("active"); + ui.toolbar.edit.removeClass("active"); + ui.toolbar.view.removeClass("active"); + var modeIcon = ui.toolbar.mode.find('i'); + modeIcon.removeClass('fa-toggle-on').removeClass('fa-toggle-off'); + if (ui.area.edit.is(":visible") && ui.area.view.is(":visible")) { //both + ui.toolbar.both.addClass("active"); + modeIcon.addClass('fa-eye'); + } else if (ui.area.edit.is(":visible")) { //edit + ui.toolbar.edit.addClass("active"); + modeIcon.addClass('fa-toggle-off'); + } else if (ui.area.view.is(":visible")) { //view + ui.toolbar.view.addClass("active"); + modeIcon.addClass('fa-toggle-on'); + } +} + +//button actions +var noteId = window.location.pathname.split('/')[1]; +var url = window.location.origin + '/' + noteId; +//pretty +ui.toolbar.pretty.attr("href", url + "/pretty"); +//download +//markdown +ui.toolbar.download.markdown.click(function() { + var filename = renderFilename(ui.area.markdown) + '.md'; + var markdown = editor.getValue(); + var blob = new Blob([markdown], {type: "text/markdown;charset=utf-8"}); + saveAs(blob, filename); +}); +//save to dropbox +ui.toolbar.save.dropbox.click(function() { + var filename = renderFilename(ui.area.markdown) + '.md'; + var options = { + files: [ + {'url': url + "/download", 'filename': filename} + ] + }; + Dropbox.save(options); +}); +//import from dropbox +ui.toolbar.import.dropbox.click(function() { + var options = { + success: function(files) { + ui.spinner.show(); + var url = files[0].link; + importFromUrl(url); + }, + linkType: "direct", + multiselect: false, + extensions: ['.md', '.html'] + }; + Dropbox.choose(options); +}); +//import from clipboard +ui.toolbar.import.clipboard.click(function() { + //na +}); +//fix for wrong autofocus +$('#clipboardModal').on('shown.bs.modal', function() { + $('#clipboardModal').blur(); +}); +$("#clipboardModalClear").click(function() { + $("#clipboardModalContent").html(''); +}); +$("#clipboardModalConfirm").click(function() { + var data = $("#clipboardModalContent").html(); + if(data) { + parseToEditor(data); + $('#clipboardModal').modal('hide'); + $("#clipboardModalContent").html(''); + } +}); +function parseToEditor(data) { + var parsed = toMarkdown(data); + if(parsed) + editor.replaceRange(parsed, {line:0, ch:0}, {line:editor.lastLine(), ch:editor.lastLine().length}, '+input'); +} +function importFromUrl(url) { + //console.log(url); + if(url == null) return; + if(!isValidURL(url)) { + alert('Not valid URL :('); + return; + } + $.ajax({ + method: "GET", + url: url, + success: function(data) { + parseToEditor(data); + }, + error: function() { + alert('Import failed :('); + }, + complete: function() { + ui.spinner.hide(); + } + }); +} +function isValidURL(str) { + var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string + '(\\#[-a-z\\d_]*)?$','i'); // fragment locator + if(!pattern.test(str)) { + return false; + } else { + return true; + } +} +//mode +ui.toolbar.mode.click(function () { + toggleMode(); +}); +//edit +ui.toolbar.edit.click(function () { + changeMode(modeType.edit); +}); +//view +ui.toolbar.view.click(function () { + changeMode(modeType.view); +}); +//both +ui.toolbar.both.click(function () { + changeMode(modeType.both); +}); + +//socket.io actions +var socket = io.connect(); +socket.on('info', function (data) { + console.error(data); + location.href = "./404.html"; +}); +socket.on('disconnect', function (data) { + showStatus(statusType.offline); + if (loaded) { + saveInfo(); + lastInfo.history = editor.getHistory(); + } + if (!editor.getOption('readOnly')) + editor.setOption('readOnly', true); +}); +socket.on('connect', function (data) { + showStatus(statusType.connected); + socket.emit('version'); +}); +socket.on('version', function (data) { + if (data != version) + location.reload(true); +}); +socket.on('refresh', function (data) { + saveInfo(); + + var body = data.body; + body = LZString.decompressFromBase64(body); + if (body) + editor.setValue(body); + else + editor.setValue(""); + + if (!loaded) { + editor.clearHistory(); + ui.spinner.hide(); + ui.content.fadeIn(); + changeMode(); + loaded = true; + } else { + if (LZString.compressToBase64(editor.getValue()) !== data.body) + editor.clearHistory(); + else { + if (lastInfo.history) + editor.setHistory(lastInfo.history); + } + lastInfo.history = null; + } + + updateView(); + + if (editor.getOption('readOnly')) + editor.setOption('readOnly', false); + + restoreInfo(); +}); +socket.on('change', function (data) { + data = LZString.decompressFromBase64(data); + data = JSON.parse(data); + editor.replaceRange(data.text, data.from, data.to, "ignoreHistory"); + isDirty = true; + clearTimeout(finishChangeTimer); + finishChangeTimer = setTimeout(finishChange, finishChangeDelay); +}); +socket.on('online users', function (data) { + if (debug) + console.debug(data); + showStatus(statusType.online, data.count); + $('.other-cursors').html(''); + for(var i = 0; i < data.users.length; i++) { + var user = data.users[i]; + if(user.id != socket.id) + buildCursor(user.id, user.color, user.cursor); + } +}); +socket.on('cursor focus', function (data) { + if(debug) + console.debug(data); + var cursor = $('#' + data.id); + if(cursor.length > 0) { + cursor.fadeIn(); + } else { + if(data.id != socket.id) + buildCursor(data.id, data.color, data.cursor); + } +}); +socket.on('cursor activity', function (data) { + if(debug) + console.debug(data); + if(data.id != socket.id) + buildCursor(data.id, data.color, data.cursor); +}); +socket.on('cursor blur', function (data) { + if(debug) + console.debug(data); + var cursor = $('#' + data.id); + if(cursor.length > 0) { + cursor.fadeOut(); + } +}); +function emitUserStatus() { + checkIfAuth( + function (data) { + socket.emit('user status', {login:true}); + }, + function () { + socket.emit('user status', {login:false}); + } + ); +} + +function buildCursor(id, color, pos) { + if(!pos) return; + if ($('.other-cursors').length <= 0) { + $("
").insertAfter('.CodeMirror-cursors'); + } + if ($('#' + id).length <= 0) { + var cursor = $('
 
'); + //console.debug(pos); + cursor.attr('data-line', pos.line); + cursor.attr('data-ch', pos.ch); + var coord = editor.charCoords(pos, 'windows'); + cursor[0].style.left = coord.left + 'px'; + cursor[0].style.top = coord.top + 'px'; + cursor[0].style.height = '18px'; + cursor[0].style.borderLeft = '2px solid ' + color; + $('.other-cursors').append(cursor); + cursor.hide().fadeIn(); + } else { + var cursor = $('#' + id); + cursor.attr('data-line', pos.line); + cursor.attr('data-ch', pos.ch); + var coord = editor.charCoords(pos, 'windows'); + cursor.stop(true).css('opacity', 1).animate({"left":coord.left, "top":coord.top}, cursorAnimatePeriod); + //cursor[0].style.left = coord.left + 'px'; + //cursor[0].style.top = coord.top + 'px'; + cursor[0].style.height = '18px'; + cursor[0].style.borderLeft = '2px solid ' + color; + } +} + +//editor actions +editor.on('beforeChange', function (cm, change) { + if (debug) + console.debug(change); +}); +editor.on('change', function (i, op) { + if (debug) + console.debug(op); + if (op.origin != 'setValue' && op.origin != 'ignoreHistory') { + socket.emit('change', LZString.compressToBase64(JSON.stringify(op))); + } + isDirty = true; + clearTimeout(doneTypingTimer); + doneTypingTimer = setTimeout(doneTyping, doneTypingDelay); +}); +editor.on('focus', function (cm) { + socket.emit('cursor focus', editor.getCursor()); +}); +var cursorActivityTimer = null; +editor.on('cursorActivity', function (cm) { + clearTimeout(cursorActivityTimer); + cursorActivityTimer = setTimeout(cursorActivity, cursorActivityDelay); +}); +function cursorActivity() { + socket.emit('cursor activity', editor.getCursor()); +} +editor.on('blur', function (cm) { + socket.emit('cursor blur'); +}); + +function saveInfo() { + var left = $(document.body).scrollLeft(); + var top = $(document.body).scrollTop(); + switch (currentMode) { + case modeType.edit: + lastInfo.edit.scroll.left = left; + lastInfo.edit.scroll.top = top; + break; + case modeType.view: + lastInfo.view.scroll.left = left; + lastInfo.view.scroll.top = top; + break; + case modeType.both: + lastInfo.edit.scroll = editor.getScrollInfo(); + lastInfo.view.scroll.left = ui.area.view.scrollLeft(); + lastInfo.view.scroll.top = ui.area.view.scrollTop(); + break; + } + lastInfo.edit.cursor = editor.getCursor(); + lastInfo.needRestore = true; +} + +function restoreInfo() { + if (lastInfo.needRestore) { + var line = lastInfo.edit.cursor.line; + var ch = lastInfo.edit.cursor.ch; + editor.setCursor(line, ch); + + switch (currentMode) { + case modeType.edit: + $(document.body).scrollLeft(lastInfo.edit.scroll.left); + $(document.body).scrollTop(lastInfo.edit.scroll.top); + break; + case modeType.view: + $(document.body).scrollLeft(lastInfo.view.scroll.left); + $(document.body).scrollTop(lastInfo.view.scroll.top); + break; + case modeType.both: + var left = lastInfo.edit.scroll.left; + var top = lastInfo.edit.scroll.top; + editor.scrollIntoView(); + editor.scrollTo(left, top); + ui.area.view.scrollLeft(lastInfo.view.scroll.left); + ui.area.view.scrollTop(lastInfo.view.scroll.top); + break; + } + + lastInfo.needRestore = false; + } +} + +//view actions +var doneTypingTimer = null; +var finishChangeTimer = null; +var input = editor.getInputField(); +//user is "finished typing," do something +function doneTyping() { + updateView(); + var value = editor.getValue(); + socket.emit('refresh', LZString.compressToBase64(value)); +} + +function finishChange() { + updateView(); +} + +var lastResult = null; + +function updateView() { + if (currentMode == modeType.edit || !isDirty) return; + var value = editor.getValue(); + var result = postProcess(md.render(value)).children().toArray(); + //ui.area.markdown.html(result); + //finishView(ui.area.markdown); + partialUpdate(result, lastResult, ui.area.markdown.children().toArray()); + lastResult = $(result).clone(true); + finishView(ui.area.view); + writeHistory(ui.area.markdown); + isDirty = false; + // reset lines mapping cache on content update + scrollMap = null; + emitUserStatus(); +} + +function partialUpdate(src, tar, des) { + if (!src || src.length == 0 || !tar || tar.length == 0 || !des || des.length == 0) { + ui.area.markdown.html(src); + return; + } + if (src.length == tar.length) { //same length + for (var i = 0; i < src.length; i++) { + copyAttribute(src[i], des[i], 'data-startline'); + copyAttribute(src[i], des[i], 'data-endline'); + var rawSrc = cloneAndRemoveDataAttr(src[i]); + var rawTar = cloneAndRemoveDataAttr(tar[i]); + if (rawSrc.outerHTML != rawTar.outerHTML) { + //console.log(rawSrc); + //console.log(rawTar); + $(des[i]).replaceWith(src[i]); + } + } + } else { //diff length + var start = 0; + var end = 0; + //find diff start position + for (var i = 0; i < tar.length; i++) { + copyAttribute(src[i], des[i], 'data-startline'); + copyAttribute(src[i], des[i], 'data-endline'); + var rawSrc = cloneAndRemoveDataAttr(src[i]); + var rawTar = cloneAndRemoveDataAttr(tar[i]); + if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) { + start = i; + break; + } + } + //find diff end position + var srcEnd = 0; + var tarEnd = 0; + for (var i = 0; i < src.length; i++) { + copyAttribute(src[i], des[i], 'data-startline'); + copyAttribute(src[i], des[i], 'data-endline'); + var rawSrc = cloneAndRemoveDataAttr(src[i]); + var rawTar = cloneAndRemoveDataAttr(tar[i]); + if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) { + start = i; + break; + } + } + //tar end + for (var i = 1; i <= tar.length; i++) { + var srcLength = src.length; + var tarLength = tar.length; + copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline'); + copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline'); + var rawSrc = cloneAndRemoveDataAttr(src[srcLength - i]); + var rawTar = cloneAndRemoveDataAttr(tar[tarLength - i]); + if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) { + tarEnd = tar.length - i; + break; + } + } + //src end + for (var i = 1; i <= src.length; i++) { + var srcLength = src.length; + var tarLength = tar.length; + copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline'); + copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline'); + var rawSrc = cloneAndRemoveDataAttr(src[srcLength - i]); + var rawTar = cloneAndRemoveDataAttr(tar[tarLength - i]); + if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) { + srcEnd = src.length - i; + break; + } + } + //check if tar end overlap tar start + var overlap = 0; + for (var i = start; i >= 0; i--) { + var rawTarStart = cloneAndRemoveDataAttr(tar[i-1]); + var rawTarEnd = cloneAndRemoveDataAttr(tar[tarEnd+1+start-i]); + if(rawTarStart && rawTarEnd && rawTarStart.outerHTML == rawTarEnd.outerHTML) + overlap++; + else + break; + } + if(debug) + console.log('overlap:' + overlap); + //show diff content + if(debug) { + console.log('start:' + start); + console.log('tarEnd:' + tarEnd); + console.log('srcEnd:' + srcEnd); + console.log('des[start]:' + des[start]); + } + tarEnd += overlap; + srcEnd += overlap; + //add new element + var newElements = ""; + for (var j = start; j <= srcEnd; j++) { + if(debug) + srcChanged += src[j].outerHTML; + newElements += src[j].outerHTML; + } + if(newElements && des[start]) { + $(newElements).insertBefore(des[start]); + } else { + $(newElements).insertAfter(des[des.length-1]); + } + if(debug) + console.log(srcChanged); + //remove old element + if(debug) + var tarChanged = ""; + for (var j = start; j <= tarEnd; j++) { + if(debug) + tarChanged += tar[j].outerHTML; + if(des[j]) + des[j].remove(); + } + if(debug) { + console.log(tarChanged); + var srcChanged = ""; + } + } +} + +function cloneAndRemoveDataAttr(el) { + if(!el) return; + var rawEl = $(el).clone(true)[0]; + rawEl.removeAttribute('data-startline'); + rawEl.removeAttribute('data-endline'); + return rawEl; +} + +function copyAttribute(src, des, attr) { + if (src && src.getAttribute(attr) && des) + des.setAttribute(attr, src.getAttribute(attr)); +} + +// +// Inject line numbers for sync scroll. Notes: +// +// - We track only headings and paragraphs on first level. That's enougth. +// - Footnotes content causes jumps. Level limit filter it automatically. +// +md.renderer.rules.paragraph_open = function (tokens, idx) { + var line; + if (tokens[idx].lines && tokens[idx].level === 0) { + var startline = tokens[idx].lines[0] + 1; + var endline = tokens[idx].lines[1]; + return '

'; + } + return ''; +}; + +md.renderer.rules.heading_open = function (tokens, idx) { + var line; + if (tokens[idx].lines && tokens[idx].level === 0) { + var startline = tokens[idx].lines[0] + 1; + var endline = tokens[idx].lines[1]; + return ''; + } + return ''; +}; + +editor.on('scroll', _.debounce(syncScrollToView, syncScrollDelay)); +//ui.area.view.on('scroll', _.debounce(syncScrollToEdit, 50)); +var scrollMap; +// Build offsets for each line (lines can be wrapped) +// That's a bit dirty to process each line everytime, but ok for demo. +// Optimizations are required only for big texts. +function buildScrollMap() { + var i, offset, nonEmptyList, pos, a, b, lineHeightMap, linesCount, + acc, sourceLikeDiv, textarea = ui.area.codemirror, + _scrollMap; + + sourceLikeDiv = $('

').css({ + position: 'absolute', + visibility: 'hidden', + height: 'auto', + width: editor.getScrollInfo().clientWidth, + 'font-size': textarea.css('font-size'), + 'font-family': textarea.css('font-family'), + 'line-height': textarea.css('line-height'), + 'white-space': textarea.css('white-space') + }).appendTo('body'); + + offset = ui.area.view.scrollTop() - ui.area.view.offset().top; + _scrollMap = []; + nonEmptyList = []; + lineHeightMap = []; + + acc = 0; + editor.getValue().split('\n').forEach(function (str) { + var h, lh; + + lineHeightMap.push(acc); + + if (str.length === 0) { + acc++; + return; + } + + sourceLikeDiv.text(str); + h = parseFloat(sourceLikeDiv.css('height')); + lh = parseFloat(sourceLikeDiv.css('line-height')); + acc += Math.round(h / lh); + }); + sourceLikeDiv.remove(); + lineHeightMap.push(acc); + linesCount = acc; + + for (i = 0; i < linesCount; i++) { + _scrollMap.push(-1); + } + + nonEmptyList.push(0); + _scrollMap[0] = 0; + + ui.area.markdown.find('.part').each(function (n, el) { + var $el = $(el), + t = $el.data('startline'); + if (t === '') { + return; + } + t = lineHeightMap[t]; + if (t !== 0) { + nonEmptyList.push(t); + } + _scrollMap[t] = Math.round($el.offset().top + offset); + }); + + nonEmptyList.push(linesCount); + _scrollMap[linesCount] = ui.area.view[0].scrollHeight; + + pos = 0; + for (i = 1; i < linesCount; i++) { + if (_scrollMap[i] !== -1) { + pos++; + continue; + } + + a = nonEmptyList[pos]; + b = nonEmptyList[pos + 1]; + _scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a)); + } + + return _scrollMap; +} + +function syncScrollToView() { + var lineNo, posTo; + var scrollInfo = editor.getScrollInfo(); + if (!scrollMap) { + scrollMap = buildScrollMap(); + } + lineNo = Math.floor(scrollInfo.top / editor.defaultTextHeight()); + posTo = scrollMap[lineNo]; + ui.area.view.stop(true).animate({scrollTop: posTo}, scrollAnimatePeriod); +} + +function syncScrollToEdit() { + var lineNo, posTo; + if (!scrollMap) { + scrollMap = buildScrollMap(); + } + var top = ui.area.view.scrollTop(); + lineNo = closestIndex(top, scrollMap); + posTo = lineNo * editor.defaultTextHeight(); + editor.scrollTo(0, posTo); +} + +function closestIndex(num, arr) { + var curr = arr[0]; + var index = 0; + var diff = Math.abs(num - curr); + for (var val = 0; val < arr.length; val++) { + var newdiff = Math.abs(num - arr[val]); + if (newdiff < diff) { + diff = newdiff; + curr = arr[val]; + index = val; + } + } + return index; +} \ No newline at end of file -- cgit v1.2.3