mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
Merge pull request #2405 from daiyam/fix-scroll
Better scroll sync between the editor and the preview in the SplitEditor
This commit is contained in:
@@ -170,6 +170,10 @@ export default class CodeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleEditorActivity() {
|
handleEditorActivity() {
|
||||||
|
if (this.props.onCursorActivity) {
|
||||||
|
this.props.onCursorActivity(this.editor)
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.textEditorInterface.transaction) {
|
if (!this.textEditorInterface.transaction) {
|
||||||
this.updateTableEditorState()
|
this.updateTableEditorState()
|
||||||
}
|
}
|
||||||
@@ -380,6 +384,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
|
|
||||||
eventEmitter.emit('code:init')
|
eventEmitter.emit('code:init')
|
||||||
this.editor.on('scroll', this.scrollHandler)
|
this.editor.on('scroll', this.scrollHandler)
|
||||||
|
this.editor.on('cursorActivity', this.editorActivityHandler)
|
||||||
|
|
||||||
const editorTheme = document.getElementById('editorTheme')
|
const editorTheme = document.getElementById('editorTheme')
|
||||||
editorTheme.addEventListener('load', this.loadStyleHandler)
|
editorTheme.addEventListener('load', this.loadStyleHandler)
|
||||||
@@ -517,7 +522,6 @@ export default class CodeEditor extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (this.props.enableTableEditor) {
|
if (this.props.enableTableEditor) {
|
||||||
this.editor.on('cursorActivity', this.editorActivityHandler)
|
|
||||||
this.editor.on('changes', this.editorActivityHandler)
|
this.editor.on('changes', this.editorActivityHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,12 +580,18 @@ export default class CodeEditor extends React.Component {
|
|||||||
this.editor.off('paste', this.pasteHandler)
|
this.editor.off('paste', this.pasteHandler)
|
||||||
eventEmitter.off('top:search', this.searchHandler)
|
eventEmitter.off('top:search', this.searchHandler)
|
||||||
this.editor.off('scroll', this.scrollHandler)
|
this.editor.off('scroll', this.scrollHandler)
|
||||||
|
this.editor.off('cursorActivity', this.editorActivityHandler)
|
||||||
this.editor.off('contextmenu', this.contextMenuHandler)
|
this.editor.off('contextmenu', this.contextMenuHandler)
|
||||||
|
|
||||||
const editorTheme = document.getElementById('editorTheme')
|
const editorTheme = document.getElementById('editorTheme')
|
||||||
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
editorTheme.removeEventListener('load', this.loadStyleHandler)
|
||||||
|
|
||||||
spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED)
|
spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED)
|
||||||
eventEmitter.off('code:format-table', this.formatTable)
|
eventEmitter.off('code:format-table', this.formatTable)
|
||||||
|
|
||||||
|
if (this.props.enableTableEditor) {
|
||||||
|
this.editor.off('changes', this.editorActivityHandler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.previewRef.current.focus()
|
this.previewRef.current.focus()
|
||||||
this.previewRef.current.scrollToRow(cursorPosition.line)
|
this.previewRef.current.scrollToLine(cursorPosition.line)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||||
|
|||||||
@@ -1145,17 +1145,18 @@ class MarkdownPreview extends React.Component {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
* @param {Number} targetRow
|
* @param {Number} targetLine
|
||||||
*/
|
*/
|
||||||
scrollToRow(targetRow) {
|
scrollToLine(targetLine) {
|
||||||
const blocks = this.getWindow().document.querySelectorAll(
|
const blocks = this.getWindow().document.querySelectorAll(
|
||||||
'body>[data-line]'
|
'body [data-line]'
|
||||||
)
|
)
|
||||||
|
|
||||||
for (let index = 0; index < blocks.length; index++) {
|
for (let index = 0; index < blocks.length; index++) {
|
||||||
let block = blocks[index]
|
let block = blocks[index]
|
||||||
const row = parseInt(block.getAttribute('data-line'))
|
const line = parseInt(block.getAttribute('data-line'))
|
||||||
if (row > targetRow || index === blocks.length - 1) {
|
|
||||||
|
if (line > targetLine || index === blocks.length - 1) {
|
||||||
block = blocks[index - 1]
|
block = blocks[index - 1]
|
||||||
block != null && this.scrollTo(0, block.offsetTop)
|
block != null && this.scrollTo(0, block.offsetTop)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
this.value = props.value
|
this.value = props.value
|
||||||
this.focus = () => this.refs.code.focus()
|
this.focus = () => this.refs.code.focus()
|
||||||
this.reload = () => this.refs.code.reload()
|
this.reload = () => this.refs.code.reload()
|
||||||
this.userScroll = true
|
this.userScroll = props.config.preview.scrollSync
|
||||||
this.state = {
|
this.state = {
|
||||||
isSliderFocused: false,
|
isSliderFocused: false,
|
||||||
codeEditorWidthInPercent: 50,
|
codeEditorWidthInPercent: 50,
|
||||||
@@ -21,6 +21,72 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
setValue(value) {
|
||||||
this.refs.code.setValue(value)
|
this.refs.code.setValue(value)
|
||||||
}
|
}
|
||||||
@@ -30,59 +96,125 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
this.props.onChange(e)
|
this.props.onChange(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleScroll(e) {
|
handleEditorScroll(e) {
|
||||||
if (!this.props.config.preview.scrollSync) return
|
if (this.userScroll) {
|
||||||
|
|
||||||
const previewDoc = _.get(
|
const previewDoc = _.get(
|
||||||
this,
|
this,
|
||||||
'refs.preview.refs.root.contentWindow.document'
|
'refs.preview.refs.root.contentWindow.document'
|
||||||
)
|
)
|
||||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||||
let srcTop, srcHeight, targetTop, targetHeight
|
|
||||||
|
|
||||||
if (this.userScroll) {
|
const from = codeDoc.cm.coordsChar({ left: 0, top: 0 }).line
|
||||||
if (e.doc) {
|
const to = codeDoc.cm.coordsChar({
|
||||||
srcTop = _.get(e, 'doc.scrollTop')
|
left: 0,
|
||||||
srcHeight = _.get(e, 'doc.height')
|
top: codeDoc.cm.display.lastWrapHeight * 1.125
|
||||||
targetTop = _.get(previewDoc, 'body.scrollTop')
|
}).line
|
||||||
targetHeight = _.get(previewDoc, 'body.scrollHeight')
|
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 {
|
} else {
|
||||||
srcTop = _.get(previewDoc, 'body.scrollTop')
|
const line = from + Math.floor((to - from) / 3)
|
||||||
srcHeight = _.get(previewDoc, 'body.scrollHeight')
|
|
||||||
targetTop = _.get(codeDoc, 'scrollTop')
|
const blockElements = previewDoc.querySelectorAll('body [data-line]')
|
||||||
targetHeight = _.get(codeDoc, 'height')
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const distance = (targetHeight * srcTop) / srcHeight - targetTop
|
if (blocks.length === 1) {
|
||||||
const framerate = 1000 / 60
|
const block = blockElements[blockElements.length - 1]
|
||||||
const frames = 20
|
|
||||||
const refractory = frames * framerate
|
|
||||||
|
|
||||||
this.userScroll = false
|
blocks.push({
|
||||||
|
line: codeDoc.size,
|
||||||
|
top: block.offsetTop + block.offsetHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let frame = 0
|
const i = blocks.length - 1
|
||||||
let scrollPos, time
|
|
||||||
const timer = setInterval(() => {
|
const ratio =
|
||||||
time = frame / frames
|
(blocks[i].top - blocks[i - 1].top) /
|
||||||
scrollPos =
|
(blocks[i].line - blocks[i - 1].line)
|
||||||
time < 0.5
|
|
||||||
? 2 * time * time // ease in
|
top =
|
||||||
: -1 + (4 - 2 * time) * time // ease out
|
blocks[i - 1].top + Math.floor((line - blocks[i - 1].line) * ratio)
|
||||||
if (e.doc)
|
}
|
||||||
_.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
|
|
||||||
else
|
this.scrollTo(previewTop, top, y =>
|
||||||
_.get(this, 'refs.code.editor').scrollTo(
|
_.set(previewDoc, 'body.scrollTop', y)
|
||||||
0,
|
|
||||||
targetTop + scrollPos * distance
|
|
||||||
)
|
)
|
||||||
if (frame >= frames) {
|
|
||||||
clearInterval(timer)
|
|
||||||
setTimeout(() => {
|
|
||||||
this.userScroll = true
|
|
||||||
}, refractory)
|
|
||||||
}
|
}
|
||||||
frame++
|
}
|
||||||
}, framerate)
|
|
||||||
|
handlePreviewScroll(e) {
|
||||||
|
if (this.userScroll) {
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
if (top > previewTop) {
|
||||||
|
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 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
@@ -280,7 +441,8 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
noteKey={noteKey}
|
noteKey={noteKey}
|
||||||
linesHighlighted={linesHighlighted}
|
linesHighlighted={linesHighlighted}
|
||||||
onChange={e => this.handleOnChange(e)}
|
onChange={e => this.handleOnChange(e)}
|
||||||
onScroll={this.handleScroll.bind(this)}
|
onScroll={e => this.handleEditorScroll(e)}
|
||||||
|
onCursorActivity={e => this.handleCursorActivity(e)}
|
||||||
spellCheck={config.editor.spellcheck}
|
spellCheck={config.editor.spellcheck}
|
||||||
enableSmartPaste={config.editor.enableSmartPaste}
|
enableSmartPaste={config.editor.enableSmartPaste}
|
||||||
hotkey={config.hotkey}
|
hotkey={config.hotkey}
|
||||||
@@ -317,7 +479,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
tabInde='0'
|
tabInde='0'
|
||||||
value={value}
|
value={value}
|
||||||
onCheckboxClick={e => this.handleCheckboxClick(e)}
|
onCheckboxClick={e => this.handleCheckboxClick(e)}
|
||||||
onScroll={this.handleScroll.bind(this)}
|
onScroll={e => this.handlePreviewScroll(e)}
|
||||||
showCopyNotification={config.ui.showCopyNotification}
|
showCopyNotification={config.ui.showCopyNotification}
|
||||||
storagePath={storage.path}
|
storagePath={storage.path}
|
||||||
noteKey={noteKey}
|
noteKey={noteKey}
|
||||||
|
|||||||
Reference in New Issue
Block a user