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:
@@ -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);
|
||||
|
||||
@@ -43,4 +43,4 @@ function safeHTML(htmlString) {
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
module.exports.safeHTML = safeHTML;
|
||||
export default safeHTML;
|
||||
@@ -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;
|
||||
@@ -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'],
|
||||
];
|
||||
@@ -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
@@ -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,4 +1,4 @@
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
@import '@sharedStyles/core.less';
|
||||
.homebrew {
|
||||
height : 100%;
|
||||
background-color:@steel;
|
||||
|
||||
@@ -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} />);
|
||||
@@ -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>
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
@import '@sharedStyles/core.less';
|
||||
|
||||
@navbarHeight : 28px;
|
||||
@viewerToolsHeight : 32px;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user