Skip to content

Commit

Permalink
Merge pull request #2405 from daiyam/fix-scroll
Browse files Browse the repository at this point in the history
Better scroll sync between the editor and the preview in the SplitEditor
  • Loading branch information
Rokt33r authored Jul 20, 2020
2 parents 1cdc74a + 81ac3d1 commit 4c39922
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 57 deletions.
12 changes: 11 additions & 1 deletion browser/components/CodeEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ export default class CodeEditor extends React.Component {
}

handleEditorActivity() {
if (this.props.onCursorActivity) {
this.props.onCursorActivity(this.editor)
}

if (!this.textEditorInterface.transaction) {
this.updateTableEditorState()
}
Expand Down Expand Up @@ -380,6 +384,7 @@ export default class CodeEditor extends React.Component {

eventEmitter.emit('code:init')
this.editor.on('scroll', this.scrollHandler)
this.editor.on('cursorActivity', this.editorActivityHandler)

const editorTheme = document.getElementById('editorTheme')
editorTheme.addEventListener('load', this.loadStyleHandler)
Expand Down Expand Up @@ -517,7 +522,6 @@ export default class CodeEditor extends React.Component {
})

if (this.props.enableTableEditor) {
this.editor.on('cursorActivity', this.editorActivityHandler)
this.editor.on('changes', this.editorActivityHandler)
}

Expand Down Expand Up @@ -576,12 +580,18 @@ export default class CodeEditor extends React.Component {
this.editor.off('paste', this.pasteHandler)
eventEmitter.off('top:search', this.searchHandler)
this.editor.off('scroll', this.scrollHandler)
this.editor.off('cursorActivity', this.editorActivityHandler)
this.editor.off('contextmenu', this.contextMenuHandler)

const editorTheme = document.getElementById('editorTheme')
editorTheme.removeEventListener('load', this.loadStyleHandler)

spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED)
eventEmitter.off('code:format-table', this.formatTable)

if (this.props.enableTableEditor) {
this.editor.off('changes', this.editorActivityHandler)
}
}

componentDidUpdate(prevProps, prevState) {
Expand Down
2 changes: 1 addition & 1 deletion browser/components/MarkdownEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class MarkdownEditor extends React.Component {
},
() => {
this.previewRef.current.focus()
this.previewRef.current.scrollToRow(cursorPosition.line)
this.previewRef.current.scrollToLine(cursorPosition.line)
}
)
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
Expand Down
11 changes: 6 additions & 5 deletions browser/components/MarkdownPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -1145,17 +1145,18 @@ class MarkdownPreview extends React.Component {

/**
* @public
* @param {Number} targetRow
* @param {Number} targetLine
*/
scrollToRow(targetRow) {
scrollToLine(targetLine) {
const blocks = this.getWindow().document.querySelectorAll(
'body>[data-line]'
'body [data-line]'
)

for (let index = 0; index < blocks.length; index++) {
let block = blocks[index]
const row = parseInt(block.getAttribute('data-line'))
if (row > targetRow || index === blocks.length - 1) {
const line = parseInt(block.getAttribute('data-line'))

if (line > targetLine || index === blocks.length - 1) {
block = blocks[index - 1]
block != null && this.scrollTo(0, block.offsetTop)
break
Expand Down
262 changes: 212 additions & 50 deletions browser/components/MarkdownSplitEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,80 @@ class MarkdownSplitEditor extends React.Component {
this.value = props.value
this.focus = () => this.refs.code.focus()
this.reload = () => this.refs.code.reload()
this.userScroll = true
this.userScroll = props.config.preview.scrollSync
this.state = {
isSliderFocused: false,
codeEditorWidthInPercent: 50,
codeEditorHeightInPercent: 50
}
}

componentDidUpdate(prevProps) {
if (
this.props.config.preview.scrollSync !==
prevProps.config.preview.scrollSync
) {
this.userScroll = this.props.config.preview.scrollSync
}
}

handleCursorActivity(editor) {
if (this.userScroll) {
const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const previewTop = _.get(previewDoc, 'body.scrollTop')

const line = editor.doc.getCursor().line
let top
if (line === 0) {
top = 0
} else {
const blockElements = previewDoc.querySelectorAll('body [data-line]')
const blocks = []
for (const block of blockElements) {
const l = parseInt(block.getAttribute('data-line'))

blocks.push({
line: l,
top: block.offsetTop
})

if (l > line) {
break
}
}

if (blocks.length === 1) {
const block = blockElements[blockElements.length - 1]

blocks.push({
line: editor.doc.size,
top: block.offsetTop + block.offsetHeight
})
}

const i = blocks.length - 1

const ratio =
(blocks[i].top - blocks[i - 1].top) /
(blocks[i].line - blocks[i - 1].line)

const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3)

top =
blocks[i - 1].top +
Math.floor((line - blocks[i - 1].line) * ratio) -
delta
}

this.scrollTo(previewTop, top, y =>
_.set(previewDoc, 'body.scrollTop', y)
)
}
}

setValue(value) {
this.refs.code.setValue(value)
}
Expand All @@ -30,59 +96,125 @@ class MarkdownSplitEditor extends React.Component {
this.props.onChange(e)
}

handleScroll(e) {
if (!this.props.config.preview.scrollSync) return
handleEditorScroll(e) {
if (this.userScroll) {
const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const codeDoc = _.get(this, 'refs.code.editor.doc')

const from = codeDoc.cm.coordsChar({ left: 0, top: 0 }).line
const to = codeDoc.cm.coordsChar({
left: 0,
top: codeDoc.cm.display.lastWrapHeight * 1.125
}).line
const previewTop = _.get(previewDoc, 'body.scrollTop')

let top
if (from === 0) {
top = 0
} else if (to === codeDoc.lastLine()) {
top =
_.get(previewDoc, 'body.scrollHeight') -
_.get(previewDoc, 'body.clientHeight')
} else {
const line = from + Math.floor((to - from) / 3)

const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const codeDoc = _.get(this, 'refs.code.editor.doc')
let srcTop, srcHeight, targetTop, targetHeight
const blockElements = previewDoc.querySelectorAll('body [data-line]')
const blocks = []
for (const block of blockElements) {
const l = parseInt(block.getAttribute('data-line'))

blocks.push({
line: l,
top: block.offsetTop
})

if (l > line) {
break
}
}

if (blocks.length === 1) {
const block = blockElements[blockElements.length - 1]

blocks.push({
line: codeDoc.size,
top: block.offsetTop + block.offsetHeight
})
}

const i = blocks.length - 1

const ratio =
(blocks[i].top - blocks[i - 1].top) /
(blocks[i].line - blocks[i - 1].line)

top =
blocks[i - 1].top + Math.floor((line - blocks[i - 1].line) * ratio)
}

this.scrollTo(previewTop, top, y =>
_.set(previewDoc, 'body.scrollTop', y)
)
}
}

handlePreviewScroll(e) {
if (this.userScroll) {
if (e.doc) {
srcTop = _.get(e, 'doc.scrollTop')
srcHeight = _.get(e, 'doc.height')
targetTop = _.get(previewDoc, 'body.scrollTop')
targetHeight = _.get(previewDoc, 'body.scrollHeight')
const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const codeDoc = _.get(this, 'refs.code.editor.doc')

const srcTop = _.get(previewDoc, 'body.scrollTop')
const editorTop = _.get(codeDoc, 'scrollTop')

let top
if (srcTop === 0) {
top = 0
} else {
srcTop = _.get(previewDoc, 'body.scrollTop')
srcHeight = _.get(previewDoc, 'body.scrollHeight')
targetTop = _.get(codeDoc, 'scrollTop')
targetHeight = _.get(codeDoc, 'height')
}
const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3)
const previewTop = srcTop + delta

const blockElements = previewDoc.querySelectorAll('body [data-line]')
const blocks = []
for (const block of blockElements) {
const top = block.offsetTop

blocks.push({
line: parseInt(block.getAttribute('data-line')),
top
})

const distance = (targetHeight * srcTop) / srcHeight - targetTop
const framerate = 1000 / 60
const frames = 20
const refractory = frames * framerate

this.userScroll = false

let frame = 0
let scrollPos, time
const timer = setInterval(() => {
time = frame / frames
scrollPos =
time < 0.5
? 2 * time * time // ease in
: -1 + (4 - 2 * time) * time // ease out
if (e.doc)
_.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
else
_.get(this, 'refs.code.editor').scrollTo(
0,
targetTop + scrollPos * distance
)
if (frame >= frames) {
clearInterval(timer)
setTimeout(() => {
this.userScroll = true
}, refractory)
if (top > previewTop) {
break
}
}

if (blocks.length === 1) {
const block = blockElements[blockElements.length - 1]

blocks.push({
line: codeDoc.size,
top: block.offsetTop + block.offsetHeight
})
}
frame++
}, framerate)

const i = blocks.length - 1

const from = codeDoc.cm.heightAtLine(blocks[i - 1].line, 'local')
const to = codeDoc.cm.heightAtLine(blocks[i].line, 'local')

const ratio =
(previewTop - blocks[i - 1].top) / (blocks[i].top - blocks[i - 1].top)

top = from + Math.floor((to - from) * ratio) - delta
}

this.scrollTo(editorTop, top, y => codeDoc.cm.scrollTo(0, y))
}
}

Expand Down Expand Up @@ -168,6 +300,35 @@ class MarkdownSplitEditor extends React.Component {
})
}

scrollTo(from, to, scroller) {
const distance = to - from
const framerate = 1000 / 60
const frames = 20
const refractory = frames * framerate

this.userScroll = false

let frame = 0
let scrollPos, time
const timer = setInterval(() => {
time = frame / frames
scrollPos =
time < 0.5
? 2 * time * time // ease in
: -1 + (4 - 2 * time) * time // ease out

scroller(from + scrollPos * distance)

if (frame >= frames) {
clearInterval(timer)
setTimeout(() => {
this.userScroll = true
}, refractory)
}
frame++
}, framerate)
}

render() {
const {
config,
Expand Down Expand Up @@ -280,7 +441,8 @@ class MarkdownSplitEditor extends React.Component {
noteKey={noteKey}
linesHighlighted={linesHighlighted}
onChange={e => this.handleOnChange(e)}
onScroll={this.handleScroll.bind(this)}
onScroll={e => this.handleEditorScroll(e)}
onCursorActivity={e => this.handleCursorActivity(e)}
spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
Expand Down Expand Up @@ -317,7 +479,7 @@ class MarkdownSplitEditor extends React.Component {
tabInde='0'
value={value}
onCheckboxClick={e => this.handleCheckboxClick(e)}
onScroll={this.handleScroll.bind(this)}
onScroll={e => this.handlePreviewScroll(e)}
showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path}
noteKey={noteKey}
Expand Down

0 comments on commit 4c39922

Please sign in to comment.