diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 2da7123cc..787423528 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -188,7 +188,7 @@ const BrewRenderer = createClass({ : null} -
{ - if(line.indexOf('\\page') !== -1) r++; + if( + (this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1) + || + (this.props.renderer == 'V3' && line.match(/^\\page$/)) + ) r++; return r; }, 1); }, @@ -120,6 +131,7 @@ const Editor = createClass({ //reset custom line styles codeMirror.removeLineClass(lineNumber, 'background', 'pageLine'); codeMirror.removeLineClass(lineNumber, 'text'); + codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash'); // Styling for \page breaks if((this.props.renderer == 'legacy' && line.includes('\\page')) || @@ -174,9 +186,76 @@ const Editor = createClass({ } }, - brewJump : function(){ - const currentPage = this.getCurrentPage(); - window.location.hash = `p${currentPage}`; + brewJump : function(targetPage=this.getCurrentPage()){ + if(!window) return; + // console.log(`Scroll to: p${targetPage}`); + 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 }); + }; + this.throttleBrewMove(currentPos, interimPos, targetPos); + + // const hashPage = (page != 1) ? `p${page}` : ''; + // window.location.hash = hashPage; + }, + + sourceJump : function(targetLine=null){ + if(this.isText()) { + if(targetLine == null) { + targetLine = 0; + + const pageCollection = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('page'); + const brewRendererHeight = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0).getBoundingClientRect().height; + + let currentPage = 1; + for (const page of pageCollection) { + if(page.getBoundingClientRect().bottom > (brewRendererHeight / 2)) { + currentPage = parseInt(page.id.slice(1)) || 1; + break; + } + } + + const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/; + const textString = this.props.brew.text.split(textSplit).slice(0, currentPage-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.refs.codeEditor.codeMirror.getScrollInfo().top; + let targetY = this.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true); + + //Scroll 1/10 of the way every 10ms until 1px off. + const incrementalScroll = setInterval(()=>{ + currentY += (targetY - currentY) / 10; + this.refs.codeEditor.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.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true); + + // End when close enough + if(Math.abs(targetY - currentY) < 1) { + this.refs.codeEditor.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference + this.refs.codeEditor.setCursorPosition({ line: targetLine + 1, ch: 0 }); + this.refs.codeEditor.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); + clearInterval(incrementalScroll); + } + }, 10); + } + } }, //Called when there are changes to the editor's dimensions diff --git a/scripts/project.json b/scripts/project.json index 07acdda48..5a0289ad0 100644 --- a/scripts/project.json +++ b/scripts/project.json @@ -15,6 +15,7 @@ "codemirror/addon/fold/foldcode.js", "codemirror/addon/fold/foldgutter.js", "codemirror/addon/fold/xml-fold.js", + "codemirror/addon/scroll/scrollpastend.js", "codemirror/addon/search/search.js", "codemirror/addon/search/searchcursor.js", "codemirror/addon/search/jump-to-line.js", diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx index 42076ed76..6340a58fe 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.jsx +++ b/shared/naturalcrit/codeEditor/codeEditor.jsx @@ -30,6 +30,8 @@ if(typeof navigator !== 'undefined'){ // require('codemirror/addon/edit/trailingspace.js'); //Active line highlighting // require('codemirror/addon/selection/active-line.js'); + //Scroll past last line + require('codemirror/addon/scroll/scrollpastend.js'); //Auto-closing //XML code folding is a requirement of the auto-closing tag feature and is not enabled require('codemirror/addon/fold/xml-fold.js'); @@ -98,6 +100,7 @@ const CodeEditor = createClass({ indentWithTabs : true, tabSize : 2, historyEventDelay : 250, + scrollPastEnd : true, extraKeys : { 'Ctrl-B' : this.makeBold, 'Cmd-B' : this.makeBold, diff --git a/shared/naturalcrit/codeEditor/codeEditor.less b/shared/naturalcrit/codeEditor/codeEditor.less index bf36293ed..c929e0d21 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.less +++ b/shared/naturalcrit/codeEditor/codeEditor.less @@ -3,6 +3,11 @@ @import (less) 'codemirror/addon/search/matchesonscrollbar.css'; @import (less) 'codemirror/addon/dialog/dialog.css'; +@keyframes sourceMoveAnimation { + 50% {background-color: red; color: white;} + 100% {background-color: unset; color: unset;} +} + .codeEditor{ .CodeMirror-foldmarker { font-family: inherit; @@ -10,6 +15,11 @@ font-weight: 600; } + .sourceMoveFlash .CodeMirror-line{ + animation-name: sourceMoveAnimation; + animation-duration: 0.4s; + } + //.cm-tab { // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right; //} @@ -19,4 +29,4 @@ // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right; // } //} -} \ No newline at end of file +} diff --git a/shared/naturalcrit/splitPane/splitPane.jsx b/shared/naturalcrit/splitPane/splitPane.jsx index 4d138e30b..f7ad4bed4 100644 --- a/shared/naturalcrit/splitPane/splitPane.jsx +++ b/shared/naturalcrit/splitPane/splitPane.jsx @@ -17,7 +17,10 @@ const SplitPane = createClass({ return { currentDividerPos : null, windowWidth : 0, - isDragging : false + isDragging : false, + moveSource : false, + moveBrew : false, + showMoveArrows : true }; }, @@ -29,6 +32,11 @@ const SplitPane = createClass({ userSetDividerPos : dividerPos, windowWidth : window.innerWidth }); + } else { + this.setState({ + currentDividerPos : window.innerWidth / 2, + userSetDividerPos : window.innerWidth / 2 + }); } window.addEventListener('resize', this.handleWindowResize); }, @@ -83,20 +91,58 @@ const SplitPane = createClass({ window.getSelection().removeAllRanges(); } }, -*/ + */ + + setMoveArrows : function(newState) { + if(this.state.showMoveArrows != newState){ + this.setState({ + showMoveArrows : newState + }); + } + }, + + renderMoveArrows : function(){ + if(this.state.showMoveArrows) { + return <> +
this.setState({ moveSource: !this.state.moveSource })} > + +
+
this.setState({ moveBrew: !this.state.moveBrew })} > + +
+ ; + } + }, + renderDivider : function(){ - return
-
- - - + return <> + {this.renderMoveArrows()} +
+
+ + + +
-
; + ; }, render : function(){ return
- {this.props.children[0]} + + {React.cloneElement(this.props.children[0], { + moveBrew : this.state.moveBrew, + moveSource : this.state.moveSource, + setMoveArrows : this.setMoveArrows + })} + {this.renderDivider()} {this.props.children[1]}
; diff --git a/shared/naturalcrit/splitPane/splitPane.less b/shared/naturalcrit/splitPane/splitPane.less index 0e9095193..700d383d5 100644 --- a/shared/naturalcrit/splitPane/splitPane.less +++ b/shared/naturalcrit/splitPane/splitPane.less @@ -13,7 +13,7 @@ .divider{ display : table; height : 100%; - width : 12px; + width : 15px; cursor : ew-resize; background-color : #bbb; text-align : center; @@ -32,4 +32,28 @@ background-color: #999; } } + .arrow{ + position : absolute; + width : 25px; + height : 25px; + border : 2px solid #bbb; + border-radius : 15px; + text-align : center; + font-size : 1.2em; + cursor : pointer; + background-color : #ddd; + z-index : 999; + box-shadow : 0 4px 5px #0000007f; + &.left{ + .tooltipLeft('Jump to location in Editor'); + top : 30px; + } + &.right{ + .tooltipRight('Jump to location in Preview'); + top : 60px; + } + &:hover{ + background-color: #666; + } + } }