0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-06-22 00:38:38 +00:00

Merge branch 'master' into preloadVars

This commit is contained in:
David Bolack
2026-05-18 21:50:32 -05:00
21 changed files with 859 additions and 1341 deletions
+55 -8
View File
@@ -13,9 +13,11 @@ import {
ViewPlugin,
drawSelection,
dropCursor,
rectangularSelection,
crosshairCursor,
} from '@codemirror/view';
import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state';
import { foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, syntaxHighlighting } from '@codemirror/language';
import { foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, foldEffect, foldState, syntaxHighlighting } from '@codemirror/language';
import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
import { languages } from '@codemirror/language-data';
import { css } from '@codemirror/lang-css';
@@ -27,11 +29,11 @@ import { closeBrackets } from '@codemirror/autocomplete';
const autoCloseBrackets = closeBrackets({ brackets: ['()', '[]', '{{}}'] });
import * as themesImport from '@uiw/codemirror-themes-all';
import defaultCM5Theme from '@themes/codeMirror/default.js';
import darkbrewery from '@themes/codeMirror/darkbrewery.js';
import cm5Themes from 'codemirror-5-themes';
const themes = { default: defaultCM5Theme, darkbrewery, ...themesImport };
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
const themeCompartment = new Compartment();
const highlightCompartment = new Compartment();
@@ -146,11 +148,14 @@ const CodeEditor = forwardRef(
const editorRef = useRef(null);
const viewRef = useRef(null);
const docsRef = useRef({});
const tabRef = useRef(tab);
const prevTabRef = useRef(tab);
const scrollRef = useRef({});
const foldsRef = useRef({});
const pageMap = useRef([]);
const recomputePages = (doc)=>{
if(tab !== 'brewText') return;
const pages = [0];
const text = doc.toString();
let offset = 0;
@@ -176,6 +181,14 @@ const CodeEditor = forwardRef(
return page;
};
const getFoldRanges = (state)=>{
const folds = [];
state.field(foldState, false)?.between(0, state.doc.length, (from, to)=>{
folds.push({ from, to });
});
return folds;
};
const createExtensions = ({ onChange, language, editorTheme })=>{
const setEventListeners = EditorView.updateListener.of((update)=>{
if(update.docChanged) {
@@ -229,6 +242,8 @@ const CodeEditor = forwardRef(
//multiple cursors and selections
drawSelection(),
rectangularSelection(),
crosshairCursor(),
EditorState.allowMultipleSelections.of(true),
dropCursor(),
programmaticCursorLineField,
@@ -260,11 +275,10 @@ const CodeEditor = forwardRef(
ticking = true;
requestAnimationFrame(()=>{
const top = view.scrollDOM.scrollTop;
scrollRef.current[tabRef.current] = top;
const block = view.lineBlockAtHeight(top);
const page = findPageFromPos(block.from); // CHANGED
const page = findPageFromPos(block.from);
onViewChange(page);
ticking = false;
});
};
@@ -279,12 +293,23 @@ const CodeEditor = forwardRef(
};
}, []);
const restoreFolds = (view, folds)=>{
if(!folds?.length) return;
view.dispatch({
effects : folds.map((f)=>foldEffect.of(f))
});
};
useEffect(()=>{
const view = viewRef.current;
if(!view) return;
tabRef.current = tab;
const prevTab = prevTabRef.current;
foldsRef.current[prevTab] = getFoldRanges(view.state);
if(prevTab !== tab) {
docsRef.current[prevTab] = view.state;
@@ -298,6 +323,16 @@ const CodeEditor = forwardRef(
}
view.setState(nextState);
restoreFolds(view, foldsRef.current[tab]);
const savedScroll = scrollRef.current[tab];
if(savedScroll != null) {
requestAnimationFrame(()=>{
view.scrollDOM.scrollTop = savedScroll;
});
}
prevTabRef.current = tab;
}
view.focus();
@@ -392,7 +427,19 @@ const CodeEditor = forwardRef(
foldAll : ()=>{
const view = viewRef.current;
if(!view) return;
view.dispatch(foldAllCmd(view));
const doc = view.state.doc;
const pages = pageMap.current;
const effects = pages.map((start, i)=>{
const next = pages[i + 1] || doc.length;
const from = i ? doc.line(doc.lineAt(start).number + 1).from : 0;
const to = doc.line(doc.lineAt(next).number).from - 1;
return to > from ? foldEffect.of({ from, to }) : null;
}).filter(Boolean);
view.dispatch({ effects });
},
unfoldAll : ()=>{
const view = viewRef.current;
+129 -22
View File
@@ -1,31 +1,138 @@
// Icon fonts for emoji/autocomplete
@import (less) "@themes/fonts/iconFonts/diceFont.less";
@import (less) "@themes/fonts/iconFonts/elderberryInn.less";
@import (less) "@themes/fonts/iconFonts/gameIcons.less";
@import (less) "@themes/fonts/iconFonts/fontAwesome.less";
@import (less) '@themes/fonts/iconFonts/diceFont.less';
@import (less) '@themes/fonts/iconFonts/elderberryInn.less';
@import (less) '@themes/fonts/iconFonts/gameIcons.less';
@import (less) '@themes/fonts/iconFonts/fontAwesome.less';
@keyframes sourceMoveAnimation {
50% {
color: white;
background-color: red;
color : white;
background-color : red;
}
100% {
color: unset;
background-color: unset;
color : unset;
background-color : unset;
}
}
:where(.codeEditor) {
font-family: monospace;
height: 100%;
width:100%;
.cm-content {
tab-size:2 !important;
width : 100%;
height : calc(100% - 25px);
font-family : monospace;
.cm-editor {
height : 100%;
outline : none !important;
}
@media screen and (pointer: coarse) {
font-size: 16px;
&.brewSnippets .cm-snippetLine,
:where(&.brewText) .cm-pageLine {
background : #33333328;
border-top : #333399 solid 1px;
}
&.brewSnippets {
.cm-pageLine {
color : #777777;
background : #3E4E3E1B;
border-top : #3399423B solid 1px;
}
}
&:where(.brewText), &.brewSnippets {
.cm-pageLine[data-page-number]::after {
float : right;
color : grey;
content : attr(data-page-number);
}
.cm-columnSplit {
font-style : italic;
color : grey;
background-color : fade(#229999, 15%);
border-bottom : #229999 solid 1px;
}
.cm-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); }
}
.cm-block:not(.cm-comment) {
font-weight : bold;
color : purple;
}
.cm-inline-block,
.cm-define .cm-inline-block {
font-weight : bold;
color : red;
span:not(.cm-comment) { color : inherit; }
}
.cm-injection:not(.cm-comment) {
font-weight : bold;
color : green;
span { color : inherit; }
}
.cm-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;
}
.cm-superscript:not(.cm-comment) {
font-size : 0.9em;
font-weight : bold;
vertical-align : super;
color : goldenrod;
}
.cm-subscript:not(.cm-comment) {
font-size : 0.9em;
font-weight : bold;
vertical-align : sub;
color : rgb(123, 123, 15);
}
.cm-strikethrough {
text-decoration: line-through;
}
.cm-definitionList {
.cm-definitionTerm { color : rgb(96, 117, 143); }
.cm-definitionColon:not(:has(.cm-comment)) {
font-weight : bold;
color : #949494;
background : #E5E5E5;
border-radius : 3px;
}
.cm-definitionDesc { color : rgb(97, 57, 178); }
}
.cm-tooltip-autocomplete {
li {
display : flex;
gap : 10px;
align-items : center;
justify-content : flex-start;
.cm-completionIcon { display : none; }
.cm-tooltip-autocomplete .cm-completionLabel { translate : 0 -2px; }
}
}
}
.cm-content { tab-size : 2 !important; }
@media screen and (pointer : coarse) {
font-size : 16px;
}
.cm-gutterElement span {
@@ -44,14 +151,14 @@
/* Flash animation for source moves */
.cm-line.sourceMoveFlash {
animation-name: sourceMoveAnimation;
animation-duration: 0.4s;
animation-name : sourceMoveAnimation;
animation-duration : 0.4s;
}
/* Search input */
.cm-searchField {
width: 25em !important;
outline: 1px inset #00000055 !important;
width : 25em !important;
outline : 1px inset #00000055 !important;
}
/* Tab character visualization (optional) */
@@ -67,6 +174,6 @@
/* Emoji preview styling */
.emojiPreview {
font-size: 1.5em;
line-height: 1.2em;
font-size : 1.5em;
line-height : 1.2em;
}
@@ -16,6 +16,7 @@ const customTags = {
definitionTerm : 'definitionTerm', // .cm-definitionTerm
definitionDesc : 'definitionDesc', // .cm-definitionDesc
definitionColon : 'definitionColon', // .cm-definitionColon
strikethrough : 'strikethrough', // .cm-strikethrough
//CSS
@@ -81,6 +82,23 @@ export function tokenizeCustomMarkdown(text) {
}
}
// --- Strikethrough ---
if(/\~/.test(lineText)) {
const strikethroughRegex = /~(?!\s)(.+?)(?<!\s)~/g;
const match = strikethroughRegex.exec(lineText);
const type = customTags.strikethrough;
if(match) {
tokens.push({
line : lineNumber,
type,
from : match.index,
to : match.index + match[0].length,
});
}
}
// --- single line def list ---
const singleLineRegex = /^(?=.*[^:])(.+?)(\s*)(::)([^\n]*)$/dmy;
const match = singleLineRegex.exec(lineText);
@@ -125,8 +143,6 @@ export function tokenizeCustomMarkdown(text) {
from : offset,
to : offset + desc.length,
});
return;
}
// --- multiline def list ---
@@ -139,14 +155,14 @@ export function tokenizeCustomMarkdown(text) {
for (let i = lineNumber + 1; i < lines.length; i++) {
const nextLine = lines[i];
const onlyColonsMatch = /^:*$/.test(nextLine);
const defMatch = /^(::)(.*\S.*)?\s*$/.exec(nextLine);
const defMatch = /^(::)(.+)$/.exec(nextLine);
if(!onlyColonsMatch && defMatch) {
defs.push({ colons: defMatch[1], desc: defMatch[2], line: i });
endLine = i;
} else break;
}
if(defs.length > 0) {
if(defs.length > 0 && lineText.trim().length > 0) {
tokens.push({
line : startLine,
type : customTags.definitionList,
@@ -177,20 +193,20 @@ export function tokenizeCustomMarkdown(text) {
line : d.line,
type : customTags.definitionDesc,
from : d.colons.length,
to : d.colons.length + d.desc.length,
to : d.colons.length + d.desc?.length,
});
});
}
}
if(lineText.includes('{') && lineText.includes('}')) {
const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gmd;
let match;
while ((match = injectionRegex.exec(lineText)) !== null) {
tokens.push({
line : lineNumber,
from : match.index,
to : match.index + match[1].length,
from : match.indices[1][0],
to : match.indices[1][1],
type : customTags.injection,
});
}
+69 -87
View File
@@ -1,6 +1,18 @@
/* eslint max-lines: ["error", { "max": 300 }] */
import { keymap } from '@codemirror/view';
import { undo, redo, indentMore } from '@codemirror/commands';
import { undo, redo, indentMore, deleteLine } from '@codemirror/commands';
import { Prec } from '@codemirror/state';
const insertTab = (view)=>{
const { from, to } = view.state.selection.main;
view.dispatch({
changes : { from, to, insert: ' ' },
selection : { anchor: from + 2 }
});
return true;
};
const indentLess = (view)=>{
const { from, to } = view.state.selection.main;
@@ -16,74 +28,43 @@ const indentLess = (view)=>{
return true;
};
const makeBold = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('**') && selected.endsWith('**')
? selected.slice(2, -2)
: `**${selected}**`;
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: from + text.length },
});
return true;
};
const wrapSelection = (prefix, suffix)=>(view)=>{
const changes = [];
const makeItalic = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('*') && selected.endsWith('*')
? selected.slice(1, -1)
: `*${selected}*`;
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: from + text.length },
});
return true;
};
for (const range of view.state.selection.ranges) {
const { from, to } = range;
const selected = view.state.doc.sliceString(from, to);
const makeUnderline = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('<u>') && selected.endsWith('</u>')
? selected.slice(3, -4)
: `<u>${selected}</u>`;
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: from + text.length },
});
return true;
};
let text;
const makeSuper = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('^') && selected.endsWith('^')
? selected.slice(1, -1)
: `^${selected}^`;
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: from + text.length },
});
return true;
};
if(from === to) { text = prefix + suffix; } else if(selected.startsWith(prefix) && selected.endsWith(suffix)) {
text = selected.slice(prefix.length, -suffix.length);
} else {text = `${prefix}${selected}${suffix}`;}
changes.push({ from, to, insert: text });
}
const makeSub = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('^^') && selected.endsWith('^^')
? selected.slice(2, -2)
: `^^${selected}^^`;
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: from + text.length },
changes
});
return true;
};
const makeNbsp = (view)=>{
const { from, to } = view.state.selection.main;
view.dispatch({ changes: { from, to, insert: '&nbsp;' } });
const { from } = view.state.selection.main;
const prev2 = from >= 2
? view.state.doc.sliceString(from - 2, from)
: '';
const insert = (prev2 === ':>' || prev2 === '>>') ? '>' : ':>';
view.dispatch({
changes : { from, to: from, insert },
selection : { anchor: from + insert.length },
});
return true;
};
@@ -187,36 +168,37 @@ const newPage = (view)=>{
return true;
};
export const generalKeymap = keymap.of([
{ key: 'Tab', run: indentMore },
export const generalKeymap = Prec.high(keymap.of([
{ key: 'Tab', run: insertTab },
{ key: 'Mod-z', run: undo }, //i think it may be unnecessary
{ key: 'Mod-Shift-z', run: redo },
]);
{ key: 'Mod-y', run: redo },
{ key: 'Mod-d', run: deleteLine },
]));
export const markdownKeymap = keymap.of([
export const markdownKeymap = Prec.highest(keymap.of([
//{ key: 'Shift-Tab', run: indentMore },
{ key: 'Shift-Tab', run: indentLess },
{ key: 'Mod-b', run: makeBold },
{ key: 'Mod-i', run: makeItalic },
{ key: 'Mod-u', run: makeUnderline },
{ key: 'Shift-Mod-=', run: makeSuper },
{ key: 'Mod-=', run: makeSub },
{ key: 'Mod-.', run: makeNbsp },
{ key: 'Shift-Mod-.', run: makeSpace },
{ key: 'Shift-Mod-,', run: removeSpace },
{ key: 'Mod-m', run: makeSpan },
{ key: 'Shift-Mod-m', run: makeDiv },
{ key: 'Mod-/', run: makeComment },
{ key: 'Mod-k', run: makeLink },
{ key: 'Mod-l', run: makeList('UL') },
{ key: 'Shift-Mod-l', run: makeList('OL') },
{ key: 'Shift-Mod-1', run: makeHeader(1) },
{ key: 'Shift-Mod-2', run: makeHeader(2) },
{ key: 'Shift-Mod-3', run: makeHeader(3) },
{ key: 'Shift-Mod-4', run: makeHeader(4) },
{ key: 'Shift-Mod-5', run: makeHeader(5) },
{ key: 'Shift-Mod-6', run: makeHeader(6) },
{ key: 'Shift-Tab', run: indentLess },
{ key: 'Mod-b', run: wrapSelection('**', '**') }, // makeBold
{ key: 'Mod-i', run: wrapSelection('*', '*') }, // makeItalic
{ key: 'Mod-u', run: wrapSelection('<u>', '</u>') }, // makeUnderline
{ key: 'Shift-Mod-=', run: wrapSelection('^', '^') }, // makeSuper
{ key: 'Mod-=', run: wrapSelection('^^', '^^') }, // makeSub
{ key: 'Mod-.', run: makeNbsp },
{ key: 'Shift-Mod-.', run: makeSpace },
{ key: 'Shift-Mod-,', run: removeSpace },
{ key: 'Mod-m', run: makeSpan },
{ key: 'Shift-Mod-m', run: makeDiv },
{ key: 'Mod-/', run: makeComment },
{ key: 'Mod-k', run: makeLink },
{ key: 'Mod-l', run: makeList('UL') },
{ key: 'Shift-Mod-l', run: makeList('OL') },
{ key: 'Shift-Mod-1', run: makeHeader(1) },
{ key: 'Shift-Mod-2', run: makeHeader(2) },
{ key: 'Shift-Mod-3', run: makeHeader(3) },
{ key: 'Shift-Mod-4', run: makeHeader(4) },
{ key: 'Shift-Mod-5', run: makeHeader(5) },
{ key: 'Shift-Mod-6', run: makeHeader(6) },
{ key: 'Mod-Enter', run: newPage },
{ key: 'Shift-Mod-Enter', run: newColumn },
{ key: 'Mod-Enter', run: newPage },
]);
]));
+2 -2
View File
@@ -11,11 +11,11 @@ import MetadataEditor from './metadataEditor/metadataEditor.jsx';
const EDITOR_THEME_KEY = 'HB_editor_theme';
import * as themesImport from '@uiw/codemirror-themes-all';
import defaultCM5Theme from '@themes/codeMirror/default.js';
import darkbrewery from '@themes/codeMirror/darkbrewery.js';
import cm5Themes from 'codemirror-5-themes';
const themes = { default: defaultCM5Theme, darkbrewery, ...themesImport };
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
const EditorThemes = Object.entries(themes)
.filter(([name, value])=>Array.isArray(value) &&
-110
View File
@@ -6,116 +6,6 @@
height : 100%;
container : editor / inline-size;
background : white;
:where(.codeEditor) {
height : calc(100% - 25px);
.cm-editor { height : 100%;
outline:none !important;
}
&.brewSnippets .cm-snippetLine {
background : #33333328;
border-top : #333399 solid 1px;
}
:where(&.brewText) .cm-pageLine {
background : #33333328;
border-top : #333399 solid 1px;
}
&.brewSnippets {
.cm-pageLine {
background : #3e4e3e1b;
border-top : #3399423b solid 1px;
color:#777;
}
}
&:where(.brewText), &.brewSnippets {
.cm-tooltip-autocomplete {
li {
display : flex;
gap : 10px;
align-items : center;
justify-content : flex-start;
.cm-completionIcon { display : none; }
.cm-tooltip-autocomplete .cm-completionLabel { translate : 0 -2px; }
}
}
.cm-pageLine[data-page-number]::after {
content:attr(data-page-number);
float:right;
color : grey;
}
.cm-columnSplit {
font-style : italic;
color : grey;
background-color : fade(#229999, 15%);
border-bottom : #229999 solid 1px;
}
.cm-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); }
}
.cm-block:not(.cm-comment) {
font-weight : bold;
color : purple;
}
.cm-inline-block:not(.cm-comment) {
font-weight : bold;
color : red ;
span { color : inherit }
}
.cm-injection:not(.cm-comment) {
font-weight : bold;
color : green;
span { color : inherit }
}
.cm-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;
}
.cm-superscript:not(.cm-comment) {
font-size : 0.9em;
font-weight : bold;
vertical-align : super;
color : goldenrod;
}
.cm-subscript:not(.cm-comment) {
font-size : 0.9em;
font-weight : bold;
vertical-align : sub;
color : rgb(123, 123, 15);
}
.cm-definitionList {
.cm-definitionTerm { color : rgb(96, 117, 143); }
.cm-definitionColon {
font-weight : bold;
color : #949494;
background : #E5E5E5;
border-radius : 3px;
}
.cm-definitionDesc { color : rgb(97, 57, 178); }
}
}
}
.brewJump {
position : absolute;
@@ -23,19 +23,25 @@ const ThemeSnippets = {
V3_Blank : V3_Blank,
};
import * as themesImport from '@uiw/codemirror-themes-all';
import defaultCM5Theme from '@themes/codeMirror/default.js';
import darkbrewery from '@themes/codeMirror/darkbrewery.js';
import cm5Themes from 'codemirror-5-themes';
const themes = { default: defaultCM5Theme, darkbrewery, ...themesImport };
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
const EditorThemes = Object.entries(themes)
.filter(([name, value]) =>
Array.isArray(value) &&
const themeNames = Object.entries(themes)
.filter(([name, value])=>Array.isArray(value) &&
!name.endsWith('Init') &&
!name.endsWith('Style')
)
.map(([name]) => name);
.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);
@@ -163,7 +169,7 @@ const Snippetbar = createReactClass({
this.props.updateEditorTheme(e.target.value);
this.setState({
showThemeSelector : false,
themeSelector : false,
});
},
+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>
+9 -11
View File
@@ -90,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();
@@ -118,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();
};
@@ -183,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;
@@ -196,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);
});
@@ -216,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,
@@ -314,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)
@@ -365,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 -1
View File
@@ -156,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
@@ -92,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>
</>
)}