0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-06-24 13:18:39 +00:00

Merge branch 'master' into V4_Persistant_Templates

Change enable_v4 to enableV4 to shut up linter.
This commit is contained in:
David Bolack
2026-05-29 21:06:55 -05:00
133 changed files with 9228 additions and 10052 deletions
+25 -11
View File
@@ -1,10 +1,12 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
import brewRendererStylesUrl from './brewRenderer.less?url';
import headerNavStylesUrl from './headerNav/headerNav.less?url';
import './brewRenderer.less';
import React, { useState, useRef, useMemo, useEffect } from 'react';
import _ from 'lodash';
import MarkdownLegacy from '../../../shared/markdownLegacy.js';
import Markdown from '../../../shared/markdown.js';
import MarkdownLegacy from '@shared/markdownLegacy.js';
import Markdown from '@shared/markdown.js';
import ErrorBar from './errorBar/errorBar.jsx';
import ToolBar from './toolBar/toolBar.jsx';
@@ -13,10 +15,10 @@ import RenderWarnings from '../../components/renderWarnings/renderWarnings.jsx';
import NotificationPopup from './notificationPopup/notificationPopup.jsx';
import Frame from 'react-frame-component';
import dedent from 'dedent';
import { printCurrentBrew } from '../../../shared/helpers.js';
import { printCurrentBrew } from '@shared/helpers.js';
import HeaderNav from './headerNav/headerNav.jsx';
import { safeHTML } from './safeHTML.js';
import safeHTML from './safeHTML.js';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
@@ -27,9 +29,10 @@ const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head>
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
<base target=_blank>
<link href="${brewRendererStylesUrl}" rel="stylesheet" />
<link href="${headerNavStylesUrl}" rel="stylesheet" />
<base target="_top">
</head><body style='overflow: hidden'><div></div></body></html>`;
@@ -38,6 +41,7 @@ const BrewPage = (props)=>{
props = {
contents : '',
index : 0,
hoisted : false,
...props
};
const pageRef = useRef(null);
@@ -132,6 +136,7 @@ const BrewRenderer = (props)=>{
const mainRef = useRef(null);
const pagesRef = useRef(null);
const urlRef = useRef('');
if(props.renderer == 'legacy') {
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
@@ -204,13 +209,13 @@ const BrewRenderer = (props)=>{
styles = _.mapKeys(styles, (v, k)=>k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React
classes = [classes, injectedTags.classes].join(' ').trim();
attributes = injectedTags.attributes;
if(global.enable_v4) {
if(global.enablev4) {
if (attributes && Object.hasOwn(attributes, 'hbtemplate')) {
pageTemplates[index] = attributes['hbtemplate'];
}
}
}
if(global.enable_v4) {
if(global.enablev4) {
// If we don't have a template for this page, look backwards until one is found or the first page.
if(!pageTemplates[index]) {
for (let i=index;i>=0; i--) {
@@ -231,7 +236,8 @@ const BrewRenderer = (props)=>{
}
};
const renderPages = ()=>{
const renderPages = (checkHoists = false)=>{
if(props.errors && props.errors.length)
return renderedPages;
@@ -245,10 +251,16 @@ const BrewRenderer = (props)=>{
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
_.forEach(rawPages, (page, index)=>{
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
const varsOnPageRegex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g; // Find out if there are any vars on the page.
const forceRender = checkHoists &&
!props.hoisted &&
(page.match(varsOnPageRegex)); // forceRender forces pages outside of the PPR range to render if true.
// This is necessary on the first load to fully populate the variable table.
if((isInView(index) || !renderedPages[index] || forceRender) && typeof window !== 'undefined'){
renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
}
});
if(!props.hoisted) { props.hoisted = true; } // Only fully hoist once.
return renderedPages;
};
@@ -285,8 +297,10 @@ const BrewRenderer = (props)=>{
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
scrollToHash(window.location.hash);
window.addEventListener('hashchange', ()=>scrollToHash(window.location.hash));
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
renderPages(); //Make sure page is renderable before showing
renderPages(true); //Make sure page is renderable before showing
setState((prevState)=>({
...prevState,
isMounted : true,
@@ -1,4 +1,4 @@
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
@import '@sharedStyles/core.less';
.brewRenderer {
height : 100vh;
@@ -1,3 +1,4 @@
@import '@sharedStyles/colors.less';
.errorBar {
position : absolute;
@@ -104,7 +104,7 @@ const HeaderNavItem = ({ link, text, depth, className })=>{
if(!link || !text) return;
return <li>
<a href={`#${link}`} target='_self' className={`depth-${depth} ${className ?? ''}`}>
<a href={`#${link}`} className={`depth-${depth} ${className ?? ''}`}>
{trimString(text, depth)}
</a>
</li>;
@@ -1,7 +1,7 @@
import './notificationPopup.less';
import React, { useEffect, useState } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'markdown.js';
import Markdown from '@shared/markdown.js';
import Dialog from '../../../components/dialog.jsx';
@@ -1,3 +1,5 @@
@import './client/homebrew/navbar/navbar.less';
.popups {
position : fixed;
top : calc(@navbarHeight + @viewerToolsHeight);
+1 -1
View File
@@ -43,4 +43,4 @@ function safeHTML(htmlString) {
return div.innerHTML;
};
module.exports.safeHTML = safeHTML;
export default safeHTML;
+55 -255
View File
@@ -4,7 +4,6 @@ import React from 'react';
import createReactClass from 'create-react-class';
import _ from 'lodash';
import dedent from 'dedent';
import Markdown from '../../../shared/markdown.js';
import CodeEditor from '../../components/codeEditor/codeEditor.jsx';
import SnippetBar from './snippetbar/snippetbar.jsx';
@@ -12,8 +11,22 @@ import MetadataEditor from './metadataEditor/metadataEditor.jsx';
const EDITOR_THEME_KEY = 'HB_editor_theme';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
import defaultCM5Theme from '@themes/codeMirror/default.js';
import darkbrewery from '@themes/codeMirror/darkbrewery.js';
import cm5Themes from 'codemirror-5-themes';
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
const EditorThemes = Object.entries(themes)
.filter(([name, value])=>Array.isArray(value) &&
!name.endsWith('Init') &&
!name.endsWith('Style')
)
.map(([name])=>name);
//const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
//const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
const DEFAULT_STYLE_TEXT = dedent`
/*=======--- Example CSS styling ---=======*/
/* Any CSS here will apply to your document! */
@@ -30,6 +43,7 @@ const DEFAULT_SNIPPET_TEXT = dedent`
This snippet is accessible in the brew tab, and will be inherited if the brew is used as a theme.
`;
let isJumping = false;
let jumpSource = null;
const Editor = createReactClass({
displayName : 'Editor',
@@ -72,23 +86,20 @@ const Editor = createReactClass({
componentDidMount : function() {
this.highlightCustomMarkdown();
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
const brewRenderer = document.getElementById('BrewRenderer');
brewRenderer.onload = ()=>brewRenderer.contentDocument?.addEventListener('keydown', this.handleControlKeys);
document.addEventListener('keydown', this.handleControlKeys);
this.codeEditor.current.codeMirror?.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());});
this.codeEditor.current.codeMirror?.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200));
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if(editorTheme) {
this.setState({
editorTheme : editorTheme
});
if(editorTheme && EditorThemes.includes(editorTheme)) {
this.setState({ editorTheme });
} else {
this.setState({ editorTheme: 'default' });
}
const snippetBar = document.querySelector('.editor > .snippetBar');
if(!snippetBar) return;
this.resizeObserver = new ResizeObserver(entries=>{
this.resizeObserver = new ResizeObserver((entries)=>{
const height = document.querySelector('.editor > .snippetBar').offsetHeight;
this.setState({ snippetBarHeight: height });
});
@@ -98,7 +109,6 @@ const Editor = createReactClass({
componentDidUpdate : function(prevProps, prevState, snapshot) {
this.highlightCustomMarkdown();
if(prevProps.moveBrew !== this.props.moveBrew)
this.brewJump();
@@ -132,22 +142,16 @@ const Editor = createReactClass({
}
},
updateCurrentCursorPage : function(cursor) {
const lines = this.props.brew.text.split('\n').slice(1, cursor.line + 1);
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
this.props.onCursorPageChange(currentPage);
updateCurrentCursorPage : function(pageNumber) {
this.props.onCursorPageChange(pageNumber);
},
updateCurrentViewPage : function(topScrollLine) {
const lines = this.props.brew.text.split('\n').slice(1, topScrollLine + 1);
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
this.props.onViewPageChange(currentPage);
updateCurrentViewPage : function(pageNumber) {
this.props.onViewPageChange(pageNumber);
},
handleInject : function(injectText){
this.codeEditor.current?.injectText(injectText, false);
this.codeEditor.current?.injectText(injectText);
},
handleViewChange : function(newView){
@@ -156,181 +160,12 @@ const Editor = createReactClass({
this.setState({
view : newView
}, ()=>{
this.codeEditor.current?.codeMirror?.focus();
this.codeEditor.current?.focus();
});
},
highlightCustomMarkdown : function(){
if(!this.codeEditor.current?.codeMirror) return;
if((this.state.view === 'text') ||(this.state.view === 'snippet')) {
const codeMirror = this.codeEditor.current.codeMirror;
codeMirror?.operation(()=>{ // Batch CodeMirror styling
const foldLines = [];
//reset custom text styles
const customHighlights = codeMirror?.getAllMarks().filter((mark)=>{
// Record details of folded sections
if(mark.__isFold) {
const fold = mark.find();
foldLines.push({ from: fold.from?.line, to: fold.to?.line });
}
return !mark.__isFold;
}); //Don't undo code folding
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
let userSnippetCount = 1; // start snippet count from snippet 1
let editorPageCount = 1; // start page count from page 1
const whichSource = this.state.view === 'text' ? this.props.brew.text : this.props.brew.snippets;
_.forEach(whichSource?.split('\n'), (line, lineNumber)=>{
const tabHighlight = this.state.view === 'text' ? 'pageLine' : 'snippetLine';
const textOrSnip = this.state.view === 'text';
//reset custom line styles
codeMirror?.removeLineClass(lineNumber, 'background', 'pageLine');
codeMirror?.removeLineClass(lineNumber, 'background', 'snippetLine');
codeMirror?.removeLineClass(lineNumber, 'text');
codeMirror?.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
// 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))
return;
// Styling for \page breaks
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
(this.props.renderer == 'V3' && line.match(textOrSnip ? PAGEBREAK_REGEX_V3 : SNIPPETBREAK_REGEX_V3))) {
if((lineNumber > 0) && (textOrSnip)) // Since \page is optional on first line of document,
editorPageCount += 1; // don't use it to increment page count; stay at 1
else if(this.state.view !== 'text') userSnippetCount += 1;
// add back the original class 'background' but also add the new class '.pageline'
codeMirror?.addLineClass(lineNumber, 'background', tabHighlight);
const pageCountElement = Object.assign(document.createElement('span'), {
className : 'editor-page-count',
textContent : textOrSnip ? editorPageCount : userSnippetCount
});
codeMirror?.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
};
// New CodeMirror styling for V3 renderer
if(this.props.renderer === 'V3') {
if(line.match(/^\\column(?:break)?$/)){
codeMirror?.addLineClass(lineNumber, 'text', 'columnSplit');
}
// definition lists
if(line.includes('::')){
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){
codeMirror?.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' });
codeMirror?.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
codeMirror?.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
const ddIndex = match.indices[2][0];
const 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' });
}
}
}
// Subscript & Superscript
if(line.includes('^')) {
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;
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' });
}
startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex));
}
}
// Highlight injectors {style}
if(line.includes('{') && line.includes('}')){
const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
let match;
while ((match = regex.exec(line)) != null) {
codeMirror?.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
}
}
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
if(match[0].startsWith('{')) {
blockCount += 1;
} else {
blockCount -= 1;
}
if(blockCount < 0) {
blockCount = 0;
continue;
}
codeMirror?.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
}
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror?.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
}
// Emojis
if(line.match(/:[^\s:]+:/g)) {
let startIndex = line.indexOf(':');
const emojiRegex = /:[^\s:]+:/gy;
while (startIndex >= 0) {
emojiRegex.lastIndex = startIndex;
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)
return;
const startPos = { line: lineNumber, ch: match.index };
const endPos = { line: lineNumber, ch: match.index + match[0].length };
// Iterate over conflicting marks and clear them
const marks = codeMirror?.findMarks(startPos, endPos);
marks.forEach(function(marker) {
if(!marker.__isFold) marker.clear();
});
codeMirror?.markText(startPos, endPos, { className: 'emoji' });
}
startIndex = line.indexOf(':', Math.max(startIndex + 1, emojiRegex.lastIndex));
}
}
}
});
});
}
},
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
if(!window || !this.isText() || isJumping)
if(!window || !this.isText() || isJumping || jumpSource === 'source')
return;
// Get current brewRenderer scroll position and calculate target position
@@ -343,11 +178,13 @@ const Editor = createReactClass({
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{
isJumping = false;
jumpSource = null;
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
}, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done
};
isJumping = true;
jumpSource = 'brew';
checkIfScrollComplete();
brewRenderer.addEventListener('scroll', checkIfScrollComplete);
@@ -371,54 +208,17 @@ const Editor = createReactClass({
},
sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
if(!this.isText() || isJumping)
if(!this.isText() || isJumping || jumpSource === 'brew')
return;
const textSplit = this.props.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
const editor = this.codeEditor.current;
if(!editor) return;
jumpSource = 'source';
let currentY = this.codeEditor.current.codeMirror?.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, 'local', true);
let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{
isJumping = false;
this.codeEditor.current.codeMirror?.off('scroll', checkIfScrollComplete);
}, 150); // If 150 ms pass without a scroll event, assume scrolling is done
};
isJumping = true;
checkIfScrollComplete();
if(this.codeEditor.current?.codeMirror) {
this.codeEditor.current.codeMirror?.on('scroll', checkIfScrollComplete);
}
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);
// 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');
}
editor.scrollToPage(targetPage);
setTimeout(()=>{
jumpSource = null;
}, 200);
},
//Called when there are changes to the editor's dimensions
@@ -446,9 +246,11 @@ const Editor = createReactClass({
view={this.state.view}
value={this.props.brew.text}
onChange={this.props.onBrewChange('text')}
onCursorChange={(page)=>this.updateCurrentCursorPage(page)}
onViewChange={(page)=>this.updateCurrentViewPage(page)}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
renderer={this.props.brew.renderer}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }}/>
</>;
}
if(this.isStyle()){
@@ -460,18 +262,16 @@ const Editor = createReactClass({
view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onBrewChange('style')}
enableFolding={true}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
renderer={this.props.brew.renderer}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }}/>
</>;
}
if(this.isMeta()){
return <>
<CodeEditor key='codeEditor'
view={this.state.view}
style={{ display: 'none' }}
rerenderParent={this.rerenderParent} />
style={{ display: 'none' }}/>
<MetadataEditor
metadata={this.props.brew}
themeBundle={this.props.themeBundle}
@@ -492,8 +292,9 @@ const Editor = createReactClass({
onChange={this.props.onBrewChange('snippets')}
enableFolding={true}
editorTheme={this.state.editorTheme}
renderer={this.props.brew.renderer}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% -${this.state.snippetBarHeight}px)` }} />
style={{ height: `calc(100% - 25px)` }}/>
</>;
}
},
@@ -510,14 +311,13 @@ const Editor = createReactClass({
return this.codeEditor.current?.undo();
},
foldCode : function(){
return this.codeEditor.current?.foldAllCode();
foldCode : function() {
return this.codeEditor.current?.foldAll();
},
unfoldCode : function(){
return this.codeEditor.current?.unfoldAllCode();
unfoldCode : function() {
return this.codeEditor.current?.unfoldAll();
},
render : function(){
return (
<div className='editor' ref={this.editor}>
@@ -547,4 +347,4 @@ const Editor = createReactClass({
}
});
export default Editor;
export default Editor;
+8 -85
View File
@@ -1,88 +1,11 @@
@import 'themes/codeMirror/customEditorStyles.less';
.editor {
position : relative;
width : 100%;
height : 100%;
container : editor / inline-size;
background:white;
.codeEditor {
height : calc(100% - 25px);
.CodeMirror { height : 100%; }
.pageLine, .snippetLine {
background : #33333328;
border-top : #333399 solid 1px;
}
.editor-page-count {
float : right;
color : grey;
}
.editor-snippet-count {
float : right;
color : grey;
}
.columnSplit {
font-style : italic;
color : grey;
background-color : fade(#229999, 15%);
border-bottom : #229999 solid 1px;
}
.define {
&:not(.term):not(.definition) {
font-weight : bold;
color : #949494;
background : #E5E5E5;
border-radius : 3px;
}
&.term { color : rgb(96, 117, 143); }
&.definition { color : rgb(97, 57, 178); }
}
.block:not(.cm-comment) {
font-weight : bold;
color : purple;
//font-style: italic;
}
.inline-block:not(.cm-comment) {
font-weight : bold;
color : red;
//font-style: italic;
}
.injection:not(.cm-comment) {
font-weight : bold;
color : green;
}
.emoji:not(.cm-comment) {
padding-bottom : 1px;
margin-left : 2px;
font-weight : bold;
color : #360034;
outline : solid 2px #FF96FC;
outline-offset : -2px;
background : #FFC8FF;
border-radius : 6px;
}
.superscript:not(.cm-comment) {
font-size : 0.9em;
font-weight : bold;
vertical-align : super;
color : goldenrod;
}
.subscript:not(.cm-comment) {
font-size : 0.9em;
font-weight : bold;
vertical-align : sub;
color : rgb(123, 123, 15);
}
.dl-highlight {
&.dl-colon-highlight {
font-weight : bold;
color : #949494;
background : #E5E5E5;
border-radius : 3px;
}
&.dt-highlight { color : rgb(96, 117, 143); }
&.dd-highlight { color : rgb(97, 57, 178); }
}
}
@import '@sharedStyles/core.less';
:where(.editor) {
position : relative;
width : 100%;
height : 100%;
container : editor / inline-size;
background : white;
.brewJump {
position : absolute;
@@ -7,7 +7,8 @@ import request from '../../utils/request-middleware.js';
import Combobox from '../../../components/combobox.jsx';
import TagInput from '../tagInput/tagInput.jsx';
import Themes from 'themes/themes.json';
import Themes from '@themes/themes.json';
import validations from './validations.js';
import homebreweryThumbnail from '../../thumbnail.png';
@@ -337,9 +338,9 @@ const MetadataEditor = createReactClass({
{this.renderThumbnail()}
</div>
<div className="field tags">
<div className='field tags'>
<label>Tags</label>
<div className="value" >
<div className='value' >
<TagInput
label='tags'
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
@@ -350,7 +351,7 @@ const MetadataEditor = createReactClass({
/>
</div>
</div>
{this.renderLanguageDropdown()}
@@ -362,9 +363,9 @@ const MetadataEditor = createReactClass({
{this.renderAuthors()}
<div className="field invitedAuthors">
<div className='field invitedAuthors'>
<label>Invited authors</label>
<div className="value">
<div className='value'>
<TagInput
label='invited authors'
valuePatterns={/.+/}
@@ -377,7 +378,7 @@ const MetadataEditor = createReactClass({
/>
</div>
</div>
<h2>Privacy</h2>
@@ -1,4 +1,4 @@
@import 'naturalcrit/styles/colors.less';
@import '@sharedStyles/core.less';
.userThemeName {
padding-right : 10px;
@@ -7,13 +7,13 @@ import _ from 'lodash';
import cx from 'classnames';
import { loadHistory } from '../../utils/versionHistory.js';
import { brewSnippetsToJSON } from '../../../../shared/helpers.js';
import { brewSnippetsToJSON } from '@shared/helpers.js';
import Legacy5ePHB from 'themes/Legacy/5ePHB/snippets.js';
import V3_5ePHB from 'themes/V3/5ePHB/snippets.js';
import V3_5eDMG from 'themes/V3/5eDMG/snippets.js';
import V3_Journal from 'themes/V3/Journal/snippets.js';
import V3_Blank from 'themes/V3/Blank/snippets.js';
import Legacy5ePHB from '@themes/Legacy/5ePHB/snippets.js';
import V3_5ePHB from '@themes/V3/5ePHB/snippets.js';
import V3_5eDMG from '@themes/V3/5eDMG/snippets.js';
import V3_Journal from '@themes/V3/Journal/snippets.js';
import V3_Blank from '@themes/V3/Blank/snippets.js';
const ThemeSnippets = {
Legacy_5ePHB : Legacy5ePHB,
@@ -23,7 +23,25 @@ const ThemeSnippets = {
V3_Blank : V3_Blank,
};
import EditorThemes from 'build/homebrew/codeMirror/editorThemes.json';
import defaultCM5Theme from '@themes/codeMirror/default.js';
import darkbrewery from '@themes/codeMirror/darkbrewery.js';
import cm5Themes from 'codemirror-5-themes';
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
const themeNames = Object.entries(themes)
.filter(([name, value])=>Array.isArray(value) &&
!name.endsWith('Init') &&
!name.endsWith('Style')
)
.map(([name])=>name);
const EditorThemes = [
'default',
...themeNames
.filter((name)=>name !== 'default')
.sort((a, b)=>a.localeCompare(b))
];
const execute = function(val, props){
if(_.isFunction(val)) return val(props);
@@ -151,7 +169,7 @@ const Snippetbar = createReactClass({
this.props.updateEditorTheme(e.target.value);
this.setState({
showThemeSelector : false,
themeSelector : false,
});
},
@@ -232,11 +250,11 @@ const Snippetbar = createReactClass({
<i className='fas fa-clock-rotate-left' />
{ this.state.showHistory && this.renderHistoryItems() }
</div>
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
<div className={`editorTool undo ${this.props.historySize.done ? 'active' : ''}`}
onClick={this.props.undo} >
<i className='fas fa-undo' />
</div>
<div className={`editorTool redo ${this.props.historySize.redo ? 'active' : ''}`}
<div className={`editorTool redo ${this.props.historySize.undone ? 'active' : ''}`}
onClick={this.props.redo} >
<i className='fas fa-redo' />
</div>
@@ -1,5 +1,6 @@
@import '@sharedStyles/core.less';
@import (less) './client/icons/customIcons.less';
@import (less) '././././themes/fonts/5e/fonts.less';
@import (less) '@themes/fonts/5e/fonts.less';
.snippetBar {
@menuHeight : 25px;
@@ -1,210 +1,219 @@
export default [
export const tagSuggestionList = [
// ############################## Systems
// D&D
"system:D&D Original",
"system:D&D Basic",
"system:AD&D 1e",
"system:AD&D 2e",
"system:D&D 3e",
"system:D&D 3.5e",
"system:D&D 4e",
"system:D&D 5e",
"system:D&D 5e 2024",
"system:BD&D (B/X)",
"system:D&D Essentials",
'system:D&D Original',
'system:D&D Basic',
'system:AD&D 1e',
'system:AD&D 2e',
'system:D&D 3e',
'system:D&D 3.5e',
'system:D&D 4e',
'system:D&D 5e',
'system:D&D 5e 2024',
'system:BD&D (B/X)',
'system:D&D Essentials',
// Other Famous RPGs
"system:Pathfinder 1e",
"system:Pathfinder 2e",
"system:Vampire: The Masquerade",
"system:Werewolf: The Apocalypse",
"system:Mage: The Ascension",
"system:Call of Cthulhu",
"system:Shadowrun",
"system:Star Wars RPG (D6/D20/Edge of the Empire)",
"system:Warhammer Fantasy Roleplay",
"system:Cyberpunk 2020",
"system:Blades in the Dark",
"system:Daggerheart",
"system:Draw Steel",
"system:Mutants and Masterminds",
'system:Pathfinder 1e',
'system:Pathfinder 2e',
'system:Vampire: The Masquerade',
'system:Werewolf: The Apocalypse',
'system:Mage: The Ascension',
'system:Call of Cthulhu',
'system:Shadowrun',
'system:Star Wars RPG (D6/D20/Edge of the Empire)',
'system:Warhammer Fantasy Roleplay',
'system:Cyberpunk 2020',
'system:Blades in the Dark',
'system:Daggerheart',
'system:Draw Steel',
'system:Mutants and Masterminds',
// Meta
"meta:V3",
"meta:Legacy",
"meta:Template",
"meta:Theme",
"meta:free",
"meta:Character Sheet",
"meta:Documentation",
"meta:NPC",
"meta:Guide",
"meta:Resource",
"meta:Notes",
"meta:Example",
'meta:V3',
'meta:Legacy',
'meta:Template',
'meta:Theme',
'meta:free',
'meta:Character Sheet',
'meta:Documentation',
'meta:NPC',
'meta:Guide',
'meta:Resource',
'meta:Notes',
'meta:Example',
// Book type
"type:Campaign",
"type:Campaign Setting",
"type:Adventure",
"type:One-Shot",
"type:Setting",
"type:World",
"type:Lore",
"type:History",
"type:Dungeon Master",
"type:Encounter Pack",
"type:Encounter",
"type:Session Notes",
"type:reference",
"type:Handbook",
"type:Manual",
"type:Manuals",
"type:Compendium",
"type:Bestiary",
'type:Campaign',
'type:Campaign Setting',
'type:Adventure',
'type:One-Shot',
'type:Setting',
'type:World',
'type:Lore',
'type:History',
'type:Dungeon Master',
'type:Encounter Pack',
'type:Encounter',
'type:Session Notes',
'type:reference',
'type:Handbook',
'type:Manual',
'type:Manuals',
'type:Compendium',
'type:Bestiary',
// ###################################### RPG Keywords
// Classes / Subclasses / Archetypes
"Class",
"Subclass",
"Archetype",
"Martial",
"Half-Caster",
"Full Caster",
"Artificer",
"Barbarian",
"Bard",
"Cleric",
"Druid",
"Fighter",
"Monk",
"Paladin",
"Rogue",
"Sorcerer",
"Warlock",
"Wizard",
'Class',
'Subclass',
'Archetype',
'Martial',
'Half-Caster',
'Full Caster',
'Artificer',
'Barbarian',
'Bard',
'Cleric',
'Druid',
'Fighter',
'Monk',
'Paladin',
'Rogue',
'Sorcerer',
'Warlock',
'Wizard',
// Races / Species / Lineages
"Race",
"Ancestry",
"Lineage",
"Aasimar",
"Beastfolk",
"Dragonborn",
"Dwarf",
"Elf",
"Goblin",
"Half-Elf",
"Half-Orc",
"Human",
"Kobold",
"Lizardfolk",
"Lycan",
"Orc",
"Tiefling",
"Vampire",
"Yuan-Ti",
'Race',
'Ancestry',
'Lineage',
'Aasimar',
'Beastfolk',
'Dragonborn',
'Dwarf',
'Elf',
'Goblin',
'Half-Elf',
'Half-Orc',
'Human',
'Kobold',
'Lizardfolk',
'Lycan',
'Orc',
'Tiefling',
'Vampire',
'Yuan-Ti',
// Magic / Spells / Items
"Magic",
"Magic Item",
"Magic Items",
"Wondrous Item",
"Magic Weapon",
"Artifact",
"Spell",
"Spells",
"Cantrip",
"Cantrips",
"Eldritch",
"Eldritch Invocation",
"Invocation",
"Invocations",
"Pact boon",
"Pact Boon",
"Spellcaster",
"Spellblade",
"Magical Tattoos",
"Enchantment",
"Enchanted",
"Attunement",
"Requires Attunement",
"Rune",
"Runes",
"Wand",
"Rod",
"Scroll",
"Potion",
"Potions",
"Item",
"Items",
"Bag of Holding",
'Magic',
'Magic Item',
'Magic Items',
'Wondrous Item',
'Magic Weapon',
'Artifact',
'Spell',
'Spells',
'Cantrip',
'Cantrips',
'Eldritch',
'Eldritch Invocation',
'Invocation',
'Invocations',
'Pact boon',
'Pact Boon',
'Spellcaster',
'Spellblade',
'Magical Tattoos',
'Enchantment',
'Enchanted',
'Attunement',
'Requires Attunement',
'Rune',
'Runes',
'Wand',
'Rod',
'Scroll',
'Potion',
'Potions',
'Item',
'Items',
'Bag of Holding',
// Monsters / Creatures / Enemies
"Monster",
"Creatures",
"Creature",
"Beast",
"Beasts",
"Humanoid",
"Undead",
"Fiend",
"Aberration",
"Ooze",
"Giant",
"Dragon",
"Monstrosity",
"Demon",
"Devil",
"Elemental",
"Construct",
"Constructs",
"Boss",
"BBEG",
'Monster',
'Creatures',
'Creature',
'Beast',
'Beasts',
'Humanoid',
'Undead',
'Fiend',
'Aberration',
'Ooze',
'Giant',
'Dragon',
'Monstrosity',
'Demon',
'Devil',
'Elemental',
'Construct',
'Constructs',
'Boss',
'BBEG',
// ############################# Media / Pop Culture
"One Piece",
"Dragon Ball",
"Dragon Ball Z",
"Naruto",
"Jujutsu Kaisen",
"Fairy Tail",
"Final Fantasy",
"Kingdom Hearts",
"Elder Scrolls",
"Skyrim",
"WoW",
"World of Warcraft",
"Marvel Comics",
"DC Comics",
"Pokemon",
"League of Legends",
"Runeterra",
"Arcane",
"Yu-Gi-Oh",
"Minecraft",
"Don't Starve",
"Witcher",
"Witcher 3",
"Cyberpunk",
"Cyberpunk 2077",
"Fallout",
"Divinity Original Sin 2",
"Fullmetal Alchemist",
"Fullmetal Alchemist Brotherhood",
"Lobotomy Corporation",
"Bloodborne",
"Dragonlance",
"Shackled City Adventure Path",
"Baldurs Gate 3",
"Library of Ruina",
"Radiant Citadel",
"Ravenloft",
"Forgotten Realms",
"Exandria",
"Critical Role",
"Star Wars",
"SW5e",
"Star Wars 5e",
'One Piece',
'Dragon Ball',
'Dragon Ball Z',
'Naruto',
'Jujutsu Kaisen',
'Fairy Tail',
'Final Fantasy',
'Kingdom Hearts',
'Elder Scrolls',
'Skyrim',
'WoW',
'World of Warcraft',
'Marvel Comics',
'DC Comics',
'Pokemon',
'League of Legends',
'Runeterra',
'Arcane',
'Yu-Gi-Oh',
'Minecraft',
'Don\'t Starve',
'Witcher',
'Witcher 3',
'Cyberpunk',
'Cyberpunk 2077',
'Fallout',
'Divinity Original Sin 2',
'Fullmetal Alchemist',
'Fullmetal Alchemist Brotherhood',
'Lobotomy Corporation',
'Bloodborne',
'Dragonlance',
'Shackled City Adventure Path',
'Baldurs Gate 3',
'Library of Ruina',
'Radiant Citadel',
'Ravenloft',
'Forgotten Realms',
'Exandria',
'Critical Role',
'Star Wars',
'SW5e',
'Star Wars 5e',
];
// substrings to be normalized to the first value on the array
export const canonizationList = [
['5e 2024', '5.5e', '5e\'24', '5.24', '5e24', '5.5'],
['5e', '5th Edition'],
['Dungeons & Dragons', 'Dungeons and Dragons', 'Dungeons n dragons'],
['D&D', 'DnD', 'dnd', 'Dnd', 'dnD', 'd&d', 'd&D', 'D&d'],
['P2e', 'p2e', 'P2E', 'Pathfinder 2e'],
];
+105 -118
View File
@@ -1,71 +1,62 @@
import "./tagInput.less";
import React, { useState, useEffect } from "react";
import Combobox from "../../../components/combobox.jsx";
import './tagInput.less';
import React, { useState, useEffect } from 'react';
import Combobox from '../../../components/combobox.jsx';
import tagSuggestionList from "./curatedTagSuggestionList.js";
import { tagSuggestionList, canonizationList } from './curatedTagSuggestionList.js';
const TagInput = ({tooltip, label, valuePatterns, values = [], unique = true, placeholder = "", smallText = "", onChange }) => {
const TagInput = ({ tooltip, label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{
const [tagList, setTagList] = useState(
values.map((value) => ({
values.map((value)=>({
value,
editing: false,
draft: "",
editing : false,
draft : '',
})),
);
useEffect(() => {
useEffect(()=>{
const incoming = values || [];
const current = tagList.map((t) => t.value);
const current = tagList.map((t)=>t.value);
const changed = incoming.length !== current.length || incoming.some((v, i) => v !== current[i]);
const changed = incoming.length !== current.length || incoming.some((v, i)=>v !== current[i]);
if (changed) {
if(changed) {
setTagList(
incoming.map((value) => ({
incoming.map((value)=>({
value,
editing: false,
editing : false,
})),
);
}
}, [values]);
useEffect(() => {
useEffect(()=>{
onChange?.({
target: { value: tagList.map((t) => t.value) },
target : { value: tagList.map((t)=>t.value) },
});
}, [tagList]);
// substrings to be normalized to the first value on the array
const duplicateGroups = [
["5e 2024", "5.5e", "5e'24", "5.24", "5e24", "5.5"],
["5e", "5th Edition"],
["Dungeons & Dragons", "Dungeons and Dragons", "Dungeons n dragons"],
["D&D", "DnD", "dnd", "Dnd", "dnD", "d&d", "d&D", "D&d"],
["P2e", "p2e", "P2E", "Pathfinder 2e"],
];
const normalizeValue = (input) => {
const normalizeValue = (input)=>{
const lowerInput = input.toLowerCase();
let normalizedTag = input;
for (const group of duplicateGroups) {
for (const group of canonizationList) {
for (const tag of group) {
if (!tag) continue;
if(!tag) continue;
const index = lowerInput.indexOf(tag.toLowerCase());
if (index !== -1) {
if(index !== -1) {
normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length);
break;
}
}
}
if (normalizedTag.includes(":")) {
const [rawType, rawValue = ""] = normalizedTag.split(":");
if(normalizedTag.includes(':')) {
const [rawType, rawValue = ''] = normalizedTag.split(':');
const tagType = rawType.trim().toLowerCase();
const tagValue = rawValue.trim();
if (tagValue.length > 0) {
if(tagValue.length > 0) {
normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`;
}
//trims spaces around colon and capitalizes the first word after the colon
@@ -75,56 +66,56 @@ const TagInput = ({tooltip, label, valuePatterns, values = [], unique = true, pl
return normalizedTag;
};
const submitTag = (newValue, index = null) => {
const submitTag = (newValue, index = null)=>{
const trimmed = newValue?.trim();
if (!trimmed) return;
if (!valuePatterns.test(trimmed)) return;
if(!trimmed) return;
if(!valuePatterns.test(trimmed)) return;
const normalizedTag = normalizeValue(trimmed);
setTagList((prev) => {
const existsIndex = prev.findIndex((t) => t.value.toLowerCase() === normalizedTag.toLowerCase());
if (unique && existsIndex !== -1) return prev;
if (index !== null) {
return prev.map((t, i) => (i === index ? { ...t, value: normalizedTag, editing: false } : t));
setTagList((prev)=>{
const existsIndex = prev.findIndex((t)=>t.value.toLowerCase() === normalizedTag.toLowerCase());
if(unique && existsIndex !== -1) return prev;
if(index !== null) {
return prev.map((t, i)=>(i === index ? { ...t, value: normalizedTag, editing: false } : t));
}
return [...prev, { value: normalizedTag, editing: false }];
});
};
const removeTag = (index) => {
setTagList((prev) => prev.filter((_, i) => i !== index));
const removeTag = (index)=>{
setTagList((prev)=>prev.filter((_, i)=>i !== index));
};
const editTag = (index) => {
setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: true, draft: t.value } : t)));
const editTag = (index)=>{
setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: true, draft: t.value } : t)));
};
const stopEditing = (index) => {
setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: false, draft: "" } : t)));
const stopEditing = (index)=>{
setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: false, draft: '' } : t)));
};
const suggestionOptions = tagSuggestionList.map((tag) => {
const tagType = tag.split(":");
const suggestionOptions = tagSuggestionList.map((tag)=>{
const tagType = tag.split(':');
let classes = "item";
let classes = 'item';
switch (tagType[0]) {
case "type":
classes = "item type";
break;
case "group":
classes = "item group";
break;
case "meta":
classes = "item meta";
break;
case "system":
classes = "item system";
break;
default:
classes = "item";
break;
case 'type':
classes = 'item type';
break;
case 'group':
classes = 'item group';
break;
case 'meta':
classes = 'item meta';
break;
case 'system':
classes = 'item system';
break;
default:
classes = 'item';
break;
}
return (
@@ -135,73 +126,69 @@ const TagInput = ({tooltip, label, valuePatterns, values = [], unique = true, pl
});
return (
<div className="tagInputWrap">
<div className='tagInputWrap'>
<Combobox
trigger="click"
className="tagInput-dropdown"
default=""
trigger='click'
className='tagInput-dropdown'
default=''
placeholder={placeholder}
options={label === "tags" ? suggestionOptions : []}
options={label === 'tags' ? suggestionOptions : []}
tooltip={tooltip}
autoSuggest={
label === "tags"
label === 'tags'
? {
suggestMethod: "startsWith",
clearAutoSuggestOnClick: true,
filterOn: ["value", "title"],
}
: { suggestMethod: "includes", clearAutoSuggestOnClick: true, filterOn: [] }
suggestMethod : 'startsWith',
clearAutoSuggestOnClick : true,
filterOn : ['value', 'title'],
}
: { suggestMethod: 'includes', clearAutoSuggestOnClick: true, filterOn: [] }
}
valuePatterns={valuePatterns.source}
onSelect={(value) => submitTag(value)}
onEntry={(e) => {
if (e.key === "Enter") {
onSelect={(value)=>submitTag(value)}
onEntry={(e)=>{
if(e.key === 'Enter') {
e.preventDefault();
submitTag(e.target.value);
}
}}
/>
<ul className="list">
{tagList.map((t, i) =>
t.editing ? (
<input
key={i}
type="text"
value={t.draft} // always use draft
pattern={valuePatterns.source}
onChange={(e) =>
setTagList((prev) =>
prev.map((tag, idx) => (idx === i ? { ...tag, draft: e.target.value } : tag)),
)
<ul className='list'>
{tagList.map((t, i)=>t.editing ? (
<input
key={i}
type='text'
value={t.draft} // always use draft
pattern={valuePatterns.source}
onChange={(e)=>setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: e.target.value } : tag)),
)
}
onKeyDown={(e)=>{
if(e.key === 'Enter') {
e.preventDefault();
submitTag(t.draft, i); // submit draft
setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: '' } : tag)),
);
}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
submitTag(t.draft, i); // submit draft
setTagList((prev) =>
prev.map((tag, idx) => (idx === i ? { ...tag, draft: "" } : tag)),
);
}
if (e.key === "Escape") {
stopEditing(i);
e.target.blur();
}
}}
autoFocus
/>
) : (
<li key={i} className="tag" onClick={() => editTag(i)}>
{t.value}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
removeTag(i);
}}>
<i className="fa fa-times fa-fw" />
</button>
</li>
),
if(e.key === 'Escape') {
stopEditing(i);
e.target.blur();
}
}}
autoFocus
/>
) : (
<li key={i} className='tag' onClick={()=>editTag(i)}>
{t.value}
<button
type='button'
onClick={(e)=>{
e.stopPropagation();
removeTag(i);
}}>
<i className='fa fa-times fa-fw' />
</button>
</li>
),
)}
</ul>
</div>
File diff suppressed because it is too large Load Diff
+26 -13
View File
@@ -1,8 +1,7 @@
import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers
import 'core-js/es/string/to-well-formed.js'; // Polyfill for older browsers
import './homebrew.less';
import React from 'react';
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
import { BrowserRouter as Router, Routes, Route, useParams, useSearchParams } from 'react-router';
import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js';
@@ -39,28 +38,42 @@ const Homebrew = (props)=>{
},
userThemes,
brews,
enable_v4
enablev4
} = props;
global.account = account;
global.version = version;
global.config = config;
global.enable_v4 = enable_v4;
global.enablev4 = enablev4;
const backgroundObject = ()=>{
if(global.config.deployment || (config.local && config.development)){
const bgText = global.config.deployment || 'Local';
return {
backgroundImage : `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='100px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${bgText}</text></svg>")`
};
if(config?.deployment || (config?.local && config?.development)) {
const bgText = config?.deployment || 'Local';
return {
backgroundImage : `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='100px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${bgText}</text></svg>")`
};
}
return null;
};
updateLocalStorage();
if(brew.pureError) {
return (
<Router>
<div className={`homebrew${(config?.deployment || config?.local) ? ' deployment' : ''}`} style={backgroundObject()}>
<Routes>
<Route path={brew.originalUrl} element={<WithRoute el={ErrorPage} brew={brew} />} />
</Routes>
</div>
</Router>
);
}
return (
<Router location={url}>
<div className={`homebrew${(config.deployment || config.local) ? ' deployment' : ''}`} style={backgroundObject()}>
<Router>
<div className={`homebrew${(config?.deployment || config?.local) ? ' deployment' : ''}`} style={backgroundObject()}>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
@@ -82,4 +95,4 @@ const Homebrew = (props)=>{
);
};
module.exports = Homebrew;
export default Homebrew;
+1 -1
View File
@@ -1,4 +1,4 @@
@import 'naturalcrit/styles/core.less';
@import '@sharedStyles/core.less';
.homebrew {
height : 100%;
background-color:@steel;
+6
View File
@@ -0,0 +1,6 @@
import { createRoot } from 'react-dom/client';
import Homebrew from './homebrew.jsx';
const props = window.__INITIAL_PROPS__ || {};
createRoot(document.getElementById('reactRoot')).render(<Homebrew {...props} />);
+1 -1
View File
@@ -97,7 +97,7 @@ const Account = createReactClass({
// Logged out
// LOCAL ONLY
if(global.config.local) {
if(global.config?.local) {
return <Nav.item color='teal' icon='fas fa-sign-in-alt' onClick={this.localLogin}>
login
</Nav.item>;
@@ -1,3 +1,5 @@
@import '@sharedStyles/core.less';
.navItem.error {
position : relative;
background-color : @red;
@@ -46,11 +46,6 @@ const MetadataNav = createReactClass({
</>;
},
getSystems : function(){
if(!this.props.brew.systems || this.props.brew.systems.length == 0) return 'No systems';
return this.props.brew.systems.join(', ');
},
renderMetaWindow : function(){
return <div className={`window ${this.state.showMetaWindow ? 'active' : 'inactive'}`}>
<div className='row'>
@@ -65,10 +60,6 @@ const MetadataNav = createReactClass({
<h4>Tags</h4>
<p>{this.getTags()}</p>
</div>
<div className='row'>
<h4>Systems</h4>
<p>{this.getSystems()}</p>
</div>
<div className='row'>
<h4>Updated</h4>
<p>{Moment(this.props.brew.updatedAt).fromNow()}</p>
+4 -10
View File
@@ -8,16 +8,10 @@ import PatreonNavItem from './patreon.navitem.jsx';
const Navbar = createReactClass({
displayName : 'Navbar',
getInitialState : function() {
return {
//showNonChromeWarning : false,
ver : '0.0.0'
};
},
getInitialState : function() {
return {
ver : global.version
};
return {
// showNonChromeWarning: false, // uncomment if needed
ver : global.version || '0.0.0'
};
},
/*
+1 -1
View File
@@ -1,4 +1,4 @@
@import 'naturalcrit/styles/colors.less';
@import '@sharedStyles/core.less';
@navbarHeight : 28px;
@viewerToolsHeight : 32px;
+2 -2
View File
@@ -1,7 +1,7 @@
import React from 'react';
import _ from 'lodash';
import Nav from './nav.jsx';
import { splitTextStyleAndMetadata } from '../../../shared/helpers.js';
import { splitTextStyleAndMetadata } from '@shared/helpers.js';
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
@@ -24,7 +24,7 @@ const NewBrew = ()=>{
localStorage.setItem(BREWKEY, newBrew.text);
localStorage.setItem(STYLEKEY, newBrew.style);
localStorage.setItem(METAKEY, JSON.stringify(
_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])
_.pick(newBrew, ['title', 'description', 'tags', 'renderer', 'theme', 'lang'])
));
window.location.href = '/new';
return;
+19 -3
View File
@@ -1,9 +1,25 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import Nav from './nav.jsx';
import { printCurrentBrew } from '../../../shared/helpers.js';
import { printCurrentBrew } from '@shared/helpers.js';
export default function(){
const [printing, setPrinting] = useState(false);
// listen for print cycle events to display "loading" message since it can take some time.
useEffect(()=>{
document.addEventListener('print:startprep', handlePrintStartPrep);
document.addEventListener('print:finishedprep', handlePrintPrepFinished);
return ()=>{
document.removeEventListener('print:startprep', handlePrintStartPrep);
document.removeEventListener('print:finishedprep', handlePrintPrepFinished);
}
}, []);
const handlePrintStartPrep = ()=>{ setPrinting(true); };
const handlePrintPrepFinished = ()=>{ setPrinting(false); };
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
get PDF
{printing ? 'loading' : 'get PDF'}
</Nav.item>;
};
+7 -1
View File
@@ -17,7 +17,7 @@ const getRedditLink = (brew)=>{
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
};
export default ({ brew })=>(
export default ({ brew, currentPage })=>(
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share
@@ -28,6 +28,12 @@ export default ({ brew })=>(
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}`);}}>
copy url
</Nav.item>
{currentPage > 1 &&
<Nav.item
color='blue'
onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}#p${currentPage}`);}}>
copy url (page {currentPage})
</Nav.item>}
<Nav.item color='blue' href={getRedditLink(brew)} newTab rel='noopener noreferrer'>
post to reddit
</Nav.item>
@@ -1,3 +1,4 @@
@import '@sharedStyles/core.less';
.brewItem {
position : relative;
+38 -39
View File
@@ -4,34 +4,35 @@ import './editPage.less';
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from '../../../../shared/markdown.js';
import Markdown from '@shared/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from '../../navbar/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import RecentNavItems from '../../navbar/recent.navitem.jsx';
import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from '@navbar/print.navitem.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
// Page specific imports
import { Meta } from 'vitreum/headtags';
import Headtags from '../../../../vitreum/headtags.js';
const Meta = Headtags.Meta;
import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate';
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
import ShareNavItem from '../../navbar/share.navitem.jsx';
import ShareNavItem from '@navbar/share.navitem.jsx';
import LockNotification from './lockNotification/lockNotification.jsx';
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
import googleDriveIcon from '../../googleDrive.svg';
@@ -56,28 +57,28 @@ const EditPage = (props)=>{
...props
};
const [currentBrew , setCurrentBrew ] = useState(props.brew);
const [isSaving , setIsSaving ] = useState(false);
const [lastSavedTime , setLastSavedTime ] = useState(new Date());
const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId);
const [error , setError ] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
const [currentBrew, setCurrentBrew] = useState(props.brew);
const [isSaving, setIsSaving] = useState(false);
const [lastSavedTime, setLastSavedTime] = useState(new Date());
const [saveGoogle, setSaveGoogle] = useState(!!props.brew.googleId);
const [error, setError] = useState(null);
const [HTMLErrors, setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false);
const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed);
const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false);
const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true);
const [warnUnsavedChanges , setWarnUnsavedChanges ] = useState(true);
const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges, setUnsavedChanges] = useState(false);
const [alertTrashedGoogleBrew, setAlertTrashedGoogleBrew] = useState(props.brew.trashed);
const [alertLoginToTransfer, setAlertLoginToTransfer] = useState(false);
const [confirmGoogleTransfer, setConfirmGoogleTransfer] = useState(false);
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const saveTimeout = useRef(null);
const warnUnsavedTimeout = useRef(null);
const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{
@@ -89,7 +90,7 @@ const EditPage = (props)=>{
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 83) trySaveRef.current(true, true, saveGoogle);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
@@ -117,13 +118,9 @@ const EditPage = (props)=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
if(autoSaveEnabled) trySave(false, hasChange, saveGoogle);
}, [currentBrew]);
useEffect(()=>{
trySave(true);
}, [saveGoogle]);
const handleSplitMove = ()=>{
editorRef.current?.update();
};
@@ -182,11 +179,13 @@ const EditPage = (props)=>{
};
const toggleGoogleStorage = ()=>{
const newSaveGoogle = !saveGoogle;
setSaveGoogle((prev)=>!prev);
setError(null);
trySave(true, true, newSaveGoogle);
};
const trySave = (immediate = false, hasChanges = true)=>{
const trySave = (immediate = false, hasChanges = true, saveToGoogle = false)=>{
clearTimeout(saveTimeout.current);
if(isSaving) return;
if(!hasChanges && !immediate) return;
@@ -195,7 +194,7 @@ const EditPage = (props)=>{
saveTimeout.current = setTimeout(async ()=>{
setIsSaving(true);
setError(null);
await save(currentBrew, saveGoogle)
await save(currentBrew, saveToGoogle)
.catch((err)=>{
setError(err);
});
@@ -215,7 +214,7 @@ const EditPage = (props)=>{
const brewToSave = {
...brew,
text : brew.text.normalize('NFC'),
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1,
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm)) || []).length + 1,
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
hash : await md5(lastSavedBrew.current.text.normalize('NFC')),
textBin : undefined,
@@ -313,7 +312,7 @@ const EditPage = (props)=>{
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>save now</Nav.item>;
return <Nav.item className='save' onClick={()=>trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
@@ -364,7 +363,7 @@ const EditPage = (props)=>{
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<ShareNavItem brew={currentBrew} />
<ShareNavItem brew={currentBrew} currentPage={currentBrewRendererPageNum} />
<RecentNavItem brew={currentBrew} storageKey='edit' />
<AccountNavItem/>
</Nav.section>
@@ -1,7 +1,7 @@
import './errorPage.less';
import React from 'react';
import UIPage from '../basePages/uiPage/uiPage.jsx';
import Markdown from '../../../../shared/markdown.js';
import Markdown from '@shared/markdown.js';
import ErrorIndex from './errors/errorIndex.js';
const ErrorPage = ({ brew })=>{
@@ -1,7 +1,6 @@
.homebrew {
.uiPage.sitePage {
.uiPage.sitePage:has(.errorTitle) {
.errorTitle {
//background-color: @orange;
color : #D02727;
text-align : center;
}
+22 -21
View File
@@ -1,33 +1,34 @@
/* eslint-disable max-lines */
import './homePage.less';
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from '../../../../shared/markdown.js';
import Markdown from '@shared/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from '../../navbar/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import RecentNavItems from '../../navbar/recent.navitem.jsx';
import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from '@navbar/print.navitem.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
// Page specific imports
import { Meta } from 'vitreum/headtags';
import Headtags from '@vitreum/headtags.js';
const Meta = Headtags.Meta;
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
@@ -44,16 +45,16 @@ const HomePage =(props)=>{
...props
};
const [currentBrew , setCurrentBrew] = useState(props.brew);
const [error , setError] = useState(undefined);
const [HTMLErrors , setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1);
const [currentBrew, setCurrentBrew] = useState(props.brew);
const [error, setError] = useState(undefined);
const [HTMLErrors, setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle] = useState({});
const [unsavedChanges , setUnsavedChanges] = useState(false);
const [isSaving , setIsSaving] = useState(false);
const [autoSaveEnabled , setAutoSaveEnable] = useState(false);
const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges, setUnsavedChanges] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [autoSaveEnabled, setAutoSaveEnable] = useState(false);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
@@ -1,3 +1,5 @@
@import '@sharedStyles/core.less';
.homePage {
position : relative;
a.floatingNewButton {
+22 -23
View File
@@ -4,29 +4,28 @@ import './newPage.less';
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from '../../../../shared/markdown.js';
import Markdown from '@shared/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from '../../navbar/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import RecentNavItems from '../../navbar/recent.navitem.jsx';
import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from '@navbar/print.navitem.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
// Page specific imports
import { Meta } from 'vitreum/headtags';
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
@@ -43,23 +42,23 @@ const NewPage = (props)=>{
...props
};
const [currentBrew , setCurrentBrew ] = useState(props.brew);
const [isSaving , setIsSaving ] = useState(false);
const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false);
const [error , setError ] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
const [currentBrew, setCurrentBrew] = useState(props.brew);
const [isSaving, setIsSaving] = useState(false);
const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false);
const [error, setError] = useState(null);
const [HTMLErrors, setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(false);
const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges, setUnsavedChanges] = useState(false);
const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
// const saveTimeout = useRef(null);
// const warnUnsavedTimeout = useRef(null);
const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{
@@ -157,7 +156,7 @@ const NewPage = (props)=>{
const updatedBrew = { ...currentBrew };
splitTextStyleAndMetadata(updatedBrew);
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm;
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
const res = await request
@@ -1,3 +1,5 @@
@import '@sharedStyles/colors.less';
.newPage {
.navItem.save {
background-color : @orange;
+22 -8
View File
@@ -1,18 +1,19 @@
import './sharePage.less';
import React, { useState, useEffect, useCallback } from 'react';
import { Meta } from 'vitreum/headtags';
import Headtags from '../../../../vitreum/headtags.js';
const Meta = Headtags.Meta;
import Nav from '../../navbar/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import MetadataNav from '../../navbar/metadata.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import RecentNavItems from '../../navbar/recent.navitem.jsx';
import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx';
import MetadataNav from '@navbar/metadata.navitem.jsx';
import PrintNavItem from '@navbar/print.navitem.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
import Account from '../../navbar/account.navitem.jsx';
import Account from '@navbar/account.navitem.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle } from '../../../../shared/helpers.js';
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
const SharePage = (props)=>{
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
@@ -91,6 +92,19 @@ const SharePage = (props)=>{
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${processShareId()}`}>
clone to new
</Nav.item>
<Nav.item
color='blue'
icon='fas fa-link'
onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${processShareId()}`);}}>
copy url
</Nav.item>
{currentBrewRendererPageNum > 1 &&
<Nav.item
color='blue'
icon='fas fa-hashtag'
onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${processShareId()}#p${currentBrewRendererPageNum}`);}}>
copy url (page {currentBrewRendererPageNum})
</Nav.item>}
</Nav.dropdown>
</>
)}
+8 -8
View File
@@ -3,15 +3,15 @@ import _ from 'lodash';
import ListPage from '../basePages/listPage/listPage.jsx';
import Nav from '../../navbar/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import RecentNavItems from '../../navbar/recent.navitem.jsx';
import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
import Account from '../../navbar/account.navitem.jsx';
import NewBrew from '../../navbar/newbrew.navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import VaultNavitem from '../../navbar/vault.navitem.jsx';
import Account from '@navbar/account.navitem.jsx';
import NewBrew from '@navbar/newbrew.navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx';
import ErrorNavItem from '@navbar/error-navitem.jsx';
import VaultNavitem from '@navbar/vault.navitem.jsx';
const UserPage = (props)=>{
props = {
@@ -3,13 +3,13 @@
import './vaultPage.less';
import React, { useState, useEffect, useRef } from 'react';
import Nav from '../../navbar/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import RecentNavItems from '../../navbar/recent.navitem.jsx';
import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
import Account from '../../navbar/account.navitem.jsx';
import NewBrew from '../../navbar/newbrew.navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import Account from '@navbar/account.navitem.jsx';
import NewBrew from '@navbar/newbrew.navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx';
import BrewItem from '../basePages/listPage/brewItem/brewItem.jsx';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import ErrorIndex from '../errorPage/errors/errorIndex.js';
@@ -1,3 +1,5 @@
@import '@sharedStyles/core.less';
.vaultPage {
height : 100%;
overflow-y : hidden;