diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 74b0a4089..3d27c1c3b 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -47,6 +47,7 @@ export default class CodeEditor extends React.Component { this.loadStyleHandler = (e) => { this.editor.refresh() } + this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true}) } componentDidMount () { @@ -107,6 +108,7 @@ export default class CodeEditor extends React.Component { this.editor.on('blur', this.blurHandler) this.editor.on('change', this.changeHandler) this.editor.on('paste', this.pasteHandler) + this.editor.on('scroll', this.scrollHandler) const editorTheme = document.getElementById('editorTheme') editorTheme.addEventListener('load', this.loadStyleHandler) @@ -126,6 +128,7 @@ export default class CodeEditor extends React.Component { this.editor.off('blur', this.blurHandler) this.editor.off('change', this.changeHandler) this.editor.off('paste', this.pasteHandler) + this.editor.off('scroll', this.scrollHandler) const editorTheme = document.getElementById('editorTheme') editorTheme.removeEventListener('load', this.loadStyleHandler) } @@ -262,6 +265,12 @@ export default class CodeEditor extends React.Component { } } + handleScroll (e) { + if (this.props.onScroll) { + this.props.onScroll(e) + } + } + handlePasteUrl (e, editor, pastedTxt) { e.preventDefault() const taggedUrl = `<${pastedTxt}>` diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 1d0f75133..c5b0355de 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -121,6 +121,7 @@ export default class MarkdownPreview extends React.Component { this.mouseDownHandler = (e) => this.handleMouseDown(e) this.mouseUpHandler = (e) => this.handleMouseUp(e) this.DoubleClickHandler = (e) => this.handleDoubleClick(e) + this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true}) this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e) this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) this.saveAsTextHandler = () => this.handleSaveAsText() @@ -151,6 +152,12 @@ export default class MarkdownPreview extends React.Component { this.props.onCheckboxClick(e) } + handleScroll (e) { + if (this.props.onScroll) { + this.props.onScroll(e) + } + } + handleContextMenu (e) { this.props.onContextMenu(e) } @@ -279,6 +286,7 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler) this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler) + this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler) eventEmitter.on('export:save-text', this.saveAsTextHandler) eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('export:save-html', this.saveAsHtmlHandler) @@ -292,6 +300,7 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler) this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler) + this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler) eventEmitter.off('export:save-text', this.saveAsTextHandler) eventEmitter.off('export:save-md', this.saveAsMdHandler) eventEmitter.off('export:save-html', this.saveAsHtmlHandler) diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 38ce903c7..505fbaf42 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -2,6 +2,7 @@ import React from 'react' import CodeEditor from 'browser/components/CodeEditor' import MarkdownPreview from 'browser/components/MarkdownPreview' import { findStorage } from 'browser/lib/findStorage' +import _ from 'lodash' import styles from './MarkdownSplitEditor.styl' import CSSModules from 'browser/lib/CSSModules' @@ -12,6 +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 } handleOnChange () { @@ -19,6 +21,49 @@ class MarkdownSplitEditor extends React.Component { this.props.onChange() } + handleScroll (e) { + const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document') + const codeDoc = _.get(this, 'refs.code.editor.doc') + let srcTop, srcHeight, targetTop, targetHeight + + 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') + } else { + srcTop = _.get(previewDoc, 'body.scrollTop') + srcHeight = _.get(previewDoc, 'body.scrollHeight') + targetTop = _.get(codeDoc, 'scrollTop') + targetHeight = _.get(codeDoc, 'height') + } + + 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) + } + frame++ + }, framerate) + } + } + handleCheckboxClick (e) { e.preventDefault() e.stopPropagation() @@ -69,6 +114,7 @@ class MarkdownSplitEditor extends React.Component { fetchUrlTitle={config.editor.fetchUrlTitle} storageKey={storageKey} onChange={this.handleOnChange.bind(this)} + onScroll={this.handleScroll.bind(this)} /> this.handleCheckboxClick(e)} + onScroll={this.handleScroll.bind(this)} showCopyNotification={config.ui.showCopyNotification} storagePath={storage.path} />