0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-03-27 14:38:11 +00:00

fix back editor.jsx

This commit is contained in:
Víctor Losada Hernández
2026-03-24 23:41:17 +01:00
parent 953ef8c534
commit bba3199dba

View File

@@ -1,16 +1,16 @@
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
import "./editor.less"; import './editor.less';
import React from "react"; import React from 'react';
import createReactClass from "create-react-class"; import createReactClass from 'create-react-class';
import _ from "lodash"; import _ from 'lodash';
import dedent from "dedent"; import dedent from 'dedent';
import Markdown from "@shared/markdown.js"; import Markdown from '@shared/markdown.js';
import CodeEditor from "../../components/codeEditor/codeEditor.jsx"; import CodeEditor from '../../components/codeEditor/codeEditor.jsx';
import SnippetBar from "./snippetbar/snippetbar.jsx"; import SnippetBar from './snippetbar/snippetbar.jsx';
import MetadataEditor from "./metadataEditor/metadataEditor.jsx"; import MetadataEditor from './metadataEditor/metadataEditor.jsx';
const EDITOR_THEME_KEY = "HB_editor_theme"; const EDITOR_THEME_KEY = 'HB_editor_theme';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
@@ -32,12 +32,12 @@ const DEFAULT_SNIPPET_TEXT = dedent`
let isJumping = false; let isJumping = false;
const Editor = createReactClass({ const Editor = createReactClass({
displayName: "Editor", displayName : 'Editor',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
brew : { brew : {
text: "", text : '',
style: "", style : ''
}, },
onBrewChange : ()=>{}, onBrewChange : ()=>{},
@@ -46,8 +46,8 @@ const Editor = createReactClass({
onCursorPageChange : ()=>{}, onCursorPageChange : ()=>{},
onViewPageChange : ()=>{}, onViewPageChange : ()=>{},
editorTheme: "default", editorTheme : 'default',
renderer: "legacy", renderer : 'legacy',
currentEditorCursorPageNum : 1, currentEditorCursorPageNum : 1,
currentEditorViewPageNum : 1, currentEditorViewPageNum : 1,
@@ -57,7 +57,7 @@ const Editor = createReactClass({
getInitialState : function() { getInitialState : function() {
return { return {
editorTheme : this.props.editorTheme, editorTheme : this.props.editorTheme,
view: "text", //'text', 'style', 'meta', 'snippet' view : 'text', //'text', 'style', 'meta', 'snippet'
snippetBarHeight : 26, snippetBarHeight : 26,
}; };
}, },
@@ -65,45 +65,31 @@ const Editor = createReactClass({
editor : React.createRef(null), editor : React.createRef(null),
codeEditor : React.createRef(null), codeEditor : React.createRef(null),
isText: function () { isText : function() {return this.state.view == 'text';},
return this.state.view == "text"; isStyle : function() {return this.state.view == 'style';},
}, isMeta : function() {return this.state.view == 'meta';},
isStyle: function () { isSnip : function() {return this.state.view == 'snippet';},
return this.state.view == "style";
},
isMeta: function () {
return this.state.view == "meta";
},
isSnip: function () {
return this.state.view == "snippet";
},
componentDidMount : function() { componentDidMount : function() {
this.highlightCustomMarkdown();
document.getElementById("BrewRenderer").addEventListener("keydown", this.handleControlKeys);
document.addEventListener("keydown", this.handleControlKeys);
this.codeEditor.current.codeMirror?.on("cursorActivity", (cm) => { this.highlightCustomMarkdown();
this.updateCurrentCursorPage(cm.getCursor()); document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
}); document.addEventListener('keydown', this.handleControlKeys);
this.codeEditor.current.codeMirror?.on(
"scroll", this.codeEditor.current.codeMirror?.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());});
_.throttle(() => { this.codeEditor.current.codeMirror?.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200));
this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());
}, 200),
);
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if(editorTheme) { if(editorTheme) {
this.setState({ this.setState({
editorTheme: editorTheme, editorTheme : editorTheme
}); });
} }
const snippetBar = document.querySelector(".editor > .snippetBar"); const snippetBar = document.querySelector('.editor > .snippetBar');
if(!snippetBar) return; if(!snippetBar) return;
this.resizeObserver = new ResizeObserver((entries)=>{ this.resizeObserver = new ResizeObserver((entries)=>{
const height = document.querySelector(".editor > .snippetBar").offsetHeight; const height = document.querySelector('.editor > .snippetBar').offsetHeight;
this.setState({ snippetBarHeight: height }); this.setState({ snippetBarHeight: height });
}); });
@@ -111,10 +97,13 @@ const Editor = createReactClass({
}, },
componentDidUpdate : function(prevProps, prevState, snapshot) { componentDidUpdate : function(prevProps, prevState, snapshot) {
this.highlightCustomMarkdown();
if (prevProps.moveBrew !== this.props.moveBrew) this.brewJump();
if (prevProps.moveSource !== this.props.moveSource) this.sourceJump(); this.highlightCustomMarkdown();
if(prevProps.moveBrew !== this.props.moveBrew)
this.brewJump();
if(prevProps.moveSource !== this.props.moveSource)
this.sourceJump();
if(this.props.liveScroll) { if(this.props.liveScroll) {
if(prevProps.currentBrewRendererPageNum !== this.props.currentBrewRendererPageNum) { if(prevProps.currentBrewRendererPageNum !== this.props.currentBrewRendererPageNum) {
@@ -144,15 +133,15 @@ const Editor = createReactClass({
}, },
updateCurrentCursorPage : function(cursor) { updateCurrentCursorPage : function(cursor) {
const lines = this.props.brew.text.split("\n").slice(1, cursor.line + 1); const lines = this.props.brew.text.split('\n').slice(1, cursor.line + 1);
const pageRegex = this.props.brew.renderer == "V3" ? PAGEBREAK_REGEX_V3 : /\\page/; const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1); const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
this.props.onCursorPageChange(currentPage); this.props.onCursorPageChange(currentPage);
}, },
updateCurrentViewPage : function(topScrollLine) { updateCurrentViewPage : function(topScrollLine) {
const lines = this.props.brew.text.split("\n").slice(1, topScrollLine + 1); const lines = this.props.brew.text.split('\n').slice(1, topScrollLine + 1);
const pageRegex = this.props.brew.renderer == "V3" ? PAGEBREAK_REGEX_V3 : /\\page/; const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1); const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
this.props.onViewPageChange(currentPage); this.props.onViewPageChange(currentPage);
}, },
@@ -162,25 +151,21 @@ const Editor = createReactClass({
}, },
handleViewChange : function(newView){ handleViewChange : function(newView){
this.props.setMoveArrows(newView === "text"); this.props.setMoveArrows(newView === 'text');
this.setState( this.setState({
{ view : newView
view: newView, }, ()=>{
},
() => {
this.codeEditor.current?.codeMirror?.focus(); this.codeEditor.current?.codeMirror?.focus();
}, });
);
}, },
highlightCustomMarkdown : function(){ highlightCustomMarkdown : function(){
if(!this.codeEditor.current?.codeMirror) return; if(!this.codeEditor.current?.codeMirror) return;
if (this.state.view === "text" || this.state.view === "snippet") { if((this.state.view === 'text') ||(this.state.view === 'snippet')) {
const codeMirror = this.codeEditor.current.codeMirror; const codeMirror = this.codeEditor.current.codeMirror;
codeMirror?.operation(() => { codeMirror?.operation(()=>{ // Batch CodeMirror styling
// Batch CodeMirror styling
const foldLines = []; const foldLines = [];
@@ -199,86 +184,68 @@ const Editor = createReactClass({
let userSnippetCount = 1; // start snippet count from snippet 1 let userSnippetCount = 1; // start snippet count from snippet 1
let editorPageCount = 1; // start page count from page 1 let editorPageCount = 1; // start page count from page 1
const whichSource = this.state.view === "text" ? this.props.brew.text : this.props.brew.snippets; const whichSource = this.state.view === 'text' ? this.props.brew.text : this.props.brew.snippets;
_.forEach(whichSource?.split("\n"), (line, lineNumber) => { _.forEach(whichSource?.split('\n'), (line, lineNumber)=>{
const tabHighlight = this.state.view === "text" ? "pageLine" : "snippetLine";
const textOrSnip = this.state.view === "text"; const tabHighlight = this.state.view === 'text' ? 'pageLine' : 'snippetLine';
const textOrSnip = this.state.view === 'text';
//reset custom line styles //reset custom line styles
codeMirror?.removeLineClass(lineNumber, "background", "pageLine"); codeMirror?.removeLineClass(lineNumber, 'background', 'pageLine');
codeMirror?.removeLineClass(lineNumber, "background", "snippetLine"); codeMirror?.removeLineClass(lineNumber, 'background', 'snippetLine');
codeMirror?.removeLineClass(lineNumber, "text"); codeMirror?.removeLineClass(lineNumber, 'text');
codeMirror?.removeLineClass(lineNumber, "wrap", "sourceMoveFlash"); codeMirror?.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
// Don't process lines inside folded text // Don't process lines inside folded text
// If the current lineNumber is inside any folded marks, skip line styling // If the current lineNumber is inside any folded marks, skip line styling
if (foldLines.some((fold) => lineNumber >= fold.from && lineNumber <= fold.to)) return; if(foldLines.some((fold)=>lineNumber >= fold.from && lineNumber <= fold.to))
return;
// Styling for \page breaks // Styling for \page breaks
if ( if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
(this.props.renderer == "legacy" && line.includes("\\page")) || (this.props.renderer == 'V3' && line.match(textOrSnip ? PAGEBREAK_REGEX_V3 : SNIPPETBREAK_REGEX_V3))) {
(this.props.renderer == "V3" &&
line.match(textOrSnip ? PAGEBREAK_REGEX_V3 : SNIPPETBREAK_REGEX_V3)) if((lineNumber > 0) && (textOrSnip)) // Since \page is optional on first line of document,
) {
if (lineNumber > 0 && textOrSnip)
// Since \page is optional on first line of document,
editorPageCount += 1; // don't use it to increment page count; stay at 1 editorPageCount += 1; // don't use it to increment page count; stay at 1
else if (this.state.view !== "text") userSnippetCount += 1; else if(this.state.view !== 'text') userSnippetCount += 1;
// add back the original class 'background' but also add the new class '.pageline' // add back the original class 'background' but also add the new class '.pageline'
codeMirror?.addLineClass(lineNumber, "background", tabHighlight); codeMirror?.addLineClass(lineNumber, 'background', tabHighlight);
const pageCountElement = Object.assign(document.createElement("span"), { const pageCountElement = Object.assign(document.createElement('span'), {
className: "editor-page-count", className : 'editor-page-count',
textContent: textOrSnip ? editorPageCount : userSnippetCount, textContent : textOrSnip ? editorPageCount : userSnippetCount
}); });
codeMirror?.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement); codeMirror?.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
} };
// New CodeMirror styling for V3 renderer // New CodeMirror styling for V3 renderer
if (this.props.renderer === "V3") { if(this.props.renderer === 'V3') {
if(line.match(/^\\column(?:break)?$/)){ if(line.match(/^\\column(?:break)?$/)){
codeMirror?.addLineClass(lineNumber, "text", "columnSplit"); codeMirror?.addLineClass(lineNumber, 'text', 'columnSplit');
} }
// definition lists // definition lists
if (line.includes("::")) { if(line.includes('::')){
if (/^:*$/.test(line) == true) { if(/^:*$/.test(line) == true){ return; };
return; const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
}
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/dmy; // the `d` flag, for match indices, throws an ESLint error.
let match; let match;
while ((match = regex.exec(line)) != null){ while ((match = regex.exec(line)) != null){
codeMirror?.markText( codeMirror?.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' });
{ line: lineNumber, ch: match.indices[0][0] }, codeMirror?.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
{ line: lineNumber, ch: match.indices[0][1] }, codeMirror?.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
{ className: "dl-highlight" },
);
codeMirror?.markText(
{ line: lineNumber, ch: match.indices[1][0] },
{ line: lineNumber, ch: match.indices[1][1] },
{ className: "dt-highlight" },
);
codeMirror?.markText(
{ line: lineNumber, ch: match.indices[2][0] },
{ line: lineNumber, ch: match.indices[2][1] },
{ className: "dd-highlight" },
);
const ddIndex = match.indices[2][0]; const ddIndex = match.indices[2][0];
const colons = /::/g; const colons = /::/g;
const colonMatches = colons.exec(match[2]); const colonMatches = colons.exec(match[2]);
if(colonMatches !== null){ if(colonMatches !== null){
codeMirror?.markText( codeMirror?.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight' });
{ line: lineNumber, ch: colonMatches.index + ddIndex },
{ line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex },
{ className: "dl-colon-highlight" },
);
} }
} }
} }
// Subscript & Superscript // Subscript & Superscript
if (line.includes("^")) { if(line.includes('^')) {
let startIndex = line.indexOf("^"); let startIndex = line.indexOf('^');
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy; const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy; const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
@@ -288,38 +255,27 @@ const Editor = createReactClass({
const match = subRegex.exec(line) || superRegex.exec(line); const match = subRegex.exec(line) || superRegex.exec(line);
if(match) { if(match) {
isSuper = !subRegex.lastIndex; isSuper = !subRegex.lastIndex;
codeMirror?.markText( codeMirror?.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
{ line: lineNumber, ch: match.index },
{ line: lineNumber, ch: match.index + match[0].length },
{ className: isSuper ? "superscript" : "subscript" },
);
} }
startIndex = line.indexOf( startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex));
"^",
Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex),
);
} }
} }
// Injections: single-line {…} // Highlight injectors {style}
if (line.includes("{") && line.includes("}")) { if(line.includes('{') && line.includes('}')){
const injectionRegex = const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
/(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
let match; let match;
while ((match = injectionRegex.exec(line)) !== null) { while ((match = regex.exec(line)) != null) {
tokens.push({ codeMirror?.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
line: lineNumber,
from: match.index,
to: match.index + match[1].length,
type: "injection",
});
} }
} else if (line.includes("{{") && line.includes("}}")) { // Inline blocks: single-line {{…}} }
const spanRegex = /{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *|}}/g; // Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *|}}/g;
let match; let match;
let blockCount = 0; let blockCount = 0;
while ((match = spanRegex.exec(line)) !== null) { while ((match = regex.exec(line)) != null) {
if (match[0].startsWith("{{")) { if(match[0].startsWith('{')) {
blockCount += 1; blockCount += 1;
} else { } else {
blockCount -= 1; blockCount -= 1;
@@ -328,31 +284,21 @@ const Editor = createReactClass({
blockCount = 0; blockCount = 0;
continue; continue;
} }
tokens.push({ codeMirror?.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
line: lineNumber,
from: match.index,
to: match.index + match[0].length,
type: "inline-block",
});
} }
} else if (line.trimLeft().startsWith("{{") || line.trimLeft().startsWith("}}")) { } else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}} // Highlight block divs {{\n Content \n}}
let endCh = line.length+1; let endCh = line.length+1;
const match = line.match( const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/);
/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/, if(match)
); endCh = match.index+match[0].length;
if (match) endCh = match.index + match[0].length; codeMirror?.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
codeMirror?.markText(
{ line: lineNumber, ch: 0 },
{ line: lineNumber, ch: endCh },
{ className: "block" },
);
} }
// Emojis // Emojis
if(line.match(/:[^\s:]+:/g)) { if(line.match(/:[^\s:]+:/g)) {
let startIndex = line.indexOf(":"); let startIndex = line.indexOf(':');
const emojiRegex = /:[^\s:]+:/gy; const emojiRegex = /:[^\s:]+:/gy;
while (startIndex >= 0) { while (startIndex >= 0) {
@@ -360,8 +306,9 @@ const Editor = createReactClass({
const match = emojiRegex.exec(line); const match = emojiRegex.exec(line);
if(match) { if(match) {
let tokens = Markdown.marked.lexer(match[0]); let tokens = Markdown.marked.lexer(match[0]);
tokens = tokens[0].tokens.filter((t) => t.type == "emoji"); tokens = tokens[0].tokens.filter((t)=>t.type == 'emoji');
if (!tokens.length) return; if(!tokens.length)
return;
const startPos = { line: lineNumber, ch: match.index }; const startPos = { line: lineNumber, ch: match.index };
const endPos = { line: lineNumber, ch: match.index + match[0].length }; const endPos = { line: lineNumber, ch: match.index + match[0].length };
@@ -371,9 +318,9 @@ const Editor = createReactClass({
marks.forEach(function(marker) { marks.forEach(function(marker) {
if(!marker.__isFold) marker.clear(); if(!marker.__isFold) marker.clear();
}); });
codeMirror?.markText(startPos, endPos, { className: "emoji" }); codeMirror?.markText(startPos, endPos, { className: 'emoji' });
} }
startIndex = line.indexOf(":", Math.max(startIndex + 1, emojiRegex.lastIndex)); startIndex = line.indexOf(':', Math.max(startIndex + 1, emojiRegex.lastIndex));
} }
} }
} }
@@ -383,28 +330,26 @@ const Editor = createReactClass({
}, },
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){ brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
if (!window || !this.isText() || isJumping) return; if(!window || !this.isText() || isJumping)
return;
// Get current brewRenderer scroll position and calculate target position // Get current brewRenderer scroll position and calculate target position
const brewRenderer = window.frames["BrewRenderer"].contentDocument.getElementsByClassName("brewRenderer")[0]; const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
const currentPos = brewRenderer.scrollTop; const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames["BrewRenderer"].contentDocument const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
.getElementById(`p${targetPage}`)
.getBoundingClientRect().top;
let scrollingTimeout; let scrollingTimeout;
const checkIfScrollComplete = () => { const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
// Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{ scrollingTimeout = setTimeout(()=>{
isJumping = false; isJumping = false;
brewRenderer.removeEventListener("scroll", checkIfScrollComplete); brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
}, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done }, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done
}; };
isJumping = true; isJumping = true;
checkIfScrollComplete(); checkIfScrollComplete();
brewRenderer.addEventListener("scroll", checkIfScrollComplete); brewRenderer.addEventListener('scroll', checkIfScrollComplete);
if(smooth) { if(smooth) {
const bouncePos = targetPos >= 0 ? -30 : 30; //Do a little bounce before scrolling const bouncePos = targetPos >= 0 ? -30 : 30; //Do a little bounce before scrolling
@@ -412,50 +357,43 @@ const Editor = createReactClass({
const scrollDelay = 500; const scrollDelay = 500;
if(!this.throttleBrewMove) { if(!this.throttleBrewMove) {
this.throttleBrewMove = _.throttle( this.throttleBrewMove = _.throttle((currentPos, bouncePos, targetPos)=>{
(currentPos, bouncePos, targetPos) => { brewRenderer.scrollTo({ top: currentPos + bouncePos, behavior: 'smooth' });
brewRenderer.scrollTo({ top: currentPos + bouncePos, behavior: "smooth" });
setTimeout(()=>{ setTimeout(()=>{
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: "smooth", block: "start" }); brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
}, bounceDelay); }, bounceDelay);
}, }, scrollDelay, { leading: true, trailing: false });
scrollDelay, };
{ leading: true, trailing: false },
);
}
this.throttleBrewMove(currentPos, bouncePos, targetPos); this.throttleBrewMove(currentPos, bouncePos, targetPos);
} else { } else {
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: "instant", block: "start" }); brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'instant', block: 'start' });
} }
}, },
sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){ sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
if (!this.isText() || isJumping) return; if(!this.isText() || isJumping)
return;
const textSplit = this.props.renderer == "V3" ? PAGEBREAK_REGEX_V3 : /\\page/; const textSplit = this.props.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
const textString = this.props.brew.text const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
.split(textSplit) const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
.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 currentY = this.codeEditor.current.codeMirror?.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, "local", true); let targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, 'local', true);
let scrollingTimeout; let scrollingTimeout;
const checkIfScrollComplete = () => { const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
// Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{ scrollingTimeout = setTimeout(()=>{
isJumping = false; isJumping = false;
this.codeEditor.current.codeMirror?.off("scroll", checkIfScrollComplete); this.codeEditor.current.codeMirror?.off('scroll', checkIfScrollComplete);
}, 150); // If 150 ms pass without a scroll event, assume scrolling is done }, 150); // If 150 ms pass without a scroll event, assume scrolling is done
}; };
isJumping = true; isJumping = true;
checkIfScrollComplete(); checkIfScrollComplete();
if(this.codeEditor.current?.codeMirror) { if(this.codeEditor.current?.codeMirror) {
this.codeEditor.current.codeMirror?.on("scroll", checkIfScrollComplete); this.codeEditor.current.codeMirror?.on('scroll', checkIfScrollComplete);
} }
if(smooth) { if(smooth) {
@@ -466,20 +404,20 @@ const Editor = createReactClass({
// Update target: target height is not accurate until within +-10 lines of the visible window // Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY > 100)) if(Math.abs(targetY - currentY > 100))
targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, "local", true); targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, 'local', true);
// End when close enough // End when close enough
if(Math.abs(targetY - currentY) < 1) { if(Math.abs(targetY - currentY) < 1) {
this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 }); this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, "wrap", "sourceMoveFlash"); this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
clearInterval(incrementalScroll); clearInterval(incrementalScroll);
} }
}, 10); }, 10);
} else { } else {
this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 }); this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, "wrap", "sourceMoveFlash"); this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
} }
}, },
@@ -489,7 +427,7 @@ const Editor = createReactClass({
updateEditorTheme : function(newTheme){ updateEditorTheme : function(newTheme){
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme); window.localStorage.setItem(EDITOR_THEME_KEY, newTheme);
this.setState({ this.setState({
editorTheme: newTheme, editorTheme : newTheme
}); });
}, },
@@ -500,82 +438,63 @@ const Editor = createReactClass({
renderEditor : function(){ renderEditor : function(){
if(this.isText()){ if(this.isText()){
return ( return <>
<> <CodeEditor key='codeEditor'
<CodeEditor
key="codeEditor"
ref={this.codeEditor} ref={this.codeEditor}
language="gfm" language='gfm'
tab="brewText" tab='brewText'
view={this.state.view} view={this.state.view}
value={this.props.brew.text} value={this.props.brew.text}
onChange={this.props.onBrewChange("text")} onChange={this.props.onBrewChange('text')}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
/> </>;
</>
);
} }
if(this.isStyle()){ if(this.isStyle()){
return ( return <>
<> <CodeEditor key='codeEditor'
<CodeEditor
key="codeEditor"
ref={this.codeEditor} ref={this.codeEditor}
language="css" language='css'
tab="brewStyles" tab='brewStyles'
view={this.state.view} view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT} value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onBrewChange("style")} onChange={this.props.onBrewChange('style')}
enableFolding={true} enableFolding={true}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
/> </>;
</>
);
} }
if(this.isMeta()){ if(this.isMeta()){
return ( return <>
<> <CodeEditor key='codeEditor'
<CodeEditor
key="codeEditor"
view={this.state.view} view={this.state.view}
style={{ display: "none" }} style={{ display: 'none' }}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent} />
/>
<MetadataEditor <MetadataEditor
metadata={this.props.brew} metadata={this.props.brew}
themeBundle={this.props.themeBundle} themeBundle={this.props.themeBundle}
onChange={this.props.onBrewChange("metadata")} onChange={this.props.onBrewChange('metadata')}
reportError={this.props.reportError} reportError={this.props.reportError}
userThemes={this.props.userThemes} userThemes={this.props.userThemes}/>
/> </>;
</>
);
} }
if(this.isSnip()){ if(this.isSnip()){
if (!this.props.brew.snippets) { if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; }
this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; return <>
} <CodeEditor key='codeEditor'
return (
<>
<CodeEditor
key="codeEditor"
ref={this.codeEditor} ref={this.codeEditor}
language="gfm" language='gfm'
tab="brewSnippets" tab='brewSnippets'
view={this.state.view} view={this.state.view}
value={this.props.brew.snippets} value={this.props.brew.snippets}
onChange={this.props.onBrewChange("snippets")} onChange={this.props.onBrewChange('snippets')}
enableFolding={true} enableFolding={true}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
style={{ height: `calc(100% -${this.state.snippetBarHeight}px)` }} style={{ height: `calc(100% -${this.state.snippetBarHeight}px)` }} />
/> </>;
</>
);
} }
}, },
@@ -601,7 +520,7 @@ const Editor = createReactClass({
render : function(){ render : function(){
return ( return (
<div className="editor" ref={this.editor}> <div className='editor' ref={this.editor}>
<SnippetBar <SnippetBar
brew={this.props.brew} brew={this.props.brew}
view={this.state.view} view={this.state.view}
@@ -625,7 +544,7 @@ const Editor = createReactClass({
{this.renderEditor()} {this.renderEditor()}
</div> </div>
); );
}, }
}); });
export default Editor; export default Editor;