diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index f3b284a93..23622223c 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -1,7 +1,7 @@ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ require('./brewRenderer.less'); const React = require('react'); -const { useState, useRef, useEffect } = React; +const { useState, useRef, useEffect, useCallback } = React; const _ = require('lodash'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); @@ -49,23 +49,25 @@ let rawPages = []; const BrewRenderer = (props)=>{ props = { - text : '', - style : '', - renderer : 'legacy', - theme : '5ePHB', - lang : '', - errors : [], - currentEditorPage : 0, - themeBundle : {}, + text : '', + style : '', + renderer : 'legacy', + theme : '5ePHB', + lang : '', + errors : [], + currentEditorCursorPageNum : 0, + currentEditorViewPageNum : 0, + currentBrewRendererPageNum : 0, + themeBundle : {}, + onPageChange : ()=>{}, ...props }; const [state, setState] = useState({ - height : PAGE_HEIGHT, - isMounted : false, - visibility : 'hidden', - zoom : 100, - currentPageNumber : 1, + height : PAGE_HEIGHT, + isMounted : false, + visibility : 'hidden', + zoom : 100 }); const mainRef = useRef(null); @@ -87,25 +89,22 @@ const BrewRenderer = (props)=>{ })); }; - const getCurrentPage = (e)=>{ + const updateCurrentPage = useCallback(_.throttle((e)=>{ const { scrollTop, clientHeight, scrollHeight } = e.target; const totalScrollableHeight = scrollHeight - clientHeight; - const currentPageNumber = Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length); + const currentPageNumber = Math.ceil(((scrollTop + 1) / totalScrollableHeight) * rawPages.length); - setState((prevState)=>({ - ...prevState, - currentPageNumber : currentPageNumber || 1 - })); - }; + props.onPageChange(currentPageNumber); + }, 200), []); const isInView = (index)=>{ if(!state.isMounted) return false; - if(index == props.currentEditorPage) //Already rendered before this step + if(index == props.currentEditorCursorPageNum - 1) //Already rendered before this step return false; - if(Math.abs(index - state.currentPageNumber) <= 3) + if(Math.abs(index - props.currentBrewRendererPageNum - 1) <= 3) return true; return false; @@ -142,7 +141,7 @@ const BrewRenderer = (props)=>{ renderedPages.length = 0; // Render currently-edited page first so cross-page effects (variables, links) can propagate out first - renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage); + renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1); _.forEach(rawPages, (page, index)=>{ if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){ @@ -192,7 +191,7 @@ const BrewRenderer = (props)=>{ <> {/*render dummy page while iFrame is mounting.*/} {!state.isMounted - ?
+ ?
{renderDummyPage(1)}
@@ -205,7 +204,7 @@ const BrewRenderer = (props)=>{
- + {/*render in iFrame so broken code doesn't crash the site.*/} { onClick={()=>{emitClick();}} >
diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index d320f1eee..17ac796d2 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -36,9 +36,16 @@ const Editor = createClass({ onStyleChange : ()=>{}, onMetaChange : ()=>{}, reportError : ()=>{}, + + onCursorPageChange : ()=>{}, + onViewPageChange : ()=>{}, editorTheme : 'default', - renderer : 'legacy' + renderer : 'legacy', + + currentEditorCursorPageNum : 0, + currentEditorViewPageNum : 0, + currentBrewRendererPageNum : 0, }; }, getInitialState : function() { @@ -62,6 +69,9 @@ const Editor = createClass({ document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys); + this.codeEditor.current.codeMirror.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor())}); + this.codeEditor.current.codeMirror.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine())}, 200)); + const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); if(editorTheme) { this.setState({ @@ -96,7 +106,6 @@ const Editor = createClass({ } }, - updateEditorSize : function() { if(this.codeEditor.current) { let paneHeight = this.editor.current.parentNode.clientHeight; @@ -105,6 +114,20 @@ const Editor = createClass({ } }, + updateCurrentCursorPage : function(cursor) { + const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1); + const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/; + const currentPage = lines.reduce((count, line) => count + (pageRegex.test(line) ? 1 : 0), 1); + this.props.onCursorPageChange(currentPage); + }, + + updateCurrentViewPage : function(topScrollLine) { + const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1); + const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/; + const currentPage = lines.reduce((count, line) => count + (pageRegex.test(line) ? 1 : 0), 1); + this.props.onViewPageChange(currentPage); + }, + handleInject : function(injectText){ this.codeEditor.current?.injectText(injectText, false); }, @@ -119,18 +142,6 @@ const Editor = createClass({ }); //TODO: not sure if updateeditorsize needed }, - getCurrentPage : function(){ - const lines = this.props.brew.text.split('\n').slice(0, this.codeEditor.current.getCursorPosition().line + 1); - return _.reduce(lines, (r, line)=>{ - if( - (this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1) - || - (this.props.renderer == 'V3' && line.match(/^\\page$/)) - ) r++; - return r; - }, 1); - }, - highlightCustomMarkdown : function(){ if(!this.codeEditor.current) return; if(this.state.view === 'text') { @@ -291,9 +302,9 @@ const Editor = createClass({ } }, - brewJump : function(targetPage=this.getCurrentPage()){ + brewJump : function(targetPage=this.props.currentEditorCursorPageNum){ if(!window) return; - // console.log(`Scroll to: p${targetPage}`); + // 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; @@ -321,19 +332,8 @@ const Editor = createClass({ 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 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; @@ -389,6 +389,8 @@ 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 ffd7dfe33..1ab90f807 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -1,8 +1,9 @@ /* eslint-disable max-lines */ require('./editPage.less'); const React = require('react'); -const createClass = require('create-react-class'); const _ = require('lodash'); +const createClass = require('create-react-class'); + const request = require('../../utils/request-middleware.js'); const { Meta } = require('vitreum/headtags'); @@ -43,22 +44,24 @@ const EditPage = createClass({ getInitialState : function() { return { - brew : this.props.brew, - isSaving : false, - isPending : false, - alertTrashedGoogleBrew : this.props.brew.trashed, - alertLoginToTransfer : false, - saveGoogle : this.props.brew.googleId ? true : false, - confirmGoogleTransfer : false, - error : null, - htmlErrors : Markdown.validate(this.props.brew.text), - url : '', - autoSave : true, - autoSaveWarning : false, - unsavedTime : new Date(), - currentEditorPage : 0, - displayLockMessage : this.props.brew.lock || false, - themeBundle : {} + brew : this.props.brew, + isSaving : false, + isPending : false, + alertTrashedGoogleBrew : this.props.brew.trashed, + alertLoginToTransfer : false, + saveGoogle : this.props.brew.googleId ? true : false, + confirmGoogleTransfer : false, + error : null, + htmlErrors : Markdown.validate(this.props.brew.text), + url : '', + autoSave : true, + autoSaveWarning : false, + unsavedTime : new Date(), + currentEditorViewPageNum : 0, + currentEditorCursorPageNum : 0, + currentBrewRendererPageNum : 0, + displayLockMessage : this.props.brew.lock || false, + themeBundle : {} }; }, @@ -115,16 +118,31 @@ const EditPage = createClass({ this.editor.current.update(); }, + 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); this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - isPending : true, - htmlErrors : htmlErrors, - currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0 + brew : { ...prevState.brew, text: text }, + isPending : true, + htmlErrors : htmlErrors, }), ()=>{if(this.state.autoSave) this.trySave();}); }, @@ -429,6 +447,11 @@ const EditPage = createClass({ userThemes={this.props.userThemes} snippetBundle={this.state.themeBundle.snippets} updateBrew={this.updateBrew} + onCursorPageChange={this.handleEditorCursorPageChange} + onViewPageChange={this.handleEditorViewPageChange} + currentEditorViewPageNum={this.state.currentEditorViewPageNum} + currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} + currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} /> diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index d7efcaf14..6e11806bd 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -1,7 +1,6 @@ require('./homePage.less'); const React = require('react'); const createClass = require('create-react-class'); -const _ = require('lodash'); const cx = require('classnames'); const request = require('../../utils/request-middleware.js'); const { Meta } = require('vitreum/headtags'); @@ -32,11 +31,13 @@ const HomePage = createClass({ }, getInitialState : function() { return { - brew : this.props.brew, - welcomeText : this.props.brew.text, - error : undefined, - currentEditorPage : 0, - themeBundle : {} + brew : this.props.brew, + welcomeText : this.props.brew.text, + error : undefined, + currentEditorViewPageNum : 0, + currentEditorCursorPageNum : 0, + currentBrewRendererPageNum : 0, + themeBundle : {} }; }, @@ -61,10 +62,25 @@ const HomePage = createClass({ handleSplitMove : function(){ this.editor.current.update(); }, + + 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){ this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0 + brew : { ...prevState.brew, text: text }, })); }, renderNavbar : function(){ @@ -97,12 +113,20 @@ const HomePage = createClass({ renderer={this.state.brew.renderer} showEditButtons={false} snippetBundle={this.state.themeBundle.snippets} + onCursorPageChange={this.handleEditorCursorPageChange} + onViewPageChange={this.handleEditorViewPageChange} + currentEditorViewPageNum={this.state.currentEditorViewPageNum} + currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} + currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} /> diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 5b0f59c00..115f4ea88 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -39,13 +39,15 @@ const NewPage = createClass({ const brew = this.props.brew; return { - brew : brew, - isSaving : false, - saveGoogle : (global.account && global.account.googleId ? true : false), - error : null, - htmlErrors : Markdown.validate(brew.text), - currentEditorPage : 0, - themeBundle : {} + brew : brew, + isSaving : false, + saveGoogle : (global.account && global.account.googleId ? true : false), + error : null, + htmlErrors : Markdown.validate(brew.text), + currentEditorViewPageNum : 0, + currentEditorCursorPageNum : 0, + currentBrewRendererPageNum : 0, + themeBundle : {} }; }, @@ -108,15 +110,29 @@ const NewPage = createClass({ this.editor.current.update(); }, + 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 let htmlErrors = this.state.htmlErrors; if(htmlErrors.length) htmlErrors = Markdown.validate(text); this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - htmlErrors : htmlErrors, - currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0 + brew : { ...prevState.brew, text: text }, + htmlErrors : htmlErrors, })); localStorage.setItem(BREWKEY, text); }, @@ -221,6 +237,11 @@ const NewPage = createClass({ renderer={this.state.brew.renderer} userThemes={this.props.userThemes} snippetBundle={this.state.themeBundle.snippets} + onCursorPageChange={this.handleEditorCursorPageChange} + onViewPageChange={this.handleEditorViewPageChange} + currentEditorViewPageNum={this.state.currentEditorViewPageNum} + currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} + currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} /> diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx index 3186e39f1..06f6ba0c2 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.jsx +++ b/shared/naturalcrit/codeEditor/codeEditor.jsx @@ -49,12 +49,12 @@ const CodeEditor = createClass({ displayName : 'CodeEditor', getDefaultProps : function() { return { - language : '', - value : '', - wrap : true, - onChange : ()=>{}, - enableFolding : true, - editorTheme : 'default' + language : '', + value : '', + wrap : true, + onChange : ()=>{}, + enableFolding : true, + editorTheme : 'default' }; }, @@ -189,7 +189,7 @@ const CodeEditor = createClass({ autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror); // Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works. - this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());}); + this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue())}); this.updateSize(); }, @@ -397,6 +397,11 @@ const CodeEditor = createClass({ getCursorPosition : function(){ return this.codeMirror.getCursor(); }, + getTopVisibleLine : function(){ + const rect = this.codeMirror.getWrapperElement().getBoundingClientRect(); + const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, "window"); + return topVisibleLine; + }, updateSize : function(){ this.codeMirror.refresh(); },