diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index a8e6fff30..b03de5628 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -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() } @@ -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) @@ -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) } @@ -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) { diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index c8ca9b98b..3f484608d 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -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) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 96b7e0654..2584effb3 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -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 diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 25991578e..790fecc23 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -13,7 +13,7 @@ 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, @@ -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) { this.refs.code.setValue(value) } @@ -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)) } } @@ -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, @@ -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} @@ -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}