mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-24 16:22:44 +00:00
Merge branch 'master' into Pagella
This commit is contained in:
@@ -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), []);
|
||||
|
||||
@@ -17,7 +17,7 @@ const NotificationPopup = ()=>{
|
||||
<ul>
|
||||
<li key='Vault'>
|
||||
<em>Search brews with our new page!</em><br />
|
||||
We have been working very hard in making this possible, now you can share your work and look at it in the new <a href="/vault">Vault</a> page!
|
||||
We have been working very hard in making this possible, now you can share your work and look at it in the new <a href='/vault'>Vault</a> page!
|
||||
All PUBLISHED brews will be available to anyone searching there, by title or author, and filtering by renderer.
|
||||
|
||||
More features will be coming.
|
||||
|
||||
@@ -56,7 +56,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
|
||||
|
||||
} else if(mode == 'fit'){
|
||||
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
||||
const minDimRatio = [...pages].reduce((minRatio, page) => Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
||||
const minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
||||
|
||||
desiredZoom = minDimRatio * 100;
|
||||
}
|
||||
@@ -67,9 +67,9 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
|
||||
return deltaZoom;
|
||||
};
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`}>
|
||||
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible)}}><i className='fas fa-glasses' /></button>
|
||||
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
|
||||
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
||||
<div className='group'>
|
||||
<button
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||
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',
|
||||
@@ -36,16 +36,16 @@ const Editor = createClass({
|
||||
onStyleChange : ()=>{},
|
||||
onMetaChange : ()=>{},
|
||||
reportError : ()=>{},
|
||||
|
||||
|
||||
onCursorPageChange : ()=>{},
|
||||
onViewPageChange : ()=>{},
|
||||
|
||||
editorTheme : 'default',
|
||||
renderer : 'legacy',
|
||||
|
||||
currentEditorCursorPageNum : 0,
|
||||
currentEditorViewPageNum : 0,
|
||||
currentBrewRendererPageNum : 0,
|
||||
currentEditorCursorPageNum : 1,
|
||||
currentEditorViewPageNum : 1,
|
||||
currentBrewRendererPageNum : 1,
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
@@ -63,14 +63,15 @@ const Editor = createClass({
|
||||
isMeta : function() {return this.state.view == 'meta';},
|
||||
|
||||
componentDidMount : function() {
|
||||
|
||||
this.updateEditorSize();
|
||||
this.highlightCustomMarkdown();
|
||||
window.addEventListener('resize', this.updateEditorSize);
|
||||
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));
|
||||
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) {
|
||||
@@ -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();
|
||||
}
|
||||
@@ -117,14 +128,14 @@ 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);
|
||||
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);
|
||||
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
||||
this.props.onViewPageChange(currentPage);
|
||||
},
|
||||
|
||||
@@ -156,7 +167,7 @@ const Editor = createClass({
|
||||
// Record details of folded sections
|
||||
if(mark.__isFold) {
|
||||
const fold = mark.find();
|
||||
foldLines.push({from: fold.from?.line, to: fold.to?.line});
|
||||
foldLines.push({ from: fold.from?.line, to: fold.to?.line });
|
||||
}
|
||||
return !mark.__isFold;
|
||||
}); //Don't undo code folding
|
||||
@@ -174,7 +185,7 @@ const Editor = createClass({
|
||||
|
||||
// Don't process lines inside folded text
|
||||
// If the current lineNumber is inside any folded marks, skip line styling
|
||||
if (foldLines.some(fold => lineNumber >= fold.from && lineNumber <= fold.to))
|
||||
if(foldLines.some((fold)=>lineNumber >= fold.from && lineNumber <= fold.to))
|
||||
return;
|
||||
|
||||
// Styling for \page breaks
|
||||
@@ -200,7 +211,7 @@ const Editor = createClass({
|
||||
|
||||
// definition lists
|
||||
if(line.includes('::')){
|
||||
if(/^:*$/.test(line) == true){ return };
|
||||
if(/^:*$/.test(line) == true){ return; };
|
||||
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
|
||||
let match;
|
||||
while ((match = regex.exec(line)) != null){
|
||||
@@ -208,10 +219,10 @@ const Editor = createClass({
|
||||
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];
|
||||
let colons = /::/g;
|
||||
let colonMatches = colons.exec(match[2]);
|
||||
const colons = /::/g;
|
||||
const colonMatches = colons.exec(match[2]);
|
||||
if(colonMatches !== null){
|
||||
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight'} )
|
||||
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight' });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,12 +232,12 @@ const Editor = createClass({
|
||||
let startIndex = line.indexOf('^');
|
||||
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
|
||||
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
|
||||
|
||||
|
||||
while (startIndex >= 0) {
|
||||
superRegex.lastIndex = subRegex.lastIndex = startIndex;
|
||||
let isSuper = false;
|
||||
let match = subRegex.exec(line) || superRegex.exec(line);
|
||||
if (match) {
|
||||
const match = subRegex.exec(line) || superRegex.exec(line);
|
||||
if(match) {
|
||||
isSuper = !subRegex.lastIndex;
|
||||
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
|
||||
}
|
||||
@@ -276,18 +287,18 @@ const Editor = createClass({
|
||||
|
||||
while (startIndex >= 0) {
|
||||
emojiRegex.lastIndex = startIndex;
|
||||
let match = emojiRegex.exec(line);
|
||||
if (match) {
|
||||
const match = emojiRegex.exec(line);
|
||||
if(match) {
|
||||
let tokens = Markdown.marked.lexer(match[0]);
|
||||
tokens = tokens[0].tokens.filter(t => t.type == 'emoji')
|
||||
if (!tokens.length)
|
||||
tokens = tokens[0].tokens.filter((t)=>t.type == 'emoji');
|
||||
if(!tokens.length)
|
||||
return;
|
||||
|
||||
let startPos = { line: lineNumber, ch: match.index };
|
||||
let endPos = { line: lineNumber, ch: match.index + match[0].length };
|
||||
const startPos = { line: lineNumber, ch: match.index };
|
||||
const endPos = { line: lineNumber, ch: match.index + match[0].length };
|
||||
|
||||
// Iterate over conflicting marks and clear them
|
||||
var marks = codeMirror.findMarks(startPos, endPos);
|
||||
const marks = codeMirror.findMarks(startPos, endPos);
|
||||
marks.forEach(function(marker) {
|
||||
if(!marker.__isFold) marker.clear();
|
||||
});
|
||||
@@ -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;
|
||||
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, this.props.currentBrewRendererPageNum-1).join(textSplit);
|
||||
const textPosition = textString.length;
|
||||
const lineCount = textString.match('\n') ? textString.slice(0, textPosition).split('\n').length : 0;
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
//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);
|
||||
isJumping = true;
|
||||
checkIfScrollComplete();
|
||||
this.codeEditor.current.codeMirror.on('scroll', checkIfScrollComplete);
|
||||
|
||||
// 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);
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
// 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} />
|
||||
</>;
|
||||
@@ -462,7 +500,9 @@ const Editor = createClass({
|
||||
currentEditorTheme={this.state.editorTheme}
|
||||
updateEditorTheme={this.updateEditorTheme}
|
||||
snippetBundle={this.props.snippetBundle}
|
||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
|
||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
|
||||
updateBrew={this.props.updateBrew}
|
||||
/>
|
||||
|
||||
{this.renderEditor()}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,8 @@ const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
import { getHistoryItems, historyExists } from '../../utils/versionHistory.js';
|
||||
|
||||
//Import all themes
|
||||
const ThemeSnippets = {};
|
||||
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
||||
@@ -38,7 +40,8 @@ const Snippetbar = createClass({
|
||||
unfoldCode : ()=>{},
|
||||
updateEditorTheme : ()=>{},
|
||||
cursorPos : {},
|
||||
snippetBundle : []
|
||||
snippetBundle : [],
|
||||
updateBrew : ()=>{}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -46,7 +49,8 @@ const Snippetbar = createClass({
|
||||
return {
|
||||
renderer : this.props.renderer,
|
||||
themeSelector : false,
|
||||
snippets : []
|
||||
snippets : [],
|
||||
historyExists : false
|
||||
};
|
||||
},
|
||||
|
||||
@@ -59,18 +63,20 @@ const Snippetbar = createClass({
|
||||
|
||||
componentDidUpdate : async function(prevProps) {
|
||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
||||
const snippets = this.compileSnippets();
|
||||
this.setState({
|
||||
snippets : snippets
|
||||
snippets : this.compileSnippets()
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
this.setState({
|
||||
historyExists : historyExists(this.props.brew)
|
||||
});
|
||||
},
|
||||
|
||||
mergeCustomizer : function(oldValue, newValue, key) {
|
||||
if(key == 'snippets') {
|
||||
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
|
||||
return result.filter(snip => snip.gen || snip.subsnippets);
|
||||
return result.filter((snip)=>snip.gen || snip.subsnippets);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -138,6 +144,36 @@ const Snippetbar = createClass({
|
||||
});
|
||||
},
|
||||
|
||||
replaceContent : function(item){
|
||||
return this.props.updateBrew(item);
|
||||
},
|
||||
|
||||
renderHistoryItems : function() {
|
||||
const historyItems = getHistoryItems(this.props.brew);
|
||||
|
||||
return <div className='dropdown'>
|
||||
{_.map(historyItems, (item, index)=>{
|
||||
if(!item.savedAt) return;
|
||||
|
||||
const saveTime = new Date(item.savedAt);
|
||||
const diffMs = new Date() - saveTime;
|
||||
const diffSecs = Math.floor(diffMs / 1000);
|
||||
|
||||
let diffString = `about ${diffSecs} seconds ago`;
|
||||
|
||||
if(diffSecs > 60) diffString = `about ${Math.floor(diffSecs / 60)} minutes ago`;
|
||||
if(diffSecs > (60 * 60)) diffString = `about ${Math.floor(diffSecs / (60 * 60))} hours ago`;
|
||||
if(diffSecs > (24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (24 * 60 * 60))} days ago`;
|
||||
if(diffSecs > (7 * 24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (7 * 24 * 60 * 60))} weeks ago`;
|
||||
|
||||
return <div className='snippet' key={index} onClick={()=>{this.replaceContent(item);}} >
|
||||
<i className={`fas fa-${index+1}`} />
|
||||
<span className='name' title={saveTime.toISOString()}>v{item.version} : {diffString}</span>
|
||||
</div>;
|
||||
})}
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderEditorButtons : function(){
|
||||
if(!this.props.showEditButtons) return;
|
||||
|
||||
@@ -158,6 +194,10 @@ const Snippetbar = createClass({
|
||||
}
|
||||
|
||||
return <div className='editors'>
|
||||
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`} >
|
||||
<i className='fas fa-clock-rotate-left' />
|
||||
{this.state.historyExists && this.renderHistoryItems() }
|
||||
</div>
|
||||
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||
onClick={this.props.undo} >
|
||||
<i className='fas fa-undo' />
|
||||
|
||||
@@ -53,6 +53,21 @@
|
||||
font-size : 0.75em;
|
||||
color : inherit;
|
||||
}
|
||||
&.history {
|
||||
.tooltipLeft('History');
|
||||
font-size : 0.75em;
|
||||
color : grey;
|
||||
position : relative;
|
||||
&.active {
|
||||
color : inherit;
|
||||
}
|
||||
&>.dropdown{
|
||||
right : -1px;
|
||||
&>.snippet{
|
||||
padding-right : 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.editorTheme {
|
||||
.tooltipLeft('Editor Themes');
|
||||
font-size : 0.75em;
|
||||
|
||||
@@ -111,7 +111,7 @@ const ErrorNavItem = createClass({
|
||||
Looks like there was a problem retreiving
|
||||
the theme, or a theme that it inherits,
|
||||
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||
{response.body.brewId}</a> still exists!
|
||||
{response.body.brewId}</a> still exists!
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
display : flex;
|
||||
align-items : center;
|
||||
&:last-child .navItem { border-left : 1px solid #666666; }
|
||||
|
||||
&:has(.brewTitle) {
|
||||
flex-grow : 1;
|
||||
min-width : 300px;
|
||||
}
|
||||
}
|
||||
// "NaturalCrit" logo
|
||||
.navLogo {
|
||||
@@ -69,6 +74,10 @@
|
||||
.navItem {
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
display : flex;
|
||||
align-items : center;
|
||||
justify-content : center;
|
||||
height : 100%;
|
||||
padding : 8px 12px;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
@@ -94,39 +103,20 @@
|
||||
animation-duration : 2s;
|
||||
}
|
||||
}
|
||||
&.editTitle { // this is not needed at all currently - you used to be able to edit the title via the navbar.
|
||||
padding : 2px 12px;
|
||||
input {
|
||||
width : 250px;
|
||||
padding : 2px;
|
||||
margin : 0;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
background-color : transparent;
|
||||
border : 1px solid @blue;
|
||||
outline : none;
|
||||
}
|
||||
.charCount {
|
||||
display : inline-block;
|
||||
margin-left : 8px;
|
||||
color : #666666;
|
||||
text-align : right;
|
||||
vertical-align : bottom;
|
||||
&.max { color : @red; }
|
||||
}
|
||||
}
|
||||
&.brewTitle {
|
||||
flex-grow : 1;
|
||||
display : block;
|
||||
width : 100%;
|
||||
overflow : hidden;
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
text-transform : initial;
|
||||
background-color : transparent;
|
||||
text-overflow : ellipsis;
|
||||
text-transform : initial;
|
||||
white-space : nowrap;
|
||||
background-color : transparent;
|
||||
}
|
||||
|
||||
// "The Homebrewery" logo
|
||||
&.homebrewLogo {
|
||||
.animate(color);
|
||||
@@ -240,23 +230,25 @@
|
||||
}
|
||||
.navDropdownContainer {
|
||||
position : relative;
|
||||
height : 100%;
|
||||
|
||||
.navDropdown {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
right: 0px;
|
||||
z-index: 10000;
|
||||
width: max-content;
|
||||
min-width:100%;
|
||||
max-height: calc(100vh - 28px);
|
||||
overflow: hidden auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
position : absolute;
|
||||
//top: 28px;
|
||||
right : 0px;
|
||||
z-index : 10000;
|
||||
display : flex;
|
||||
flex-direction : column;
|
||||
align-items : flex-end;
|
||||
width : max-content;
|
||||
min-width : 100%;
|
||||
max-height : calc(100vh - 28px);
|
||||
overflow : hidden auto;
|
||||
.navItem {
|
||||
position : relative;
|
||||
display : flex;
|
||||
justify-content : space-between;
|
||||
align-items : center;
|
||||
justify-content : space-between;
|
||||
width : 100%;
|
||||
border : 1px solid #888888;
|
||||
border-bottom : 0;
|
||||
@@ -278,10 +270,10 @@
|
||||
overflow : hidden auto;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
background-color : #333333;
|
||||
border-top : 1px solid #888888;
|
||||
scrollbar-color : #666666 #333333;
|
||||
scrollbar-width : thin;
|
||||
background-color : #333333;
|
||||
border-top : 1px solid #888888;
|
||||
.clear {
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
|
||||
@@ -28,6 +28,8 @@ const Markdown = require('naturalcrit/markdown.js');
|
||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
|
||||
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
||||
|
||||
const googleDriveIcon = require('../../googleDrive.svg');
|
||||
|
||||
const SAVE_TIMEOUT = 3000;
|
||||
@@ -55,9 +57,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 +119,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);
|
||||
|
||||
@@ -168,6 +166,16 @@ const EditPage = createClass({
|
||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||
},
|
||||
|
||||
updateBrew : function(newData){
|
||||
this.setState((prevState)=>({
|
||||
brew : {
|
||||
...prevState.brew,
|
||||
style : newData.style,
|
||||
text : newData.text
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
trySave : function(immediate=false){
|
||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||
if(this.hasChanges()){
|
||||
@@ -220,6 +228,9 @@ const EditPage = createClass({
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
|
||||
updateHistory(this.state.brew);
|
||||
versionHistoryGarbageCollection();
|
||||
|
||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||
|
||||
const brew = this.state.brew;
|
||||
@@ -431,6 +442,7 @@ const EditPage = createClass({
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
snippetBundle={this.state.themeBundle.snippets}
|
||||
updateBrew={this.updateBrew}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
|
||||
|
||||
@@ -330,7 +330,7 @@ const VaultPage = (props)=>{
|
||||
|
||||
if(error) {
|
||||
const errorText = ErrorIndex()[error.HBErrorCode.toString()] || '';
|
||||
|
||||
|
||||
return (
|
||||
<div className='foundBrews noBrews'>
|
||||
<h3>Error: {errorText}</h3>
|
||||
|
||||
116
client/homebrew/utils/versionHistory.js
Normal file
116
client/homebrew/utils/versionHistory.js
Normal file
@@ -0,0 +1,116 @@
|
||||
export const HISTORY_PREFIX = 'HOMEBREWERY-HISTORY';
|
||||
export const HISTORY_SLOTS = 5;
|
||||
|
||||
// History values in minutes
|
||||
const DEFAULT_HISTORY_SAVE_DELAYS = {
|
||||
'0' : 0,
|
||||
'1' : 2,
|
||||
'2' : 10,
|
||||
'3' : 60,
|
||||
'4' : 12 * 60,
|
||||
'5' : 2 * 24 * 60
|
||||
};
|
||||
|
||||
const DEFAULT_GARBAGE_COLLECT_DELAY = 28 * 24 * 60;
|
||||
|
||||
const HISTORY_SAVE_DELAYS = global.config?.historyData?.HISTORY_SAVE_DELAYS ?? DEFAULT_HISTORY_SAVE_DELAYS;
|
||||
const GARBAGE_COLLECT_DELAY = global.config?.historyData?.GARBAGE_COLLECT_DELAY ?? DEFAULT_GARBAGE_COLLECT_DELAY;
|
||||
|
||||
|
||||
|
||||
function getKeyBySlot(brew, slot){
|
||||
return `${HISTORY_PREFIX}-${brew.shareId}-${slot}`;
|
||||
};
|
||||
|
||||
function getVersionBySlot(brew, slot){
|
||||
// Read stored brew data
|
||||
// - If it exists, parse data to object
|
||||
// - If it doesn't exist, pass default object
|
||||
const key = getKeyBySlot(brew, slot);
|
||||
const storedVersion = localStorage.getItem(key);
|
||||
const output = storedVersion ? JSON.parse(storedVersion) : { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true };
|
||||
return output;
|
||||
};
|
||||
|
||||
function updateStoredBrew(brew, slot = 0) {
|
||||
const archiveBrew = {
|
||||
title : brew.title,
|
||||
text : brew.text,
|
||||
style : brew.style,
|
||||
version : brew.version,
|
||||
shareId : brew.shareId,
|
||||
savedAt : brew?.savedAt || new Date(),
|
||||
expireAt : new Date()
|
||||
};
|
||||
|
||||
archiveBrew.expireAt.setMinutes(archiveBrew.expireAt.getMinutes() + HISTORY_SAVE_DELAYS[slot]);
|
||||
|
||||
const key = getKeyBySlot(brew, slot);
|
||||
localStorage.setItem(key, JSON.stringify(archiveBrew));
|
||||
}
|
||||
|
||||
|
||||
export function historyExists(brew){
|
||||
return Object.keys(localStorage)
|
||||
.some((key)=>{
|
||||
return key.startsWith(`${HISTORY_PREFIX}-${brew.shareId}`);
|
||||
});
|
||||
}
|
||||
|
||||
export function loadHistory(brew){
|
||||
const history = {};
|
||||
|
||||
// Load data from local storage to History object
|
||||
for (let i = 1; i <= HISTORY_SLOTS; i++){
|
||||
history[i] = getVersionBySlot(brew, i);
|
||||
};
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
export function updateHistory(brew) {
|
||||
const history = loadHistory(brew);
|
||||
|
||||
// Walk each version position
|
||||
for (let slot = HISTORY_SLOTS; slot > 0; slot--){
|
||||
const storedVersion = history[slot];
|
||||
|
||||
// If slot has expired, update all lower slots and break
|
||||
if(new Date() >= new Date(storedVersion.expireAt)){
|
||||
for (let updateSlot = slot - 1; updateSlot>0; updateSlot--){
|
||||
// Move data from updateSlot to updateSlot + 1
|
||||
!history[updateSlot]?.noData && updateStoredBrew(history[updateSlot], updateSlot + 1);
|
||||
};
|
||||
|
||||
// Update the most recent brew
|
||||
updateStoredBrew(brew, 1);
|
||||
|
||||
// Break out of data checks because we found an expired value
|
||||
break;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export function getHistoryItems(brew){
|
||||
const historyArray = [];
|
||||
|
||||
for (let i = 1; i <= HISTORY_SLOTS; i++){
|
||||
historyArray.push(getVersionBySlot(brew, i));
|
||||
}
|
||||
|
||||
return historyArray;
|
||||
};
|
||||
|
||||
export function versionHistoryGarbageCollection(){
|
||||
Object.keys(localStorage)
|
||||
.filter((key)=>{
|
||||
return key.startsWith(HISTORY_PREFIX);
|
||||
})
|
||||
.forEach((key)=>{
|
||||
const collectAt = new Date(JSON.parse(localStorage.getItem(key)).savedAt);
|
||||
collectAt.setMinutes(collectAt.getMinutes() + GARBAGE_COLLECT_DELAY);
|
||||
if(new Date() > collectAt){
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -25,7 +25,7 @@
|
||||
"expr-eval": "^2.0.2",
|
||||
"express": "^4.21.0",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.1.7",
|
||||
"express-static-gzip": "2.1.8",
|
||||
"fs-extra": "11.2.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
@@ -6362,12 +6362,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express-static-gzip": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.7.tgz",
|
||||
"integrity": "sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw==",
|
||||
"license": "MIT",
|
||||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.8.tgz",
|
||||
"integrity": "sha512-g8tiJuI9Y9Ffy59ehVXvqb0hhP83JwZiLxzanobPaMbkB5qBWA8nuVgd+rcd5qzH3GkgogTALlc0BaADYwnMbQ==",
|
||||
"dependencies": {
|
||||
"serve-static": "^1.14.1"
|
||||
"serve-static": "^1.16.2"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
"expr-eval": "^2.0.2",
|
||||
"express": "^4.21.0",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.1.7",
|
||||
"express-static-gzip": "2.1.8",
|
||||
"fs-extra": "11.2.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
@@ -138,4 +138,4 @@
|
||||
"stylelint-config-recommended": "^14.0.1",
|
||||
"supertest": "^7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
1121
server/app.js
1121
server/app.js
File diff suppressed because it is too large
Load Diff
@@ -934,7 +934,7 @@ brew`);
|
||||
expect(req.brew).toEqual(testBrew);
|
||||
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith("\nI Have a style!\n");
|
||||
expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
|
||||
expect(res.set).toHaveBeenCalledWith({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/css'
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -399,7 +399,7 @@ const CodeEditor = createClass({
|
||||
},
|
||||
getTopVisibleLine : function(){
|
||||
const rect = this.codeMirror.getWrapperElement().getBoundingClientRect();
|
||||
const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, "window");
|
||||
const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window');
|
||||
return topVisibleLine;
|
||||
},
|
||||
updateSize : function(){
|
||||
|
||||
@@ -105,16 +105,16 @@ renderer.link = function (href, title, text) {
|
||||
// Expose `src` attribute as `--HB_src` to make the URL accessible via CSS
|
||||
renderer.image = function (href, title, text) {
|
||||
href = cleanUrl(href);
|
||||
if (href === null)
|
||||
if(href === null)
|
||||
return text;
|
||||
|
||||
let out = `<img src="${href}" alt="${text}" style="--HB_src:url(${href});"`;
|
||||
if (title)
|
||||
if(title)
|
||||
out += ` title="${title}"`;
|
||||
|
||||
out += '>';
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
// Disable default reflink behavior, as it steps on our variables extension
|
||||
tokenizer.def = function () {
|
||||
@@ -745,7 +745,7 @@ const tableTerminators = [
|
||||
`:+\\n`, // hardBreak
|
||||
` *{[^\n]+}`, // blockInjector
|
||||
` *{{[^{\n]*\n.*?\n}}` // mustacheDiv
|
||||
]
|
||||
];
|
||||
|
||||
Marked.use(MarkedVariables());
|
||||
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts,
|
||||
@@ -755,12 +755,12 @@ Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
||||
Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
||||
|
||||
function cleanUrl(href) {
|
||||
try {
|
||||
href = encodeURI(href).replace(/%25/g, '%');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
try {
|
||||
href = encodeURI(href).replace(/%25/g, '%');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
}
|
||||
|
||||
const escapeTest = /[&<>"']/;
|
||||
|
||||
@@ -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 })} >
|
||||
<i className='fas fa-arrow-right' />
|
||||
</div>
|
||||
<div id='scrollToggleDiv' className={this.state.liveScroll ? 'arrow lock' : 'arrow unlock'}
|
||||
style={{ left: this.state.currentDividerPos-4 }}
|
||||
onClick={this.liveScrollToggle} >
|
||||
<i id='scrollToggle' className={this.state.liveScroll ? 'fas fa-lock' : 'fas fa-unlock'} />
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
},
|
||||
@@ -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,
|
||||
}),
|
||||
})}
|
||||
</Pane>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -27,35 +27,154 @@ module.exports = [
|
||||
experimental : true,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
name : 'Generate Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
gen : TableOfContentsGen,
|
||||
experimental : true
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H3',
|
||||
icon : 'fas fa-dice-three',
|
||||
name : 'Table of Contents Individual Inclusion',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocInclude# CHANGE # to your header level
|
||||
}}\n`,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Individual Inclusion H1',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH1 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Inclusion H2',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH2 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Inclusion H3',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH3 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Inclusion H4',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH4 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Inclusion H5',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH5 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Inclusion H6',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocIncludeH6 \n
|
||||
}}\n`,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name : 'Table of Contents Range Inclusion',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocDepthH3
|
||||
}}\n`,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Include in ToC up to H3',
|
||||
icon : 'fas fa-dice-three',
|
||||
gen : dedent `\n{{tocDepthH3
|
||||
}}\n`,
|
||||
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H4',
|
||||
icon : 'fas fa-dice-four',
|
||||
gen : dedent `\n{{tocDepthH4
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H5',
|
||||
icon : 'fas fa-dice-five',
|
||||
gen : dedent `\n{{tocDepthH5
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H6',
|
||||
icon : 'fas fa-dice-six',
|
||||
gen : dedent `\n{{tocDepthH6
|
||||
}}\n`,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H4',
|
||||
icon : 'fas fa-dice-four',
|
||||
gen : dedent `\n{{tocDepthH4
|
||||
name : 'Table of Contents Individual Exclusion',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH1 \n
|
||||
}}\n`,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Individual Exclusion H1',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH1 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Exclusion H2',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH2 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Exclusion H3',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH3 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Exclusion H4',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH4 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Exclusion H5',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH5 \n
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Individual Exclusion H6',
|
||||
icon : 'fas fa-book',
|
||||
gen : dedent `\n{{tocExcludeH6 \n
|
||||
}}\n`,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Include in ToC up to H5',
|
||||
icon : 'fas fa-dice-five',
|
||||
gen : dedent `\n{{tocDepthH5
|
||||
}}\n`,
|
||||
},
|
||||
{
|
||||
name : 'Include in ToC up to H6',
|
||||
icon : 'fas fa-dice-six',
|
||||
gen : dedent `\n{{tocDepthH6
|
||||
}}\n`,
|
||||
name : 'Table of Contents Toggles',
|
||||
icon : 'fas fa-book',
|
||||
gen : `{{tocGlobalH4}}\n\n`,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Enable H1-H4 all pages',
|
||||
icon : 'fas fa-dice-four',
|
||||
gen : `{{tocGlobalH4}}\n\n`,
|
||||
},
|
||||
{
|
||||
name : 'Enable H1-H5 all pages',
|
||||
icon : 'fas fa-dice-five',
|
||||
gen : `{{tocGlobalH5}}\n\n`,
|
||||
},
|
||||
{
|
||||
name : 'Enable H1-H6 all pages',
|
||||
icon : 'fas fa-dice-six',
|
||||
gen : `{{tocGlobalH6}}\n\n`,
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -94,7 +213,7 @@ module.exports = [
|
||||
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
|
||||
line-height: 1em;
|
||||
}\n\n`
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ const dedent = require('dedent-tabs').default;
|
||||
const mapPages = (pages)=>{
|
||||
let actualPage = 0;
|
||||
let mappedPage = 0; // Number displayed in footer
|
||||
let pageMap = [];
|
||||
const pageMap = [];
|
||||
|
||||
pages.forEach(page => {
|
||||
pages.forEach((page)=>{
|
||||
actualPage++;
|
||||
const doSkip = page.querySelector('.skipCounting');
|
||||
const doReset = page.querySelector('.resetCounting');
|
||||
@@ -24,13 +24,13 @@ const mapPages = (pages)=>{
|
||||
return pageMap;
|
||||
};
|
||||
|
||||
const getMarkdown = (headings, pageMap) => {
|
||||
const getMarkdown = (headings, pageMap)=>{
|
||||
const levelPad = ['- ###', ' - ####', ' -', ' -', ' -', ' -'];
|
||||
|
||||
let allMarkdown = [];
|
||||
let depthChain = [0];
|
||||
|
||||
headings.forEach(heading => {
|
||||
const allMarkdown = [];
|
||||
const depthChain = [0];
|
||||
|
||||
headings.forEach((heading)=>{
|
||||
const page = parseInt(heading.closest('.page').id?.replace(/^p/, ''));
|
||||
const mappedPage = pageMap[page].mappedPage;
|
||||
const showPage = pageMap[page].showPage;
|
||||
@@ -42,14 +42,14 @@ const getMarkdown = (headings, pageMap) => {
|
||||
return;
|
||||
|
||||
//If different header depth than last, remove indents until nearest higher-level header, then indent once
|
||||
if (depth !== depthChain[depthChain.length -1]) {
|
||||
if(depth !== depthChain[depthChain.length -1]) {
|
||||
while (depth <= depthChain[depthChain.length - 1]) {
|
||||
depthChain.pop();
|
||||
}
|
||||
depthChain.push(depth);
|
||||
}
|
||||
|
||||
let markdown = `${levelPad[depthChain.length - 2]} [{{ ${title}}}{{ ${mappedPage}}}](#p${page})`;
|
||||
const markdown = `${levelPad[depthChain.length - 2]} [{{ ${title}}}{{ ${mappedPage}}}](#p${page})`;
|
||||
allMarkdown.push(markdown);
|
||||
});
|
||||
return allMarkdown.join('\n');
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
--HB_Color_CaptionText : #766649; // Brown
|
||||
--HB_Color_WatercolorStain : #BBAD82; // Light brown
|
||||
--HB_Color_Footnotes : #C9AD6A; // Gold
|
||||
--TOC : 'include';
|
||||
}
|
||||
|
||||
.useSansSerif() {
|
||||
@@ -797,7 +798,7 @@
|
||||
// *****************************/
|
||||
|
||||
// Default Exclusions
|
||||
// Anything not exlcuded is included, default Headers are H1, H2, and H3.
|
||||
// Anything not excluded is included, default Headers are H1, H2, and H3.
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
@@ -808,12 +809,23 @@ h6,
|
||||
.noToC,
|
||||
.toc { --TOC: exclude; }
|
||||
|
||||
.tocDepthH2 :is(h1, h2) {--TOC: include; }
|
||||
.tocDepthH3 :is(h1, h2, h3) {--TOC: include; }
|
||||
.tocDepthH4 :is(h1, h2, h3, h4) {--TOC: include; }
|
||||
.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC: include; }
|
||||
.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC: include; }
|
||||
|
||||
// Brew level default inclusion changes.
|
||||
// These add Headers 'back' to inclusion.
|
||||
.pages:has(.tocGlobalH4) {
|
||||
h4 {--TOC: include; }
|
||||
}
|
||||
|
||||
.pages:has(.tocGlobalH5) {
|
||||
h4, h5 {--TOC: include; }
|
||||
}
|
||||
|
||||
.pages:has(.tocGlobalH6) {
|
||||
h4, h5, h6 {--TOC: include; }
|
||||
}
|
||||
|
||||
// Block level inclusion changes
|
||||
// These include either a single (include) or a range (depth)
|
||||
.tocIncludeH1 h1 {--TOC: include; }
|
||||
.tocIncludeH2 h2 {--TOC: include; }
|
||||
.tocIncludeH3 h3 {--TOC: include; }
|
||||
@@ -821,6 +833,21 @@ h6,
|
||||
.tocIncludeH5 h5 {--TOC: include; }
|
||||
.tocIncludeH6 h6 {--TOC: include; }
|
||||
|
||||
.tocDepthH2 :is(h1, h2) {--TOC: include; }
|
||||
.tocDepthH3 :is(h1, h2, h3) {--TOC: include; }
|
||||
.tocDepthH4 :is(h1, h2, h3, h4) {--TOC: include; }
|
||||
.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC: include; }
|
||||
.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC: include; }
|
||||
|
||||
// Block level exclusion changes
|
||||
// These exclude a single block level
|
||||
.tocExcludeH1 h1 {--TOC: exclude; }
|
||||
.tocExcludeH2 h2 {--TOC: exclude; }
|
||||
.tocExcludeH3 h3 {--TOC: exclude; }
|
||||
.tocExcludeH4 h4 {--TOC: exclude; }
|
||||
.tocExcludeH5 h5 {--TOC: exclude; }
|
||||
.tocExcludeH6 h6 {--TOC: exclude; }
|
||||
|
||||
.page:has(.partCover) {
|
||||
--TOC: exclude;
|
||||
& h1 {
|
||||
|
||||
@@ -431,22 +431,40 @@ module.exports = [
|
||||
]
|
||||
},
|
||||
|
||||
/**************** PAGE *************/
|
||||
/**************** LAYOUT *************/
|
||||
|
||||
{
|
||||
groupName : 'Print',
|
||||
icon : 'fas fa-print',
|
||||
view : 'style',
|
||||
snippets : [
|
||||
{
|
||||
name : 'A3 Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : dedent`/* A3 Page Size */
|
||||
.page {
|
||||
width : 297mm;
|
||||
height : 420mm;
|
||||
}\n\n`,
|
||||
},
|
||||
{
|
||||
name : 'A4 Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : dedent`/* A4 Page Size */
|
||||
.page{
|
||||
.page {
|
||||
width : 210mm;
|
||||
height : 296.8mm;
|
||||
}\n\n`
|
||||
},
|
||||
{
|
||||
name : 'A5 Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : dedent`/* A5 Page Size */
|
||||
.page {
|
||||
width : 148mm;
|
||||
height : 210mm;
|
||||
}\n\n`,
|
||||
},
|
||||
{
|
||||
name : 'Square Page Size',
|
||||
icon : 'far fa-file',
|
||||
@@ -458,6 +476,17 @@ module.exports = [
|
||||
columns : unset;
|
||||
}\n\n`
|
||||
},
|
||||
{
|
||||
name : 'Card Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : dedent`/* Card Size */
|
||||
.page {
|
||||
width : 63.5mm;
|
||||
height : 88.9mm;
|
||||
padding : 5mm;
|
||||
columns : unset;
|
||||
}\n\n`
|
||||
},
|
||||
{
|
||||
name : 'Ink Friendly',
|
||||
icon : 'fas fa-tint',
|
||||
@@ -473,5 +502,5 @@ module.exports = [
|
||||
}\n\n`
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user