/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ import './editor.less'; import React from 'react'; import createReactClass from 'create-react-class'; import _ from 'lodash'; import dedent from 'dedent'; import CodeEditor from '../../components/codeEditor/codeEditor.jsx'; import SnippetBar from './snippetbar/snippetbar.jsx'; import MetadataEditor from './metadataEditor/metadataEditor.jsx'; const EDITOR_THEME_KEY = 'HB_editor_theme'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; //const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; const DEFAULT_STYLE_TEXT = dedent` /*=======--- Example CSS styling ---=======*/ /* Any CSS here will apply to your document! */ .myExampleClass { color: black; }`; const DEFAULT_SNIPPET_TEXT = dedent` \snippet example snippet The text between \`\snippet title\` lines will become a snippet of name \`title\` as this example provides. This snippet is accessible in the brew tab, and will be inherited if the brew is used as a theme. `; let isJumping = false; const Editor = createReactClass({ displayName : 'Editor', getDefaultProps : function() { return { brew : { text : '', style : '' }, onBrewChange : ()=>{}, reportError : ()=>{}, onCursorPageChange : ()=>{}, onViewPageChange : ()=>{}, editorTheme : 'default', renderer : 'legacy', currentEditorCursorPageNum : 1, currentEditorViewPageNum : 1, currentBrewRendererPageNum : 1, }; }, getInitialState : function() { return { editorTheme : this.props.editorTheme, view : 'text', //'text', 'style', 'meta', 'snippet' snippetBarHeight : 26, }; }, editor : React.createRef(null), codeEditor : React.createRef(null), isText : function() {return this.state.view == 'text';}, isStyle : function() {return this.state.view == 'style';}, isMeta : function() {return this.state.view == 'meta';}, isSnip : function() {return this.state.view == 'snippet';}, componentDidMount : function() { document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys); const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); if(editorTheme) { this.setState({ editorTheme : editorTheme }); } const snippetBar = document.querySelector('.editor > .snippetBar'); if(!snippetBar) return; this.resizeObserver = new ResizeObserver((entries)=>{ const height = document.querySelector('.editor > .snippetBar').offsetHeight; this.setState({ snippetBarHeight: height }); }); this.resizeObserver.observe(snippetBar); }, componentDidUpdate : function(prevProps, prevState, snapshot) { if(prevProps.moveBrew !== this.props.moveBrew) this.brewJump(); 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); } } }, componentWillUnmount() { if(this.resizeObserver) this.resizeObserver.disconnect(); }, handleControlKeys : function(e){ if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return; const LEFTARROW_KEY = 37; const RIGHTARROW_KEY = 39; 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(); } }, updateCurrentCursorPage : function(lineNumber) { const lines = this.props.brew.text.split('\n').slice(0, lineNumber); const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/; const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1); this.props.onCursorPageChange(currentPage); }, updateCurrentViewPage : function(topLine) { const lines = this.props.brew.text.split('\n').slice(0, topLine); const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\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); }, handleViewChange : function(newView){ this.props.setMoveArrows(newView === 'text'); this.setState({ view : newView }, ()=>{ this.codeEditor.current?.codeMirror?.focus(); }); }, brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){ if(!window || !this.isText() || 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; let scrollingTimeout; const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times 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 }; 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(targetPage=this.props.currentBrewRendererPageNum, smooth=true){ if(!this.isText() || isJumping) return; const textSplit = this.props.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\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; const editor = this.codeEditor.current; let currentY = editor.getScrollTop(); const targetY = editor.getLineTop(targetLine); let scrollingTimeout; const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs scrollingTimeout = setTimeout(()=>{ isJumping = false; }, 150); // If 150 ms pass without a scroll event, assume scrolling is done }; isJumping = true; checkIfScrollComplete(); if(smooth) { //Scroll 1/10 of the way every 10ms until 1px off. const incrementalScroll = setInterval(()=>{ currentY += (targetY - currentY) / 10; editor.scrollToY(currentY); if(Math.abs(targetY - currentY) < 1) { editor.scrollToY(targetY); editor.setCursorToLine(targetLine); clearInterval(incrementalScroll); } }, 10); } else { editor.scrollToY(targetY); editor.setCursorToLine(targetLine); } }, //Called when there are changes to the editor's dimensions update : function(){}, updateEditorTheme : function(newTheme){ window.localStorage.setItem(EDITOR_THEME_KEY, newTheme); this.setState({ editorTheme : newTheme }); }, //Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory rerenderParent : function (){ this.forceUpdate(); }, renderEditor : function(){ if(this.isText()){ return <> this.updateCurrentCursorPage(line)} onViewChange={(line)=>this.updateCurrentViewPage(line)} editorTheme={this.state.editorTheme} renderer={this.props.brew.renderer} rerenderParent={this.rerenderParent} style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} /> ; } if(this.isStyle()){ return <> ; } if(this.isMeta()){ return <> ; } if(this.isSnip()){ if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; } return <> ; } }, redo : function(){ return this.codeEditor.current?.redo(); }, historySize : function(){ return this.codeEditor.current?.historySize(); }, undo : function(){ return this.codeEditor.current?.undo(); }, foldCode : function(){ return this.codeEditor.current?.foldAllCode(); }, unfoldCode : function(){ return this.codeEditor.current?.unfoldAllCode(); }, render : function(){ return (
{this.renderEditor()}
); } }); export default Editor;