mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-05 21:02:43 +00:00
Merge branch 'master' of https://github.com/naturalcrit/homebrewery into scroll-to-element
This commit is contained in:
@@ -138,7 +138,7 @@ const BrewRenderer = (props)=>{
|
|||||||
const updateCurrentPage = useCallback(_.throttle((e)=>{
|
const updateCurrentPage = useCallback(_.throttle((e)=>{
|
||||||
const { scrollTop, clientHeight, scrollHeight } = e.target;
|
const { scrollTop, clientHeight, scrollHeight } = e.target;
|
||||||
const totalScrollableHeight = scrollHeight - clientHeight;
|
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);
|
props.onPageChange(currentPageNumber);
|
||||||
}, 200), []);
|
}, 200), []);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const NotificationPopup = ()=>{
|
|||||||
<ul>
|
<ul>
|
||||||
<li key='Vault'>
|
<li key='Vault'>
|
||||||
<em>Search brews with our new page!</em><br />
|
<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.
|
All PUBLISHED brews will be available to anyone searching there, by title or author, and filtering by renderer.
|
||||||
|
|
||||||
More features will be coming.
|
More features will be coming.
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
|
|||||||
|
|
||||||
} else if(mode == 'fit'){
|
} else if(mode == 'fit'){
|
||||||
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
// 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;
|
desiredZoom = minDimRatio * 100;
|
||||||
}
|
}
|
||||||
@@ -67,9 +67,9 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
|
|||||||
return deltaZoom;
|
return deltaZoom;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`}>
|
<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*/}
|
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
||||||
<div className='group'>
|
<div className='group'>
|
||||||
<button
|
<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');
|
require('./editor.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
const Markdown = require('../../../shared/naturalcrit/markdown.js');
|
const Markdown = require('../../../shared/naturalcrit/markdown.js');
|
||||||
|
|
||||||
@@ -22,6 +21,7 @@ const DEFAULT_STYLE_TEXT = dedent`
|
|||||||
color: black;
|
color: black;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
let isJumping = false;
|
||||||
|
|
||||||
const Editor = createClass({
|
const Editor = createClass({
|
||||||
displayName : 'Editor',
|
displayName : 'Editor',
|
||||||
@@ -36,16 +36,16 @@ const Editor = createClass({
|
|||||||
onStyleChange : ()=>{},
|
onStyleChange : ()=>{},
|
||||||
onMetaChange : ()=>{},
|
onMetaChange : ()=>{},
|
||||||
reportError : ()=>{},
|
reportError : ()=>{},
|
||||||
|
|
||||||
onCursorPageChange : ()=>{},
|
onCursorPageChange : ()=>{},
|
||||||
onViewPageChange : ()=>{},
|
onViewPageChange : ()=>{},
|
||||||
|
|
||||||
editorTheme : 'default',
|
editorTheme : 'default',
|
||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
|
|
||||||
currentEditorCursorPageNum : 0,
|
currentEditorCursorPageNum : 1,
|
||||||
currentEditorViewPageNum : 0,
|
currentEditorViewPageNum : 1,
|
||||||
currentBrewRendererPageNum : 0,
|
currentBrewRendererPageNum : 1,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
@@ -63,14 +63,15 @@ const Editor = createClass({
|
|||||||
isMeta : function() {return this.state.view == 'meta';},
|
isMeta : function() {return this.state.view == 'meta';},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
|
|
||||||
this.updateEditorSize();
|
this.updateEditorSize();
|
||||||
this.highlightCustomMarkdown();
|
this.highlightCustomMarkdown();
|
||||||
window.addEventListener('resize', this.updateEditorSize);
|
window.addEventListener('resize', this.updateEditorSize);
|
||||||
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
|
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
|
||||||
document.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('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('scroll', _.throttle(()=>{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) {
|
||||||
@@ -85,22 +86,32 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
||||||
|
|
||||||
this.highlightCustomMarkdown();
|
this.highlightCustomMarkdown();
|
||||||
if(prevProps.moveBrew !== this.props.moveBrew) {
|
if(prevProps.moveBrew !== this.props.moveBrew)
|
||||||
this.brewJump();
|
this.brewJump();
|
||||||
};
|
|
||||||
if(prevProps.moveSource !== this.props.moveSource) {
|
if(prevProps.moveSource !== this.props.moveSource)
|
||||||
this.sourceJump();
|
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){
|
handleControlKeys : function(e){
|
||||||
if(!(e.ctrlKey && e.metaKey)) return;
|
if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return;
|
||||||
const LEFTARROW_KEY = 37;
|
const LEFTARROW_KEY = 37;
|
||||||
const RIGHTARROW_KEY = 39;
|
const RIGHTARROW_KEY = 39;
|
||||||
if (e.shiftKey && (e.keyCode == RIGHTARROW_KEY)) this.brewJump();
|
if(e.keyCode == RIGHTARROW_KEY) this.brewJump();
|
||||||
if (e.shiftKey && (e.keyCode == LEFTARROW_KEY)) this.sourceJump();
|
if(e.keyCode == LEFTARROW_KEY) this.sourceJump();
|
||||||
if ((e.keyCode == LEFTARROW_KEY) || (e.keyCode == RIGHTARROW_KEY)) {
|
if(e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -117,14 +128,14 @@ const Editor = createClass({
|
|||||||
updateCurrentCursorPage : function(cursor) {
|
updateCurrentCursorPage : function(cursor) {
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1);
|
const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1);
|
||||||
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
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);
|
this.props.onCursorPageChange(currentPage);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCurrentViewPage : function(topScrollLine) {
|
updateCurrentViewPage : function(topScrollLine) {
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1);
|
const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1);
|
||||||
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
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);
|
this.props.onViewPageChange(currentPage);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -156,7 +167,7 @@ const Editor = createClass({
|
|||||||
// Record details of folded sections
|
// Record details of folded sections
|
||||||
if(mark.__isFold) {
|
if(mark.__isFold) {
|
||||||
const fold = mark.find();
|
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;
|
return !mark.__isFold;
|
||||||
}); //Don't undo code folding
|
}); //Don't undo code folding
|
||||||
@@ -174,7 +185,7 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// 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))
|
if(foldLines.some((fold)=>lineNumber >= fold.from && lineNumber <= fold.to))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Styling for \page breaks
|
// Styling for \page breaks
|
||||||
@@ -200,7 +211,7 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// definition lists
|
// definition lists
|
||||||
if(line.includes('::')){
|
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.
|
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // 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){
|
||||||
@@ -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[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' });
|
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];
|
||||||
let colons = /::/g;
|
const colons = /::/g;
|
||||||
let colonMatches = colons.exec(match[2]);
|
const colonMatches = colons.exec(match[2]);
|
||||||
if(colonMatches !== null){
|
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('^');
|
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;
|
||||||
|
|
||||||
while (startIndex >= 0) {
|
while (startIndex >= 0) {
|
||||||
superRegex.lastIndex = subRegex.lastIndex = startIndex;
|
superRegex.lastIndex = subRegex.lastIndex = startIndex;
|
||||||
let isSuper = false;
|
let isSuper = false;
|
||||||
let 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({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
|
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) {
|
while (startIndex >= 0) {
|
||||||
emojiRegex.lastIndex = startIndex;
|
emojiRegex.lastIndex = startIndex;
|
||||||
let 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)
|
if(!tokens.length)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let startPos = { line: lineNumber, ch: match.index };
|
const startPos = { line: lineNumber, ch: match.index };
|
||||||
let endPos = { line: lineNumber, ch: match.index + match[0].length };
|
const endPos = { line: lineNumber, ch: match.index + match[0].length };
|
||||||
|
|
||||||
// Iterate over conflicting marks and clear them
|
// Iterate over conflicting marks and clear them
|
||||||
var marks = codeMirror.findMarks(startPos, endPos);
|
const marks = codeMirror.findMarks(startPos, endPos);
|
||||||
marks.forEach(function(marker) {
|
marks.forEach(function(marker) {
|
||||||
if(!marker.__isFold) marker.clear();
|
if(!marker.__isFold) marker.clear();
|
||||||
});
|
});
|
||||||
@@ -302,64 +313,93 @@ const Editor = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
brewJump : function(targetPage=this.props.currentEditorCursorPageNum){
|
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
|
||||||
if(!window) return;
|
if(!window || 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.getElementById(`p${targetPage}`).getBoundingClientRect().top;
|
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
|
||||||
const interimPos = targetPos >= 0 ? -30 : 30;
|
|
||||||
|
|
||||||
const bounceDelay = 100;
|
const checkIfScrollComplete = ()=>{
|
||||||
const scrollDelay = 500;
|
let scrollingTimeout;
|
||||||
|
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
|
||||||
if(!this.throttleBrewMove) {
|
scrollingTimeout = setTimeout(()=>{
|
||||||
this.throttleBrewMove = _.throttle((currentPos, interimPos, targetPos)=>{
|
isJumping = false;
|
||||||
brewRenderer.scrollTo({ top: currentPos + interimPos, behavior: 'smooth' });
|
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
|
||||||
setTimeout(()=>{
|
}, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done
|
||||||
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
|
|
||||||
}, bounceDelay);
|
|
||||||
}, scrollDelay, { leading: true, trailing: false });
|
|
||||||
};
|
};
|
||||||
this.throttleBrewMove(currentPos, interimPos, targetPos);
|
|
||||||
|
|
||||||
// const hashPage = (page != 1) ? `p${page}` : '';
|
isJumping = true;
|
||||||
// window.location.hash = hashPage;
|
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){
|
sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
|
||||||
if(this.isText()) {
|
if(!this.isText || isJumping)
|
||||||
if(targetLine == null) {
|
return;
|
||||||
targetLine = 0;
|
|
||||||
|
|
||||||
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
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 textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
|
||||||
const textPosition = textString.length;
|
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
|
||||||
const lineCount = textString.match('\n') ? textString.slice(0, textPosition).split('\n').length : 0;
|
|
||||||
|
|
||||||
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
|
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
|
||||||
|
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||||
|
|
||||||
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
|
const checkIfScrollComplete = ()=>{
|
||||||
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
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.
|
isJumping = true;
|
||||||
const incrementalScroll = setInterval(()=>{
|
checkIfScrollComplete();
|
||||||
currentY += (targetY - currentY) / 10;
|
this.codeEditor.current.codeMirror.on('scroll', checkIfScrollComplete);
|
||||||
this.codeEditor.current.codeMirror.scrollTo(null, currentY);
|
|
||||||
|
|
||||||
// Update target: target height is not accurate until within +-10 lines of the visible window
|
if(smooth) {
|
||||||
if(Math.abs(targetY - currentY > 100))
|
//Scroll 1/10 of the way every 10ms until 1px off.
|
||||||
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
const incrementalScroll = setInterval(()=>{
|
||||||
|
currentY += (targetY - currentY) / 10;
|
||||||
|
this.codeEditor.current.codeMirror.scrollTo(null, currentY);
|
||||||
|
|
||||||
// End when close enough
|
// Update target: target height is not accurate until within +-10 lines of the visible window
|
||||||
if(Math.abs(targetY - currentY) < 1) {
|
if(Math.abs(targetY - currentY > 100))
|
||||||
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||||
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
|
|
||||||
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
|
// End when close enough
|
||||||
clearInterval(incrementalScroll);
|
if(Math.abs(targetY - currentY) < 1) {
|
||||||
}
|
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
||||||
}, 10);
|
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}
|
view={this.state.view}
|
||||||
value={this.props.brew.text}
|
value={this.props.brew.text}
|
||||||
onChange={this.props.onTextChange}
|
onChange={this.props.onTextChange}
|
||||||
onCursorActivity={this.props.onCursorActivity}
|
|
||||||
onScroll={this.props.onPageChange}
|
|
||||||
editorTheme={this.state.editorTheme}
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
</>;
|
</>;
|
||||||
@@ -462,7 +500,9 @@ const Editor = createClass({
|
|||||||
currentEditorTheme={this.state.editorTheme}
|
currentEditorTheme={this.state.editorTheme}
|
||||||
updateEditorTheme={this.updateEditorTheme}
|
updateEditorTheme={this.updateEditorTheme}
|
||||||
snippetBundle={this.props.snippetBundle}
|
snippetBundle={this.props.snippetBundle}
|
||||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
|
cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
|
||||||
|
updateBrew={this.props.updateBrew}
|
||||||
|
/>
|
||||||
|
|
||||||
{this.renderEditor()}
|
{this.renderEditor()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
import { getHistoryItems, historyExists } from '../../utils/versionHistory.js';
|
||||||
|
|
||||||
//Import all themes
|
//Import all themes
|
||||||
const ThemeSnippets = {};
|
const ThemeSnippets = {};
|
||||||
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
||||||
@@ -38,7 +40,8 @@ const Snippetbar = createClass({
|
|||||||
unfoldCode : ()=>{},
|
unfoldCode : ()=>{},
|
||||||
updateEditorTheme : ()=>{},
|
updateEditorTheme : ()=>{},
|
||||||
cursorPos : {},
|
cursorPos : {},
|
||||||
snippetBundle : []
|
snippetBundle : [],
|
||||||
|
updateBrew : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -46,7 +49,8 @@ const Snippetbar = createClass({
|
|||||||
return {
|
return {
|
||||||
renderer : this.props.renderer,
|
renderer : this.props.renderer,
|
||||||
themeSelector : false,
|
themeSelector : false,
|
||||||
snippets : []
|
snippets : [],
|
||||||
|
historyExists : false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -59,18 +63,20 @@ const Snippetbar = createClass({
|
|||||||
|
|
||||||
componentDidUpdate : async function(prevProps) {
|
componentDidUpdate : async function(prevProps) {
|
||||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
||||||
const snippets = this.compileSnippets();
|
|
||||||
this.setState({
|
this.setState({
|
||||||
snippets : snippets
|
snippets : this.compileSnippets()
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
},
|
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
historyExists : historyExists(this.props.brew)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
mergeCustomizer : function(oldValue, newValue, key) {
|
mergeCustomizer : function(oldValue, newValue, key) {
|
||||||
if(key == 'snippets') {
|
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
|
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(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
@@ -158,6 +194,10 @@ const Snippetbar = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div className='editors'>
|
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' : ''}`}
|
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||||
onClick={this.props.undo} >
|
onClick={this.props.undo} >
|
||||||
<i className='fas fa-undo' />
|
<i className='fas fa-undo' />
|
||||||
|
|||||||
@@ -53,6 +53,21 @@
|
|||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
color : inherit;
|
color : inherit;
|
||||||
}
|
}
|
||||||
|
&.history {
|
||||||
|
.tooltipLeft('History');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : grey;
|
||||||
|
position : relative;
|
||||||
|
&.active {
|
||||||
|
color : inherit;
|
||||||
|
}
|
||||||
|
&>.dropdown{
|
||||||
|
right : -1px;
|
||||||
|
&>.snippet{
|
||||||
|
padding-right : 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
&.editorTheme {
|
&.editorTheme {
|
||||||
.tooltipLeft('Editor Themes');
|
.tooltipLeft('Editor Themes');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ const ErrorNavItem = createClass({
|
|||||||
Looks like there was a problem retreiving
|
Looks like there was a problem retreiving
|
||||||
the theme, or a theme that it inherits,
|
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}`}>
|
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>
|
</div>
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const Markdown = require('naturalcrit/markdown.js');
|
|||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
|
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
||||||
|
|
||||||
const googleDriveIcon = require('../../googleDrive.svg');
|
const googleDriveIcon = require('../../googleDrive.svg');
|
||||||
|
|
||||||
const SAVE_TIMEOUT = 3000;
|
const SAVE_TIMEOUT = 3000;
|
||||||
@@ -55,9 +57,9 @@ const EditPage = createClass({
|
|||||||
autoSave : true,
|
autoSave : true,
|
||||||
autoSaveWarning : false,
|
autoSaveWarning : false,
|
||||||
unsavedTime : new Date(),
|
unsavedTime : new Date(),
|
||||||
currentEditorViewPageNum : 0,
|
currentEditorViewPageNum : 1,
|
||||||
currentEditorCursorPageNum : 0,
|
currentEditorCursorPageNum : 1,
|
||||||
currentBrewRendererPageNum : 0,
|
currentBrewRendererPageNum : 1,
|
||||||
displayLockMessage : this.props.brew.lock || false,
|
displayLockMessage : this.props.brew.lock || false,
|
||||||
themeBundle : {}
|
themeBundle : {}
|
||||||
};
|
};
|
||||||
@@ -117,23 +119,19 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleEditorViewPageChange : function(pageNumber){
|
handleEditorViewPageChange : function(pageNumber){
|
||||||
console.log(`editor view : ${pageNumber}`);
|
|
||||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleEditorCursorPageChange : function(pageNumber){
|
handleEditorCursorPageChange : function(pageNumber){
|
||||||
console.log(`editor cursor : ${pageNumber}`);
|
|
||||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
handleBrewRendererPageChange : function(pageNumber){
|
||||||
console.log(`brewRenderer view : ${pageNumber}`);
|
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
//If there are errors, run the validator on every change to give quick feedback
|
//If there are errors, run the validator on every change to give quick feedback
|
||||||
console.log('text change');
|
|
||||||
let htmlErrors = this.state.htmlErrors;
|
let htmlErrors = this.state.htmlErrors;
|
||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||||
|
|
||||||
@@ -168,6 +166,16 @@ const EditPage = createClass({
|
|||||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
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){
|
trySave : function(immediate=false){
|
||||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||||
if(this.hasChanges()){
|
if(this.hasChanges()){
|
||||||
@@ -220,6 +228,9 @@ const EditPage = createClass({
|
|||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
updateHistory(this.state.brew);
|
||||||
|
versionHistoryGarbageCollection();
|
||||||
|
|
||||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||||
|
|
||||||
const brew = this.state.brew;
|
const brew = this.state.brew;
|
||||||
@@ -431,6 +442,7 @@ const EditPage = createClass({
|
|||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
userThemes={this.props.userThemes}
|
userThemes={this.props.userThemes}
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
snippetBundle={this.state.themeBundle.snippets}
|
||||||
|
updateBrew={this.updateBrew}
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||||
onViewPageChange={this.handleEditorViewPageChange}
|
onViewPageChange={this.handleEditorViewPageChange}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ const HomePage = createClass({
|
|||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
welcomeText : this.props.brew.text,
|
welcomeText : this.props.brew.text,
|
||||||
error : undefined,
|
error : undefined,
|
||||||
currentEditorViewPageNum : 0,
|
currentEditorViewPageNum : 1,
|
||||||
currentEditorCursorPageNum : 0,
|
currentEditorCursorPageNum : 1,
|
||||||
currentBrewRendererPageNum : 0,
|
currentBrewRendererPageNum : 1,
|
||||||
themeBundle : {}
|
themeBundle : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -64,17 +64,14 @@ const HomePage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleEditorViewPageChange : function(pageNumber){
|
handleEditorViewPageChange : function(pageNumber){
|
||||||
console.log(`editor view : ${pageNumber}`);
|
|
||||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleEditorCursorPageChange : function(pageNumber){
|
handleEditorCursorPageChange : function(pageNumber){
|
||||||
console.log(`editor cursor : ${pageNumber}`);
|
|
||||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
handleBrewRendererPageChange : function(pageNumber){
|
||||||
console.log(`brewRenderer view : ${pageNumber}`);
|
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ const NewPage = createClass({
|
|||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
error : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(brew.text),
|
htmlErrors : Markdown.validate(brew.text),
|
||||||
currentEditorViewPageNum : 0,
|
currentEditorViewPageNum : 1,
|
||||||
currentEditorCursorPageNum : 0,
|
currentEditorCursorPageNum : 1,
|
||||||
currentBrewRendererPageNum : 0,
|
currentBrewRendererPageNum : 1,
|
||||||
themeBundle : {}
|
themeBundle : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -111,17 +111,14 @@ const NewPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleEditorViewPageChange : function(pageNumber){
|
handleEditorViewPageChange : function(pageNumber){
|
||||||
console.log(`editor view : ${pageNumber}`);
|
|
||||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleEditorCursorPageChange : function(pageNumber){
|
handleEditorCursorPageChange : function(pageNumber){
|
||||||
console.log(`editor cursor : ${pageNumber}`);
|
|
||||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
handleBrewRendererPageChange : function(pageNumber){
|
||||||
console.log(`brewRenderer view : ${pageNumber}`);
|
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ const VaultPage = (props)=>{
|
|||||||
|
|
||||||
if(error) {
|
if(error) {
|
||||||
const errorText = ErrorIndex()[error.HBErrorCode.toString()] || '';
|
const errorText = ErrorIndex()[error.HBErrorCode.toString()] || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='foundBrews noBrews'>
|
<div className='foundBrews noBrews'>
|
||||||
<h3>Error: {errorText}</h3>
|
<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",
|
"expr-eval": "^2.0.2",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.7",
|
"express-static-gzip": "2.1.8",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.2.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
@@ -6362,12 +6362,11 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/express-static-gzip": {
|
"node_modules/express-static-gzip": {
|
||||||
"version": "2.1.7",
|
"version": "2.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.8.tgz",
|
||||||
"integrity": "sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw==",
|
"integrity": "sha512-g8tiJuI9Y9Ffy59ehVXvqb0hhP83JwZiLxzanobPaMbkB5qBWA8nuVgd+rcd5qzH3GkgogTALlc0BaADYwnMbQ==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"serve-static": "^1.14.1"
|
"serve-static": "^1.16.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express/node_modules/cookie": {
|
"node_modules/express/node_modules/cookie": {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
"expr-eval": "^2.0.2",
|
"expr-eval": "^2.0.2",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.7",
|
"express-static-gzip": "2.1.8",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.2.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
|
|||||||
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).toEqual(testBrew);
|
||||||
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
|
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
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({
|
expect(res.set).toHaveBeenCalledWith({
|
||||||
'Cache-Control' : 'no-cache',
|
'Cache-Control' : 'no-cache',
|
||||||
'Content-Type' : 'text/css'
|
'Content-Type' : 'text/css'
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ const CodeEditor = createClass({
|
|||||||
displayName : 'CodeEditor',
|
displayName : 'CodeEditor',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
language : '',
|
language : '',
|
||||||
value : '',
|
value : '',
|
||||||
wrap : true,
|
wrap : true,
|
||||||
onChange : ()=>{},
|
onChange : ()=>{},
|
||||||
enableFolding : true,
|
enableFolding : true,
|
||||||
editorTheme : 'default'
|
editorTheme : 'default'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ const CodeEditor = createClass({
|
|||||||
autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror);
|
autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror);
|
||||||
|
|
||||||
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
|
// 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();
|
this.updateSize();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -399,7 +399,7 @@ const CodeEditor = createClass({
|
|||||||
},
|
},
|
||||||
getTopVisibleLine : function(){
|
getTopVisibleLine : function(){
|
||||||
const rect = this.codeMirror.getWrapperElement().getBoundingClientRect();
|
const rect = this.codeMirror.getWrapperElement().getBoundingClientRect();
|
||||||
const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, "window");
|
const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window');
|
||||||
return topVisibleLine;
|
return topVisibleLine;
|
||||||
},
|
},
|
||||||
updateSize : function(){
|
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
|
// Expose `src` attribute as `--HB_src` to make the URL accessible via CSS
|
||||||
renderer.image = function (href, title, text) {
|
renderer.image = function (href, title, text) {
|
||||||
href = cleanUrl(href);
|
href = cleanUrl(href);
|
||||||
if (href === null)
|
if(href === null)
|
||||||
return text;
|
return text;
|
||||||
|
|
||||||
let out = `<img src="${href}" alt="${text}" style="--HB_src:url(${href});"`;
|
let out = `<img src="${href}" alt="${text}" style="--HB_src:url(${href});"`;
|
||||||
if (title)
|
if(title)
|
||||||
out += ` title="${title}"`;
|
out += ` title="${title}"`;
|
||||||
|
|
||||||
out += '>';
|
out += '>';
|
||||||
return out;
|
return out;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Disable default reflink behavior, as it steps on our variables extension
|
// Disable default reflink behavior, as it steps on our variables extension
|
||||||
tokenizer.def = function () {
|
tokenizer.def = function () {
|
||||||
@@ -745,7 +745,7 @@ const tableTerminators = [
|
|||||||
`:+\\n`, // hardBreak
|
`:+\\n`, // hardBreak
|
||||||
` *{[^\n]+}`, // blockInjector
|
` *{[^\n]+}`, // blockInjector
|
||||||
` *{{[^{\n]*\n.*?\n}}` // mustacheDiv
|
` *{{[^{\n]*\n.*?\n}}` // mustacheDiv
|
||||||
]
|
];
|
||||||
|
|
||||||
Marked.use(MarkedVariables());
|
Marked.use(MarkedVariables());
|
||||||
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts,
|
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));
|
Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
||||||
|
|
||||||
function cleanUrl(href) {
|
function cleanUrl(href) {
|
||||||
try {
|
try {
|
||||||
href = encodeURI(href).replace(/%25/g, '%');
|
href = encodeURI(href).replace(/%25/g, '%');
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return href;
|
return href;
|
||||||
}
|
}
|
||||||
|
|
||||||
const escapeTest = /[&<>"']/;
|
const escapeTest = /[&<>"']/;
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ const SplitPane = createClass({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', this.handleWindowResize);
|
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() {
|
componentWillUnmount : function() {
|
||||||
@@ -89,6 +93,11 @@ const SplitPane = createClass({
|
|||||||
userSetDividerPos : newSize
|
userSetDividerPos : newSize
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
liveScrollToggle : function() {
|
||||||
|
window.localStorage.setItem('liveScroll', String(!this.state.liveScroll));
|
||||||
|
this.setState({ liveScroll: !this.state.liveScroll });
|
||||||
|
},
|
||||||
/*
|
/*
|
||||||
unFocus : function() {
|
unFocus : function() {
|
||||||
if(document.selection){
|
if(document.selection){
|
||||||
@@ -120,6 +129,11 @@ const SplitPane = createClass({
|
|||||||
onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} >
|
onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} >
|
||||||
<i className='fas fa-arrow-right' />
|
<i className='fas fa-arrow-right' />
|
||||||
</div>
|
</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], {
|
{React.cloneElement(this.props.children[0], {
|
||||||
...(this.props.showDividerButtons && {
|
...(this.props.showDividerButtons && {
|
||||||
moveBrew: this.state.moveBrew,
|
moveBrew : this.state.moveBrew,
|
||||||
moveSource: this.state.moveSource,
|
moveSource : this.state.moveSource,
|
||||||
setMoveArrows: this.setMoveArrows,
|
liveScroll : this.state.liveScroll,
|
||||||
|
setMoveArrows : this.setMoveArrows,
|
||||||
}),
|
}),
|
||||||
})}
|
})}
|
||||||
</Pane>
|
</Pane>
|
||||||
|
|||||||
@@ -53,6 +53,15 @@
|
|||||||
.tooltipRight('Jump to location in Preview');
|
.tooltipRight('Jump to location in Preview');
|
||||||
top : 60px;
|
top : 60px;
|
||||||
}
|
}
|
||||||
|
&.lock{
|
||||||
|
.tooltipRight('De-sync Editor and Preview locations.');
|
||||||
|
top : 90px;
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
&.unlock{
|
||||||
|
.tooltipRight('Sync Editor and Preview locations');
|
||||||
|
top : 90px;
|
||||||
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color: #666;
|
background-color: #666;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,35 +27,154 @@ module.exports = [
|
|||||||
experimental : true,
|
experimental : true,
|
||||||
subsnippets : [
|
subsnippets : [
|
||||||
{
|
{
|
||||||
name : 'Table of Contents',
|
name : 'Generate Table of Contents',
|
||||||
icon : 'fas fa-book',
|
icon : 'fas fa-book',
|
||||||
gen : TableOfContentsGen,
|
gen : TableOfContentsGen,
|
||||||
experimental : true
|
experimental : true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Include in ToC up to H3',
|
name : 'Table of Contents Individual Inclusion',
|
||||||
icon : 'fas fa-dice-three',
|
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
|
gen : dedent `\n{{tocDepthH3
|
||||||
}}\n`,
|
}}\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',
|
name : 'Table of Contents Individual Exclusion',
|
||||||
icon : 'fas fa-dice-four',
|
icon : 'fas fa-book',
|
||||||
gen : dedent `\n{{tocDepthH4
|
gen : dedent `\n{{tocExcludeH1 \n
|
||||||
}}\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',
|
name : 'Table of Contents Toggles',
|
||||||
icon : 'fas fa-dice-five',
|
icon : 'fas fa-book',
|
||||||
gen : dedent `\n{{tocDepthH5
|
gen : `{{tocGlobalH4}}\n\n`,
|
||||||
}}\n`,
|
subsnippets : [
|
||||||
},
|
{
|
||||||
{
|
name : 'Enable H1-H4 all pages',
|
||||||
name : 'Include in ToC up to H6',
|
icon : 'fas fa-dice-four',
|
||||||
icon : 'fas fa-dice-six',
|
gen : `{{tocGlobalH4}}\n\n`,
|
||||||
gen : dedent `\n{{tocDepthH6
|
},
|
||||||
}}\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);
|
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
}\n\n`
|
}\n\n`
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ const dedent = require('dedent-tabs').default;
|
|||||||
const mapPages = (pages)=>{
|
const mapPages = (pages)=>{
|
||||||
let actualPage = 0;
|
let actualPage = 0;
|
||||||
let mappedPage = 0; // Number displayed in footer
|
let mappedPage = 0; // Number displayed in footer
|
||||||
let pageMap = [];
|
const pageMap = [];
|
||||||
|
|
||||||
pages.forEach(page => {
|
pages.forEach((page)=>{
|
||||||
actualPage++;
|
actualPage++;
|
||||||
const doSkip = page.querySelector('.skipCounting');
|
const doSkip = page.querySelector('.skipCounting');
|
||||||
const doReset = page.querySelector('.resetCounting');
|
const doReset = page.querySelector('.resetCounting');
|
||||||
@@ -24,13 +24,13 @@ const mapPages = (pages)=>{
|
|||||||
return pageMap;
|
return pageMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMarkdown = (headings, pageMap) => {
|
const getMarkdown = (headings, pageMap)=>{
|
||||||
const levelPad = ['- ###', ' - ####', ' -', ' -', ' -', ' -'];
|
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 page = parseInt(heading.closest('.page').id?.replace(/^p/, ''));
|
||||||
const mappedPage = pageMap[page].mappedPage;
|
const mappedPage = pageMap[page].mappedPage;
|
||||||
const showPage = pageMap[page].showPage;
|
const showPage = pageMap[page].showPage;
|
||||||
@@ -42,14 +42,14 @@ const getMarkdown = (headings, pageMap) => {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
//If different header depth than last, remove indents until nearest higher-level header, then indent once
|
//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]) {
|
while (depth <= depthChain[depthChain.length - 1]) {
|
||||||
depthChain.pop();
|
depthChain.pop();
|
||||||
}
|
}
|
||||||
depthChain.push(depth);
|
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);
|
allMarkdown.push(markdown);
|
||||||
});
|
});
|
||||||
return allMarkdown.join('\n');
|
return allMarkdown.join('\n');
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
--HB_Color_CaptionText : #766649; // Brown
|
--HB_Color_CaptionText : #766649; // Brown
|
||||||
--HB_Color_WatercolorStain : #BBAD82; // Light brown
|
--HB_Color_WatercolorStain : #BBAD82; // Light brown
|
||||||
--HB_Color_Footnotes : #C9AD6A; // Gold
|
--HB_Color_Footnotes : #C9AD6A; // Gold
|
||||||
|
--TOC : 'include';
|
||||||
}
|
}
|
||||||
|
|
||||||
.useSansSerif() {
|
.useSansSerif() {
|
||||||
@@ -797,7 +798,7 @@
|
|||||||
// *****************************/
|
// *****************************/
|
||||||
|
|
||||||
// Default Exclusions
|
// 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,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6,
|
h6,
|
||||||
@@ -808,12 +809,23 @@ h6,
|
|||||||
.noToC,
|
.noToC,
|
||||||
.toc { --TOC: exclude; }
|
.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; }
|
.tocIncludeH1 h1 {--TOC: include; }
|
||||||
.tocIncludeH2 h2 {--TOC: include; }
|
.tocIncludeH2 h2 {--TOC: include; }
|
||||||
.tocIncludeH3 h3 {--TOC: include; }
|
.tocIncludeH3 h3 {--TOC: include; }
|
||||||
@@ -821,6 +833,21 @@ h6,
|
|||||||
.tocIncludeH5 h5 {--TOC: include; }
|
.tocIncludeH5 h5 {--TOC: include; }
|
||||||
.tocIncludeH6 h6 {--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) {
|
.page:has(.partCover) {
|
||||||
--TOC: exclude;
|
--TOC: exclude;
|
||||||
& h1 {
|
& h1 {
|
||||||
|
|||||||
@@ -426,22 +426,40 @@ module.exports = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
/**************** PAGE *************/
|
/**************** LAYOUT *************/
|
||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'Print',
|
groupName : 'Print',
|
||||||
icon : 'fas fa-print',
|
icon : 'fas fa-print',
|
||||||
view : 'style',
|
view : 'style',
|
||||||
snippets : [
|
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',
|
name : 'A4 Page Size',
|
||||||
icon : 'far fa-file',
|
icon : 'far fa-file',
|
||||||
gen : dedent`/* A4 Page Size */
|
gen : dedent`/* A4 Page Size */
|
||||||
.page{
|
.page {
|
||||||
width : 210mm;
|
width : 210mm;
|
||||||
height : 296.8mm;
|
height : 296.8mm;
|
||||||
}\n\n`
|
}\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',
|
name : 'Square Page Size',
|
||||||
icon : 'far fa-file',
|
icon : 'far fa-file',
|
||||||
@@ -453,6 +471,17 @@ module.exports = [
|
|||||||
columns : unset;
|
columns : unset;
|
||||||
}\n\n`
|
}\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',
|
name : 'Ink Friendly',
|
||||||
icon : 'fas fa-tint',
|
icon : 'fas fa-tint',
|
||||||
@@ -468,5 +497,5 @@ module.exports = [
|
|||||||
}\n\n`
|
}\n\n`
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user