diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 23622223c..7268e4b34 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -92,7 +92,7 @@ const BrewRenderer = (props)=>{ const updateCurrentPage = useCallback(_.throttle((e)=>{ const { scrollTop, clientHeight, scrollHeight } = e.target; const totalScrollableHeight = scrollHeight - clientHeight; - const currentPageNumber = Math.ceil(((scrollTop + 1) / totalScrollableHeight) * rawPages.length); + const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1); props.onPageChange(currentPageNumber); }, 200), []); diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index f8287dea1..0ecb1787f 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -3,7 +3,6 @@ require('./editor.less'); const React = require('react'); const createClass = require('create-react-class'); const _ = require('lodash'); -const cx = require('classnames'); const dedent = require('dedent-tabs').default; const Markdown = require('../../../shared/naturalcrit/markdown.js'); @@ -22,6 +21,7 @@ const DEFAULT_STYLE_TEXT = dedent` color: black; }`; +let isJumping = false; const Editor = createClass({ displayName : 'Editor', @@ -43,9 +43,9 @@ const Editor = createClass({ editorTheme : 'default', renderer : 'legacy', - currentEditorCursorPageNum : 0, - currentEditorViewPageNum : 0, - currentBrewRendererPageNum : 0, + currentEditorCursorPageNum : 1, + currentEditorViewPageNum : 1, + currentBrewRendererPageNum : 1, }; }, getInitialState : function() { @@ -63,6 +63,7 @@ const Editor = createClass({ isMeta : function() {return this.state.view == 'meta';}, componentDidMount : function() { + this.updateEditorSize(); this.highlightCustomMarkdown(); window.addEventListener('resize', this.updateEditorSize); @@ -85,22 +86,32 @@ const Editor = createClass({ }, componentDidUpdate : function(prevProps, prevState, snapshot) { + this.highlightCustomMarkdown(); - if(prevProps.moveBrew !== this.props.moveBrew) { + if(prevProps.moveBrew !== this.props.moveBrew) this.brewJump(); - }; - if(prevProps.moveSource !== this.props.moveSource) { + + if(prevProps.moveSource !== this.props.moveSource) this.sourceJump(); - }; + + if(this.props.liveScroll) { + if(prevProps.currentBrewRendererPageNum !== this.props.currentBrewRendererPageNum) { + this.sourceJump(this.props.currentBrewRendererPageNum, false); + } else if(prevProps.currentEditorViewPageNum !== this.props.currentEditorViewPageNum) { + this.brewJump(this.props.currentEditorViewPageNum, false); + } else if(prevProps.currentEditorCursorPageNum !== this.props.currentEditorCursorPageNum) { + this.brewJump(this.props.currentEditorCursorPageNum, false); + } + } }, handleControlKeys : function(e){ - if(!(e.ctrlKey && e.metaKey)) return; + if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return; const LEFTARROW_KEY = 37; const RIGHTARROW_KEY = 39; - if (e.shiftKey && (e.keyCode == RIGHTARROW_KEY)) this.brewJump(); - if (e.shiftKey && (e.keyCode == LEFTARROW_KEY)) this.sourceJump(); - if ((e.keyCode == LEFTARROW_KEY) || (e.keyCode == RIGHTARROW_KEY)) { + if (e.keyCode == RIGHTARROW_KEY) this.brewJump(); + if (e.keyCode == LEFTARROW_KEY) this.sourceJump(); + if (e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) { e.stopPropagation(); e.preventDefault(); } @@ -302,64 +313,93 @@ const Editor = createClass({ } }, - brewJump : function(targetPage=this.props.currentEditorCursorPageNum){ - if(!window) return; + brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){ + if(!window || isJumping) + return; + // Get current brewRenderer scroll position and calculate target position const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0]; const currentPos = brewRenderer.scrollTop; const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top; - const interimPos = targetPos >= 0 ? -30 : 30; - - const bounceDelay = 100; - const scrollDelay = 500; - - if(!this.throttleBrewMove) { - this.throttleBrewMove = _.throttle((currentPos, interimPos, targetPos)=>{ - brewRenderer.scrollTo({ top: currentPos + interimPos, behavior: 'smooth' }); - setTimeout(()=>{ - brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' }); - }, bounceDelay); - }, scrollDelay, { leading: true, trailing: false }); + + const checkIfScrollComplete = () => { + let scrollingTimeout; + clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs + scrollingTimeout = setTimeout(() => { + isJumping = false; + brewRenderer.removeEventListener('scroll', checkIfScrollComplete); + }, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done }; - this.throttleBrewMove(currentPos, interimPos, targetPos); - // const hashPage = (page != 1) ? `p${page}` : ''; - // window.location.hash = hashPage; + isJumping = true; + checkIfScrollComplete(); + brewRenderer.addEventListener('scroll', checkIfScrollComplete); + + if(smooth) { + const bouncePos = targetPos >= 0 ? -30 : 30; //Do a little bounce before scrolling + const bounceDelay = 100; + const scrollDelay = 500; + + if(!this.throttleBrewMove) { + this.throttleBrewMove = _.throttle((currentPos, bouncePos, targetPos)=>{ + brewRenderer.scrollTo({ top: currentPos + bouncePos, behavior: 'smooth' }); + setTimeout(()=>{ + brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' }); + }, bounceDelay); + }, scrollDelay, { leading: true, trailing: false }); + }; + this.throttleBrewMove(currentPos, bouncePos, targetPos); + } else { + brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'instant', block: 'start' }); + } }, - sourceJump : function(targetLine=null){ - if(this.isText()) { - if(targetLine == null) { - targetLine = 0; - - const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/; - const textString = this.props.brew.text.split(textSplit).slice(0, this.props.currentBrewRendererPageNum-1).join(textSplit); - const textPosition = textString.length; - const lineCount = textString.match('\n') ? textString.slice(0, textPosition).split('\n').length : 0; - - targetLine = lineCount - 1; //Scroll to `\page`, which is one line back. - - let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top; - let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); - - //Scroll 1/10 of the way every 10ms until 1px off. - const incrementalScroll = setInterval(()=>{ - currentY += (targetY - currentY) / 10; - this.codeEditor.current.codeMirror.scrollTo(null, currentY); - - // Update target: target height is not accurate until within +-10 lines of the visible window - if(Math.abs(targetY - currentY > 100)) - targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); - - // End when close enough - if(Math.abs(targetY - currentY) < 1) { - this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference - this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 }); - this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); - clearInterval(incrementalScroll); - } - }, 10); - } + sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){ + if(!this.isText || isJumping) + return; + + const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/; + const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit); + const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1; + + let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top; + let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); + + const checkIfScrollComplete = () => { + let scrollingTimeout; + clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs + scrollingTimeout = setTimeout(() => { + isJumping = false; + this.codeEditor.current.codeMirror.off('scroll', checkIfScrollComplete); + }, 150); // If 150 ms pass without a scroll event, assume scrolling is done + }; + + isJumping = true; + checkIfScrollComplete(); + this.codeEditor.current.codeMirror.on('scroll', checkIfScrollComplete); + + if(smooth) { + //Scroll 1/10 of the way every 10ms until 1px off. + const incrementalScroll = setInterval(()=>{ + currentY += (targetY - currentY) / 10; + this.codeEditor.current.codeMirror.scrollTo(null, currentY); + + // Update target: target height is not accurate until within +-10 lines of the visible window + if(Math.abs(targetY - currentY > 100)) + targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); + + // End when close enough + if(Math.abs(targetY - currentY) < 1) { + this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference + this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 }); + this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); + clearInterval(incrementalScroll); + } + }, 10); + } else { + this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference + this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 }); + this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); } }, @@ -389,8 +429,6 @@ const Editor = createClass({ view={this.state.view} value={this.props.brew.text} onChange={this.props.onTextChange} - onCursorActivity={this.props.onCursorActivity} - onScroll={this.props.onPageChange} editorTheme={this.state.editorTheme} rerenderParent={this.rerenderParent} /> ; diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index e6a67e414..1ff841024 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -55,9 +55,9 @@ const EditPage = createClass({ autoSave : true, autoSaveWarning : false, unsavedTime : new Date(), - currentEditorViewPageNum : 0, - currentEditorCursorPageNum : 0, - currentBrewRendererPageNum : 0, + currentEditorViewPageNum : 1, + currentEditorCursorPageNum : 1, + currentBrewRendererPageNum : 1, displayLockMessage : this.props.brew.lock || false, themeBundle : {} }; @@ -117,23 +117,19 @@ const EditPage = createClass({ }, handleEditorViewPageChange : function(pageNumber){ - console.log(`editor view : ${pageNumber}`); this.setState({ currentEditorViewPageNum: pageNumber }); }, handleEditorCursorPageChange : function(pageNumber){ - console.log(`editor cursor : ${pageNumber}`); this.setState({ currentEditorCursorPageNum: pageNumber }); }, handleBrewRendererPageChange : function(pageNumber){ - console.log(`brewRenderer view : ${pageNumber}`); this.setState({ currentBrewRendererPageNum: pageNumber }); }, handleTextChange : function(text){ //If there are errors, run the validator on every change to give quick feedback - console.log('text change'); let htmlErrors = this.state.htmlErrors; if(htmlErrors.length) htmlErrors = Markdown.validate(text); diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 6e11806bd..ac3be81df 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -34,9 +34,9 @@ const HomePage = createClass({ brew : this.props.brew, welcomeText : this.props.brew.text, error : undefined, - currentEditorViewPageNum : 0, - currentEditorCursorPageNum : 0, - currentBrewRendererPageNum : 0, + currentEditorViewPageNum : 1, + currentEditorCursorPageNum : 1, + currentBrewRendererPageNum : 1, themeBundle : {} }; }, @@ -64,17 +64,14 @@ const HomePage = createClass({ }, handleEditorViewPageChange : function(pageNumber){ - console.log(`editor view : ${pageNumber}`); this.setState({ currentEditorViewPageNum: pageNumber }); }, handleEditorCursorPageChange : function(pageNumber){ - console.log(`editor cursor : ${pageNumber}`); this.setState({ currentEditorCursorPageNum: pageNumber }); }, handleBrewRendererPageChange : function(pageNumber){ - console.log(`brewRenderer view : ${pageNumber}`); this.setState({ currentBrewRendererPageNum: pageNumber }); }, diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 115f4ea88..c147cd474 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -44,9 +44,9 @@ const NewPage = createClass({ saveGoogle : (global.account && global.account.googleId ? true : false), error : null, htmlErrors : Markdown.validate(brew.text), - currentEditorViewPageNum : 0, - currentEditorCursorPageNum : 0, - currentBrewRendererPageNum : 0, + currentEditorViewPageNum : 1, + currentEditorCursorPageNum : 1, + currentBrewRendererPageNum : 1, themeBundle : {} }; }, @@ -111,17 +111,14 @@ const NewPage = createClass({ }, handleEditorViewPageChange : function(pageNumber){ - console.log(`editor view : ${pageNumber}`); this.setState({ currentEditorViewPageNum: pageNumber }); }, handleEditorCursorPageChange : function(pageNumber){ - console.log(`editor cursor : ${pageNumber}`); this.setState({ currentEditorCursorPageNum: pageNumber }); }, handleBrewRendererPageChange : function(pageNumber){ - console.log(`brewRenderer view : ${pageNumber}`); this.setState({ currentBrewRendererPageNum: pageNumber }); }, diff --git a/shared/naturalcrit/splitPane/splitPane.jsx b/shared/naturalcrit/splitPane/splitPane.jsx index 606b22d40..01807db70 100644 --- a/shared/naturalcrit/splitPane/splitPane.jsx +++ b/shared/naturalcrit/splitPane/splitPane.jsx @@ -15,12 +15,12 @@ const SplitPane = createClass({ getInitialState : function() { return { - currentDividerPos : null, - windowWidth : 0, - isDragging : false, - moveSource : false, - moveBrew : false, - showMoveArrows : true + currentDividerPos : null, + windowWidth : 0, + isDragging : false, + moveSource : false, + moveBrew : false, + showMoveArrows : true }; }, @@ -42,6 +42,10 @@ const SplitPane = createClass({ }); } window.addEventListener('resize', this.handleWindowResize); + + // This lives here instead of in the initial render because you cannot touch localStorage until the componant mounts. + const loadLiveScroll = window.localStorage.getItem('liveScroll') === 'true'; + this.setState({ liveScroll : loadLiveScroll }); }, componentWillUnmount : function() { @@ -89,6 +93,11 @@ const SplitPane = createClass({ userSetDividerPos : newSize }); }, + + liveScrollToggle : function() { + window.localStorage.setItem('liveScroll', String(!this.state.liveScroll)); + this.setState({ liveScroll: !this.state.liveScroll }); + }, /* unFocus : function() { if(document.selection){ @@ -120,6 +129,11 @@ const SplitPane = createClass({ onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} > +
+ +
; } }, @@ -144,9 +158,10 @@ const SplitPane = createClass({ > {React.cloneElement(this.props.children[0], { ...(this.props.showDividerButtons && { - moveBrew: this.state.moveBrew, - moveSource: this.state.moveSource, - setMoveArrows: this.setMoveArrows, + moveBrew : this.state.moveBrew, + moveSource : this.state.moveSource, + liveScroll : this.state.liveScroll, + setMoveArrows : this.setMoveArrows, }), })} diff --git a/shared/naturalcrit/splitPane/splitPane.less b/shared/naturalcrit/splitPane/splitPane.less index 831b5ce47..e5b3dd7f8 100644 --- a/shared/naturalcrit/splitPane/splitPane.less +++ b/shared/naturalcrit/splitPane/splitPane.less @@ -53,6 +53,15 @@ .tooltipRight('Jump to location in Preview'); top : 60px; } + &.lock{ + .tooltipRight('De-sync Editor and Preview locations.'); + top : 90px; + background: #666; + } + &.unlock{ + .tooltipRight('Sync Editor and Preview locations'); + top : 90px; + } &:hover{ background-color: #666; }