HackMD/public/js/lib/syncscroll.js
Sheogorath 4da68597f7
Fix eslint warnings
Since we are about to release it's time to finally fix our linting. This
patch basically runs eslint --fix and does some further manual fixes.
Also it sets up eslint to fail on every warning on order to make
warnings visable in the CI process.

There should no functional change be introduced.

Signed-off-by: Sheogorath <sheogorath@shivering-isles.com>
2019-05-31 00:30:29 +02:00

372 lines
11 KiB
JavaScript

/* eslint-env browser, jquery */
/* global _ */
// Inject line numbers for sync scroll.
import markdownitContainer from 'markdown-it-container'
import { md } from '../extra'
import modeType from './modeType'
import appState from './appState'
function addPart (tokens, idx) {
if (tokens[idx].map && tokens[idx].level === 0) {
const startline = tokens[idx].map[0] + 1
const endline = tokens[idx].map[1]
tokens[idx].attrJoin('class', 'part')
tokens[idx].attrJoin('data-startline', startline)
tokens[idx].attrJoin('data-endline', endline)
}
}
md.renderer.rules.blockquote_open = function (tokens, idx, options, env, self) {
tokens[idx].attrJoin('class', 'raw')
addPart(tokens, idx)
return self.renderToken(...arguments)
}
md.renderer.rules.table_open = function (tokens, idx, options, env, self) {
addPart(tokens, idx)
return self.renderToken(...arguments)
}
md.renderer.rules.bullet_list_open = function (tokens, idx, options, env, self) {
addPart(tokens, idx)
return self.renderToken(...arguments)
}
md.renderer.rules.list_item_open = function (tokens, idx, options, env, self) {
tokens[idx].attrJoin('class', 'raw')
if (tokens[idx].map) {
const startline = tokens[idx].map[0] + 1
const endline = tokens[idx].map[1]
tokens[idx].attrJoin('data-startline', startline)
tokens[idx].attrJoin('data-endline', endline)
}
return self.renderToken(...arguments)
}
md.renderer.rules.ordered_list_open = function (tokens, idx, options, env, self) {
addPart(tokens, idx)
return self.renderToken(...arguments)
}
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
addPart(tokens, idx)
return self.renderToken(...arguments)
}
md.renderer.rules.paragraph_open = function (tokens, idx, options, env, self) {
addPart(tokens, idx)
return self.renderToken(...arguments)
}
md.renderer.rules.heading_open = function (tokens, idx, options, env, self) {
tokens[idx].attrJoin('class', 'raw')
addPart(tokens, idx)
return self.renderToken(...arguments)
}
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx]
const info = token.info ? md.utils.unescapeAll(token.info).trim() : ''
let langName = ''
let highlighted
if (info) {
langName = info.split(/\s+/g)[0]
if (/!$/.test(info)) token.attrJoin('class', 'wrap')
token.attrJoin('class', options.langPrefix + langName.replace(/=$|=\d+$|=\+$|!$|=!/, ''))
token.attrJoin('class', 'hljs')
token.attrJoin('class', 'raw')
}
if (options.highlight) {
highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content)
} else {
highlighted = md.utils.escapeHtml(token.content)
}
if (highlighted.indexOf('<pre') === 0) {
return `${highlighted}\n`
}
if (tokens[idx].map && tokens[idx].level === 0) {
const startline = tokens[idx].map[0] + 1
const endline = tokens[idx].map[1]
return `<pre class="part" data-startline="${startline}" data-endline="${endline}"><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`
}
return `<pre><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`
}
md.renderer.rules.code_block = (tokens, idx, options, env, self) => {
if (tokens[idx].map && tokens[idx].level === 0) {
const startline = tokens[idx].map[0] + 1
const endline = tokens[idx].map[1]
return `<pre class="part" data-startline="${startline}" data-endline="${endline}"><code>${md.utils.escapeHtml(tokens[idx].content)}</code></pre>\n`
}
return `<pre><code>${md.utils.escapeHtml(tokens[idx].content)}</code></pre>\n`
}
function renderContainer (tokens, idx, options, env, self) {
tokens[idx].attrJoin('role', 'alert')
tokens[idx].attrJoin('class', 'alert')
tokens[idx].attrJoin('class', `alert-${tokens[idx].info.trim()}`)
addPart(tokens, idx)
return self.renderToken(...arguments)
}
md.use(markdownitContainer, 'success', { render: renderContainer })
md.use(markdownitContainer, 'info', { render: renderContainer })
md.use(markdownitContainer, 'warning', { render: renderContainer })
md.use(markdownitContainer, 'danger', { render: renderContainer })
window.preventSyncScrollToEdit = false
window.preventSyncScrollToView = false
const editScrollThrottle = 5
const viewScrollThrottle = 5
const buildMapThrottle = 100
let viewScrolling = false
let editScrolling = false
let editArea = null
let viewArea = null
let markdownArea = null
let editor
export function setupSyncAreas (edit, view, markdown, _editor) {
editArea = edit
viewArea = view
markdownArea = markdown
editor = _editor
editArea.on('scroll', _.throttle(syncScrollToView, editScrollThrottle))
viewArea.on('scroll', _.throttle(syncScrollToEdit, viewScrollThrottle))
}
let scrollMap, lineHeightMap, viewTop, viewBottom
export function clearMap () {
scrollMap = null
lineHeightMap = null
viewTop = null
viewBottom = null
}
window.viewAjaxCallback = clearMap
const buildMap = _.throttle(buildMapInner, buildMapThrottle)
// 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 buildMapInner (callback) {
if (!viewArea || !markdownArea) return
let i, offset, nonEmptyList, pos, a, b, _lineHeightMap, linesCount, acc, _scrollMap
offset = viewArea.scrollTop() - viewArea.offset().top
_scrollMap = []
nonEmptyList = []
_lineHeightMap = []
viewTop = 0
viewBottom = viewArea[0].scrollHeight - viewArea.height()
acc = 0
const lines = editor.getValue().split('\n')
const lineHeight = editor.defaultTextHeight()
for (i = 0; i < lines.length; i++) {
const str = lines[i]
_lineHeightMap.push(acc)
if (str.length === 0) {
acc++
continue
}
const h = editor.heightAtLine(i + 1) - editor.heightAtLine(i)
acc += Math.round(h / lineHeight)
}
_lineHeightMap.push(acc)
linesCount = acc
for (i = 0; i < linesCount; i++) {
_scrollMap.push(-1)
}
nonEmptyList.push(0)
// make the first line go top
_scrollMap[0] = viewTop
const parts = markdownArea.find('.part').toArray()
for (i = 0; i < parts.length; i++) {
const $el = $(parts[i])
let t = $el.attr('data-startline') - 1
if (t === '') {
return
}
t = _lineHeightMap[t]
if (t !== 0 && t !== nonEmptyList[nonEmptyList.length - 1]) {
nonEmptyList.push(t)
}
_scrollMap[t] = Math.round($el.offset().top + offset - 10)
}
nonEmptyList.push(linesCount)
_scrollMap[linesCount] = viewArea[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))
}
_scrollMap[0] = 0
scrollMap = _scrollMap
lineHeightMap = _lineHeightMap
if (window.loaded && callback) callback()
}
// sync view scroll progress to edit
let viewScrollingTimer = null
export function syncScrollToEdit (event, preventAnimate) {
if (appState.currentMode !== modeType.both || !appState.syncscroll || !editArea) return
if (window.preventSyncScrollToEdit) {
if (typeof window.preventSyncScrollToEdit === 'number') {
window.preventSyncScrollToEdit--
} else {
window.preventSyncScrollToEdit = false
}
return
}
if (!scrollMap || !lineHeightMap) {
buildMap(() => {
syncScrollToEdit(event, preventAnimate)
})
return
}
if (editScrolling) return
const scrollTop = viewArea[0].scrollTop
let lineIndex = 0
for (let i = 0, l = scrollMap.length; i < l; i++) {
if (scrollMap[i] > scrollTop) {
break
} else {
lineIndex = i
}
}
let lineNo = 0
let lineDiff = 0
for (let i = 0, l = lineHeightMap.length; i < l; i++) {
if (lineHeightMap[i] > lineIndex) {
break
} else {
lineNo = lineHeightMap[i]
lineDiff = lineHeightMap[i + 1] - lineNo
}
}
let posTo = 0
let topDiffPercent = 0
let posToNextDiff = 0
const scrollInfo = editor.getScrollInfo()
const textHeight = editor.defaultTextHeight()
const preLastLineHeight = scrollInfo.height - scrollInfo.clientHeight - textHeight
const preLastLineNo = Math.round(preLastLineHeight / textHeight)
const preLastLinePos = scrollMap[preLastLineNo]
if (scrollInfo.height > scrollInfo.clientHeight && scrollTop >= preLastLinePos) {
posTo = preLastLineHeight
topDiffPercent = (scrollTop - preLastLinePos) / (viewBottom - preLastLinePos)
posToNextDiff = textHeight * topDiffPercent
posTo += Math.ceil(posToNextDiff)
} else {
posTo = lineNo * textHeight
topDiffPercent = (scrollTop - scrollMap[lineNo]) / (scrollMap[lineNo + lineDiff] - scrollMap[lineNo])
posToNextDiff = textHeight * lineDiff * topDiffPercent
posTo += Math.ceil(posToNextDiff)
}
if (preventAnimate) {
editArea.scrollTop(posTo)
} else {
const posDiff = Math.abs(scrollInfo.top - posTo)
var duration = posDiff / 50
duration = duration >= 100 ? duration : 100
editArea.stop(true, true).animate({
scrollTop: posTo
}, duration, 'linear')
}
viewScrolling = true
clearTimeout(viewScrollingTimer)
viewScrollingTimer = setTimeout(viewScrollingTimeoutInner, duration * 1.5)
}
function viewScrollingTimeoutInner () {
viewScrolling = false
}
// sync edit scroll progress to view
let editScrollingTimer = null
export function syncScrollToView (event, preventAnimate) {
if (appState.currentMode !== modeType.both || !appState.syncscroll || !viewArea) return
if (window.preventSyncScrollToView) {
if (typeof preventSyncScrollToView === 'number') {
window.preventSyncScrollToView--
} else {
window.preventSyncScrollToView = false
}
return
}
if (!scrollMap || !lineHeightMap) {
buildMap(() => {
syncScrollToView(event, preventAnimate)
})
return
}
if (viewScrolling) return
let lineNo, posTo
let topDiffPercent, posToNextDiff
const scrollInfo = editor.getScrollInfo()
const textHeight = editor.defaultTextHeight()
lineNo = Math.floor(scrollInfo.top / textHeight)
// if reach the last line, will start lerp to the bottom
const diffToBottom = (scrollInfo.top + scrollInfo.clientHeight) - (scrollInfo.height - textHeight)
if (scrollInfo.height > scrollInfo.clientHeight && diffToBottom > 0) {
topDiffPercent = diffToBottom / textHeight
posTo = scrollMap[lineNo + 1]
posToNextDiff = (viewBottom - posTo) * topDiffPercent
posTo += Math.floor(posToNextDiff)
} else {
topDiffPercent = (scrollInfo.top % textHeight) / textHeight
posTo = scrollMap[lineNo]
posToNextDiff = (scrollMap[lineNo + 1] - posTo) * topDiffPercent
posTo += Math.floor(posToNextDiff)
}
if (preventAnimate) {
viewArea.scrollTop(posTo)
} else {
const posDiff = Math.abs(viewArea.scrollTop() - posTo)
var duration = posDiff / 50
duration = duration >= 100 ? duration : 100
viewArea.stop(true, true).animate({
scrollTop: posTo
}, duration, 'linear')
}
editScrolling = true
clearTimeout(editScrollingTimer)
editScrollingTimer = setTimeout(editScrollingTimeoutInner, duration * 1.5)
}
function editScrollingTimeoutInner () {
editScrolling = false
}