mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-11-04 18:18:46 +00:00
Merge branch 'master' into fixPageTitle
This commit is contained in:
@@ -82,6 +82,9 @@ jobs:
|
||||
- run:
|
||||
name: Test - HTML sanitization
|
||||
command: npm run test:safehtml
|
||||
- run:
|
||||
name: Test - Helpers
|
||||
command: npm run test:helpers
|
||||
- run:
|
||||
name: Test - Coverage
|
||||
command: npm run test:coverage
|
||||
|
||||
@@ -6,6 +6,9 @@ updates:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 99
|
||||
groups:
|
||||
eslint-bundle:
|
||||
patterns: ["*eslint*"]
|
||||
update-types: ["patch", "minor", "major"]
|
||||
dev-dependencies:
|
||||
dependency-type: "development"
|
||||
patterns: ["*"]
|
||||
|
||||
+65
-11
@@ -93,30 +93,83 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Saturday 4/20/2026 - v3.22.0
|
||||
|
||||
### Monday 9/07/2026 - v3.23.0
|
||||
{{taskList
|
||||
##### firstmatekidd (new contributor!)
|
||||
* [x] Add nav buttons to tab order for accesibility
|
||||
* [x] Add title to brewRenderer iframe for accessibility
|
||||
* [x] Add aria labels to toolbars
|
||||
|
||||
Partially fixes issue [#3032](https://github.com/naturalcrit/homebrewery/issues/3032)
|
||||
|
||||
##### ReinaSweet (new contributor!)
|
||||
* [x] Added security to prevent scripts running in brews
|
||||
|
||||
##### Gazook89
|
||||
* [x] Lazy load brew images for faster first load
|
||||
* [x] Fix snippet menus getting cut off behind divider bar
|
||||
|
||||
Fixes issue [#3840](https://github.com/naturalcrit/homebrewery/issues/3840)
|
||||
|
||||
##### Abquintic
|
||||
* [x] Fix variables not hoisting in later pages
|
||||
|
||||
##### 5e-Cleric
|
||||
* [x] Major update to editor framework (Codemirror 6)
|
||||
Fixes issues [#3511](https://github.com/naturalcrit/homebrewery/issues/3511), [#4590](https://github.com/naturalcrit/homebrewery/issues/4590), [#4563](https://github.com/naturalcrit/homebrewery/issues/4653), [#4655](https://github.com/naturalcrit/homebrewery/issues/4655)
|
||||
* [x] Fix to Admin page tab names
|
||||
* [x] Add image preview when hovering over image syntax
|
||||
|
||||
Fixes issue [#2037](https://github.com/naturalcrit/homebrewery/issues/2037)
|
||||
|
||||
* [x] Remember editor theme across tabs
|
||||
|
||||
Fixes issue [#4858](https://github.com/naturalcrit/homebrewery/issues/4858)
|
||||
|
||||
* [x] Add ability to remove co-authors if you are the owner
|
||||
|
||||
Fixes part of issue [#4101](https://github.com/naturalcrit/homebrewery/issues/4101)
|
||||
|
||||
##### G-Ambatte
|
||||
* [x] Fix white page crash on certain browsers
|
||||
* [x] Fix editor panel shrinking when opening dev tools
|
||||
|
||||
Fixes issue [#4866](https://github.com/naturalcrit/homebrewery/issues/4866)
|
||||
|
||||
* [x] Added security to prevent scripts running in brews
|
||||
|
||||
Fixes issue [#4904](https://github.com/naturalcrit/homebrewery/issues/4904)
|
||||
|
||||
##### Abquintic, G-Ambatte, Gazook89, 5e-Cleric
|
||||
* [x] Multiple background fixes and code cleanups
|
||||
}}
|
||||
|
||||
### Wednesday 5/13/2026 - v3.22.1
|
||||
{{taskList
|
||||
##### Frederlk (new Contributor!)
|
||||
* [x] Add "share current page" to {{openSans **SHARE :fas_share_nodes:**}} menu
|
||||
|
||||
##### Calculuschild, 5e-Cleric
|
||||
* [x] Fix brews triggering unneeded save when loaded
|
||||
|
||||
##### G-Ambatte
|
||||
* [x] Fix brews with `$` in text getting out of sync on save
|
||||
|
||||
##### 5e-Cleric, Gazook89
|
||||
* [x] Fix various issues with Codemirror 6
|
||||
|
||||
Fixes issues [#4771](https://github.com/naturalcrit/homebrewery/issues/4771), [#4783](https://github.com/naturalcrit/homebrewery/issues/4783)
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
### Saturday 4/04/2026 - v3.21.0
|
||||
|
||||
{{taskList
|
||||
##### Gazook89
|
||||
* [x] Allow custom {{openSans **:fas_table_list: SNIPPETS**}} to be inserted mid-line
|
||||
* [x] Allow custom {{openSans **:fas_table_list: SNIPPETS**}} dropdown to be inserted mid-line
|
||||
|
||||
##### abquintic
|
||||
* [x] Move example snippet images out of imgur (for folks without imgur access)
|
||||
|
||||
##### 5e-Cleric
|
||||
* [x] Add auto-suggest to tag entry input box
|
||||
* [x] Replace all example artwork with
|
||||
* [x] Added tooltips to the {{openSans :fas_circle_info: **Properties**}} menu
|
||||
* [x] Removed {{openSans **SYSTEMS**}} checkboxes from {{openSans :fas_circle_info: **Properties**}} menu; instead {{openSans **TAGS**}} should be used for this purpose
|
||||
* [x] Replace all AI-generated art with public domain art
|
||||
@@ -131,6 +184,7 @@ Fixes issues [#543](https://github.com/naturalcrit/homebrewery/issues/543), [#24
|
||||
* [x] Multiple other backend fixes and refactors
|
||||
}}
|
||||
|
||||
|
||||
### Friday 1/11/2026 - v3.20.1
|
||||
|
||||
{{taskList
|
||||
@@ -167,7 +221,7 @@ Fixes issue [#4559](https://github.com/naturalcrit/homebrewery/issues/4559)
|
||||
##### G-Ambatte
|
||||
* [x] Fix default save location failing on new documents
|
||||
|
||||
Fixes issue [#4437](https://github.com/naturalcrit/homebrewery/issues/3175)
|
||||
Fixes issue [#4437](https://github.com/naturalcrit/homebrewery/issues/4437)
|
||||
* [x] Fix usernames with special symbols unable to open userpage
|
||||
|
||||
Fixes issue [#807](https://github.com/naturalcrit/homebrewery/issues/807)
|
||||
@@ -326,7 +380,7 @@ Fixes issue [#1729](https://github.com/naturalcrit/homebrewery/issues/1729)
|
||||
Fixes issue [#4079](https://github.com/naturalcrit/homebrewery/issues/4079)
|
||||
|
||||
##### Calculuschild
|
||||
* [x] `꞉꞉꞉꞉` now produces `<br>` instead of a `<div>`
|
||||
* [x] `꞉꞉꞉꞉` now produces `<br>` instead of a `<div>`
|
||||
* [x] Fix typos in tables freezing the editor
|
||||
|
||||
Fixes issue [#4059](https://github.com/naturalcrit/homebrewery/issues/4059)
|
||||
@@ -2396,4 +2450,4 @@ Massive changelog incoming:
|
||||
|
||||
* Added `phb.standalone.css` plus a build system for creating it
|
||||
* Added page numbers and footer text
|
||||
* Page accent now flips each page
|
||||
* Page accent now flips each page
|
||||
|
||||
@@ -107,14 +107,14 @@ body {
|
||||
}
|
||||
|
||||
th, td {
|
||||
max-width : 50ch;
|
||||
padding : 5px 10px;
|
||||
overflow : hidden;
|
||||
text-overflow : ellipsis;
|
||||
vertical-align : middle;
|
||||
text-align : center;
|
||||
white-space : nowrap;
|
||||
border-right : 1px solid;
|
||||
max-width:50ch;
|
||||
overflow:hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child { border-right : none; }
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const BrewCleanup = ({})=>{
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const find = async (type)=>{
|
||||
|
||||
|
||||
|
||||
if(type === 'junk') try {
|
||||
setPendingJunk(true);
|
||||
@@ -109,18 +109,18 @@ const BrewCleanup = ({})=>{
|
||||
<td style={{ width: '200px' }}>{Moment(brew.updatedAt).fromNow()}</td>
|
||||
<td>{brew.lastViewed ? Moment(brew.lastViewed).fromNow() : 'No last viewed date'}</td>
|
||||
<td>{brew.googleId ? 'Google' : 'Homebrewery'}</td>
|
||||
</tr>
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>;
|
||||
};
|
||||
const renderFound = (type)=>{
|
||||
const deleteButton = !(type === 'junk' && junkBrewCollection.length === 0 || type === 'lost' && lostBrewCollection.length === 0);
|
||||
const deleteButton = !(type === 'junk' && junkBrewCollection.length === 0 || type === 'lost' && lostBrewCollection.length === 0);
|
||||
|
||||
return <div className='result'>
|
||||
{deleteButton && <button onClick={()=>cleanup(type)} className='remove'>
|
||||
{pendingLost && type === "lost" || pendingJunk && type === "junk"
|
||||
{pendingLost && type === 'lost' || pendingJunk && type === 'junk'
|
||||
? <i className='fas fa-spin fa-spinner' />
|
||||
: <span><i className='fas fa-times' /> Remove</span>
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
margin-left : 10px;
|
||||
}
|
||||
&.checkbox {
|
||||
line-height: 1.5em;
|
||||
line-height : 1.5em;
|
||||
input {
|
||||
width : 1.5em;
|
||||
height : 1.5em;
|
||||
@@ -26,9 +26,9 @@
|
||||
}
|
||||
|
||||
.lockSuggestions {
|
||||
columns : 2;
|
||||
line-height : 1.2em;
|
||||
break-inside : avoid;
|
||||
columns : 2;
|
||||
h2 { column-span : all; }
|
||||
h3 { margin-top : 0px; }
|
||||
b { font-weight : 600; }
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
.anchored-box {
|
||||
position : absolute;
|
||||
inset-block-start : anchor(bottom);
|
||||
visibility : hidden;
|
||||
justify-self : anchor-center;
|
||||
inset-block-start : anchor(bottom);
|
||||
&.active { visibility : visible; }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
This file basically checks support for Anchor Positioning API in the browser,
|
||||
and then loads the Oddbird polyfill if support is lacking.
|
||||
and then loads the Oddbird polyfill if support is lacking.
|
||||
*/
|
||||
|
||||
let polyfillPromise;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint max-lines: ["error", { "max": 405 }] */
|
||||
/* eslint max-lines: ["error", { "max": 455 }] */
|
||||
import './codeEditor.less';
|
||||
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
|
||||
@@ -42,10 +42,11 @@ import cm5Themes from 'codemirror-5-themes';
|
||||
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
|
||||
const themeCompartment = new Compartment();
|
||||
const highlightCompartment = new Compartment();
|
||||
const settingsCompartment = new Compartment();
|
||||
|
||||
import { generalKeymap, markdownKeymap, cssKeymap, formatCSS } from './extensions/customKeyMaps.js';
|
||||
import foldOnPages from './extensions/customFolding.js';
|
||||
import { customHighlightStyle , customHighlightPlugin } from './extensions/customHighlight.js';
|
||||
import { customHighlightStyle, customHighlightPlugin } from './extensions/customHighlight.js';
|
||||
import { legacyCustomHighlightStyle } from './extensions/legacyCustomHighlight.js';
|
||||
|
||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||
@@ -78,6 +79,20 @@ const programmaticCursorLineField = StateField.define({
|
||||
provide : (decorationSet)=>EditorView.decorations.from(decorationSet)
|
||||
});
|
||||
|
||||
const createSettingsExtensions = (settings)=>[
|
||||
...(settings.autoCloseBrackets ? [autoCloseBrackets] : []),
|
||||
...(settings.lineNumbers ? [lineNumbers()] : []),
|
||||
...(settings.activeLineShading ? [highlightActiveLine(),
|
||||
highlightActiveLineGutter()] : []),
|
||||
...(settings.fontSize
|
||||
? [EditorView.theme({
|
||||
'&, .cm-content' : {
|
||||
fontSize : `${settings.fontSize || 1}em`,
|
||||
},
|
||||
})]
|
||||
: []),
|
||||
];
|
||||
|
||||
const CodeEditor = forwardRef(
|
||||
(
|
||||
{
|
||||
@@ -88,9 +103,11 @@ const CodeEditor = forwardRef(
|
||||
onChange = ()=>{},
|
||||
onCursorChange = ()=>{},
|
||||
onViewChange = ()=>{},
|
||||
onThemeChange = ()=>{},
|
||||
editorTheme = 'default',
|
||||
style,
|
||||
renderer,
|
||||
settings = {},
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -163,8 +180,7 @@ const CodeEditor = forwardRef(
|
||||
EditorView.lineWrapping,
|
||||
setEventListeners,
|
||||
languageExtension,
|
||||
autoCloseBrackets,
|
||||
lineNumbers(),
|
||||
settingsCompartment.of(createSettingsExtensions(settings)),
|
||||
scrollPastEnd(),
|
||||
search(),
|
||||
history(), //allows for undo and redo
|
||||
@@ -178,10 +194,8 @@ const CodeEditor = forwardRef(
|
||||
}),
|
||||
|
||||
//highlights
|
||||
highlightCompartment.of([customHighlightPlugin(renderer, tab), highlightExtension]),
|
||||
highlightCompartment.of([customHighlightPlugin(renderer, tab, settings), highlightExtension]),
|
||||
themeCompartment.of(themeExtension),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
|
||||
//keyboard shortcut
|
||||
keymap.of([...defaultKeymap, foldKeymap, ...searchKeymap]),
|
||||
@@ -271,6 +285,12 @@ const CodeEditor = forwardRef(
|
||||
}
|
||||
|
||||
view.setState(nextState);
|
||||
view.dispatch({
|
||||
effects : settingsCompartment.reconfigure(
|
||||
createSettingsExtensions(settings)
|
||||
),
|
||||
});
|
||||
|
||||
restoreFolds(view, foldsRef.current[tab]);
|
||||
|
||||
const savedScroll = scrollRef.current[tab];
|
||||
@@ -308,6 +328,9 @@ const CodeEditor = forwardRef(
|
||||
view.dispatch({
|
||||
effects : themeCompartment.reconfigure(themeExtension),
|
||||
});
|
||||
|
||||
const isDark = view.state.facet(EditorView.darkTheme);
|
||||
onThemeChange(isDark);
|
||||
}, [editorTheme, tab]);
|
||||
|
||||
useEffect(()=>{
|
||||
@@ -320,10 +343,21 @@ const CodeEditor = forwardRef(
|
||||
: syntaxHighlighting(legacyCustomHighlightStyle);
|
||||
|
||||
view.dispatch({
|
||||
effects : highlightCompartment.reconfigure([customHighlightPlugin(renderer, tab), highlightExtension]),
|
||||
effects : highlightCompartment.reconfigure([customHighlightPlugin(renderer, tab, settings), highlightExtension])
|
||||
});
|
||||
}, [renderer, tab]);
|
||||
|
||||
useEffect(()=>{
|
||||
const view = viewRef.current;
|
||||
if(!view) return;
|
||||
|
||||
view.dispatch({
|
||||
effects : settingsCompartment.reconfigure(
|
||||
createSettingsExtensions(settings)
|
||||
),
|
||||
});
|
||||
}, [settings]);
|
||||
|
||||
useImperativeHandle(ref, ()=>({
|
||||
|
||||
injectText : (text)=>{
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
}
|
||||
|
||||
@keyframes slideacross {
|
||||
0% {
|
||||
bottom: -200px;
|
||||
left: -200px;
|
||||
}
|
||||
0% {
|
||||
bottom : -200px;
|
||||
left : -200px;
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom:100%;
|
||||
left:100%;
|
||||
}
|
||||
100% {
|
||||
bottom : 100%;
|
||||
left : 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:where(.codeEditor) {
|
||||
@@ -37,22 +37,22 @@
|
||||
outline : none !important;
|
||||
|
||||
&.cm-flash {
|
||||
position:relative;
|
||||
position : relative;
|
||||
&::after {
|
||||
position:absolute;
|
||||
content:'';
|
||||
bottom:-200px;
|
||||
left:-200px;
|
||||
translate:-50%;
|
||||
width:200%;
|
||||
height:100px;
|
||||
background:linear-gradient(0deg, #89eafc00 0px, #89ebfc8e 50px, #89eafc00 100px, transparent);
|
||||
display:block;
|
||||
position : absolute;
|
||||
bottom : -200px;
|
||||
left : -200px;
|
||||
display : block;
|
||||
width : 200%;
|
||||
height : 100px;
|
||||
content : '';
|
||||
background : linear-gradient(0deg, #89EAFC00 0px, #89EBFC8E 50px, #89EAFC00 100px, transparent);
|
||||
|
||||
rotate:30deg;
|
||||
rotate : 30deg;
|
||||
translate : -50%;
|
||||
|
||||
@media screen and (prefers-reduced-motion: no-preference) {
|
||||
animation: .5s linear 1 slideacross ;
|
||||
@media screen and (prefers-reduced-motion : no-preference) {
|
||||
animation : 0.5s linear 1 slideacross;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,9 +134,7 @@
|
||||
vertical-align : sub;
|
||||
color : rgb(123, 123, 15);
|
||||
}
|
||||
.cm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.cm-strikethrough { text-decoration : line-through; }
|
||||
|
||||
.cm-definitionList {
|
||||
.cm-definitionTerm { color : rgb(96, 117, 143); }
|
||||
@@ -196,30 +194,27 @@
|
||||
}
|
||||
|
||||
.cm-image {
|
||||
position:relative;
|
||||
position : relative;
|
||||
|
||||
.cm-preview {
|
||||
object-fit: contain;
|
||||
position:absolute;
|
||||
bottom:0;
|
||||
left:0;
|
||||
height:200px;
|
||||
width:200px;
|
||||
height:200px;
|
||||
padding:5px;
|
||||
background-color: #fff;
|
||||
border-radius:10px;
|
||||
border:3px solid grey;
|
||||
pointer-events: none;
|
||||
opacity:0;
|
||||
transition:0.2s opacity 0.5s;
|
||||
translate:0 100%;
|
||||
z-index:1000;
|
||||
position : absolute;
|
||||
bottom : 0;
|
||||
left : 0;
|
||||
z-index : 1000;
|
||||
width : 200px;
|
||||
height : 200px;
|
||||
padding : 5px;
|
||||
pointer-events : none;
|
||||
object-fit : contain;
|
||||
background-color : #FFFFFF;
|
||||
border : 3px solid grey;
|
||||
border-radius : 10px;
|
||||
opacity : 0;
|
||||
translate : 0 100%;
|
||||
transition : 0.2s opacity 0.5s;
|
||||
}
|
||||
|
||||
&:hover .cm-preview {
|
||||
opacity:1;
|
||||
}
|
||||
&:hover .cm-preview { opacity : 1; }
|
||||
}
|
||||
|
||||
/* Tab character visualization (optional) */
|
||||
|
||||
@@ -366,7 +366,7 @@ class ImageWidget extends WidgetType {
|
||||
}
|
||||
}
|
||||
|
||||
export function customHighlightPlugin(renderer, tab) {
|
||||
export function customHighlightPlugin(renderer, tab, settings) {
|
||||
//this function takes the custom tokens created in the tokenize function in customhighlight files
|
||||
//takes the tokens defined by that function and assigns classes to them
|
||||
//it also creates page number and snippet number widgets
|
||||
@@ -398,7 +398,7 @@ export function customHighlightPlugin(renderer, tab) {
|
||||
const tree = ensureSyntaxTree(view.state, view.state.doc.length, 50) || syntaxTree(view.state);
|
||||
tree.iterate({
|
||||
enter : (node)=>{
|
||||
if(node.name === 'Image') {
|
||||
if(node.name === 'Image' && settings.showImagePreviews) {
|
||||
const url = getUrl(node, view.state.doc);
|
||||
|
||||
const widgetPosition = node.node.lastChild.from;
|
||||
@@ -431,7 +431,7 @@ export function customHighlightPlugin(renderer, tab) {
|
||||
const to = line.from + token.to;
|
||||
|
||||
const attrs = {};
|
||||
if(token.type === 'Image' && token.url) {
|
||||
if(token.type === 'Image' && token.url && settings.showImagePreviews) {
|
||||
|
||||
attrs['data-url'] = token.url;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint max-lines: ["error", { "max": 300 }] */
|
||||
import { keymap } from '@codemirror/view';
|
||||
import { undo, redo, indentMore, indentLess, deleteLine } from '@codemirror/commands';
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import { EditorSelection, ChangeSet } from '@codemirror/state';
|
||||
import { Prec } from '@codemirror/state';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import * as postcssPlugin from 'prettier/plugins/postcss';
|
||||
@@ -14,33 +14,30 @@ export async function formatCSS(view) {
|
||||
const code = empty ? fullDoc : selection;
|
||||
|
||||
let formatted = await prettier.format(code, {
|
||||
parser: 'css',
|
||||
plugins: [postcssPlugin],
|
||||
parser : 'css',
|
||||
plugins : [postcssPlugin],
|
||||
|
||||
// formatting options
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
printWidth: 100,
|
||||
singleQuote: false,
|
||||
trailingComma: 'all',
|
||||
bracketSpacing: true,
|
||||
endOfLine: 'lf'
|
||||
tabWidth : 2,
|
||||
useTabs : false,
|
||||
printWidth : 100,
|
||||
singleQuote : false,
|
||||
trailingComma : 'all',
|
||||
bracketSpacing : true,
|
||||
endOfLine : 'lf'
|
||||
});
|
||||
|
||||
//format manually single declaration rules to span one line.
|
||||
//Prettier can't do it by default, this is crude but it works
|
||||
formatted = formatted.replace(
|
||||
/([^{]+)\{\s*\n\s*([^;\n]+:[^;\n]+;)\s*\n\s*\}(\s*)/g,
|
||||
(_, selector, decl, whitespace) =>
|
||||
`${selector} { ${decl.trim()} }${whitespace}`
|
||||
/([^{]+)\{\s*\n\s*([^;\n]+:[^;\n]+;)\s*\n\s*\}(\s*)/g,
|
||||
(_, selector, decl, whitespace)=>`${selector} { ${decl.trim()} }${whitespace}`
|
||||
);
|
||||
if(formatted === code) return true;
|
||||
|
||||
const dom = view.dom;
|
||||
dom.classList.add('cm-flash');
|
||||
|
||||
view.dom.classList.add('cm-flash');
|
||||
setTimeout(()=>{
|
||||
dom.classList.remove('cm-flash');
|
||||
view.dom.classList.remove('cm-flash');
|
||||
|
||||
view.dispatch({
|
||||
changes : {
|
||||
@@ -54,186 +51,172 @@ export async function formatCSS(view) {
|
||||
} catch (err) {
|
||||
console.error('Error formatting css: ', err);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const forEachSelection = (view, fn)=>{
|
||||
view.dispatch(view.state.changeByRange((range)=>fn(view.state, range.from, range.to, view.state.doc.sliceString(range.from, range.to))));
|
||||
return true;
|
||||
};
|
||||
|
||||
const insertTab = (view)=>{
|
||||
// If any selection spans multiple lines, delegates to CodeMirror's indentMore
|
||||
// Otherwise inserts two spaces at each cursor/selection
|
||||
// Otherwise inserts two spaces at each cursor/selection
|
||||
const shouldIndent = view.state.selection.ranges.some((range)=>view.state.doc.lineAt(range.from).number !==
|
||||
view.state.doc.lineAt(range.to).number
|
||||
);
|
||||
|
||||
if(shouldIndent) return indentMore(view);
|
||||
|
||||
const changes = [];
|
||||
|
||||
for (const range of view.state.selection.ranges) {
|
||||
changes.push({
|
||||
from : range.from,
|
||||
to : range.to,
|
||||
insert : ' ' // Insert two spaces, not a tab char!
|
||||
});
|
||||
}
|
||||
// Create a transaction so we can map old positions to
|
||||
// their new positions after the edits are applied
|
||||
const mappedChanges = view.state.update({ changes });
|
||||
|
||||
view.dispatch({
|
||||
changes,
|
||||
selection : EditorSelection.create(
|
||||
view.state.selection.ranges.map((range)=>EditorSelection.cursor(
|
||||
mappedChanges.changes.mapPos(range.from, -1) + 2
|
||||
)
|
||||
)
|
||||
)
|
||||
return forEachSelection(view, (state, from, to, selected)=>{
|
||||
return {
|
||||
changes : { from, to, insert: ' ' }, // Insert two spaces, not a tab char!
|
||||
range : EditorSelection.cursor(from + 2)
|
||||
};
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const wrapSelection = (prefix, suffix)=>(view)=>{
|
||||
const changes = [];
|
||||
const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view, (state, from, to, selected)=>{
|
||||
const noSelection = from === to;
|
||||
if(noSelection) {
|
||||
const insert = prefix + suffix;
|
||||
|
||||
for (const range of view.state.selection.ranges) {
|
||||
const { from, to } = range;
|
||||
const selected = view.state.doc.sliceString(from, to);
|
||||
|
||||
let text;
|
||||
|
||||
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 });
|
||||
return {
|
||||
changes : { from, to, insert },
|
||||
range : EditorSelection.cursor(from + prefix.length)
|
||||
};
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
changes
|
||||
});
|
||||
const hasWrapper = selected.startsWith(prefix) && selected.endsWith(suffix);
|
||||
if(hasWrapper) {
|
||||
return {
|
||||
changes : [
|
||||
{ from: to - suffix.length, to, insert: '' },
|
||||
{ from, to: from + prefix.length, insert: '' }
|
||||
],
|
||||
range : EditorSelection.range(from, to - prefix.length - suffix.length)
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const makeNbsp = (view)=>{
|
||||
const { from } = view.state.selection.main;
|
||||
|
||||
const prev2 = from >= 2
|
||||
? view.state.doc.sliceString(from - 2, from)
|
||||
: '';
|
||||
return {
|
||||
changes : { from, to, insert: prefix + selected + suffix },
|
||||
range : EditorSelection.range(from, to + prefix.length + suffix.length)
|
||||
};
|
||||
});
|
||||
|
||||
const makeNbsp = (view)=>forEachSelection(view, (state, from, to, selected)=>{
|
||||
const prev2 = from >= 2 ? state.doc.sliceString(from - 2, from) : '';
|
||||
const insert = (prev2 === ':>' || prev2 === '>>') ? '>' : ':>';
|
||||
|
||||
view.dispatch({
|
||||
changes : { from, to: from, insert },
|
||||
selection : { anchor: from + insert.length },
|
||||
});
|
||||
return {
|
||||
changes : { from, to, insert },
|
||||
range : EditorSelection.cursor(from + insert.length)
|
||||
};
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const makeSpace = (view)=>{
|
||||
const { from, to } = view.state.selection.main;
|
||||
const selected = view.state.doc.sliceString(from, to);
|
||||
const makeSpace = (view)=>forEachSelection(view, (state, from, to, selected)=>{
|
||||
let insert = '{{width:10% }}';
|
||||
const match = selected.match(/^{{width:(\d+)% }}$/);
|
||||
let newText = '{{width:10% }}';
|
||||
if(match) {
|
||||
const percent = Math.min(parseInt(match[1], 10) + 10, 100);
|
||||
newText = `{{width:${percent}% }}`;
|
||||
insert = `{{width:${percent}% }}`;
|
||||
}
|
||||
view.dispatch({ changes: { from, to, insert: newText } });
|
||||
return true;
|
||||
};
|
||||
return {
|
||||
changes : { from, to, insert },
|
||||
range : EditorSelection.range(from, from + insert.length)
|
||||
};
|
||||
});
|
||||
|
||||
const removeSpace = (view)=>{
|
||||
const { from, to } = view.state.selection.main;
|
||||
const selected = view.state.doc.sliceString(from, to);
|
||||
const removeSpace = (view)=>forEachSelection(view, (state, from, to, selected)=>{
|
||||
const match = selected.match(/^{{width:(\d+)% }}$/);
|
||||
if(match) {
|
||||
const percent = parseInt(match[1], 10) - 10;
|
||||
const newText = percent > 0 ? `{{width:${percent}% }}` : '';
|
||||
view.dispatch({ changes: { from, to, insert: newText } });
|
||||
const insert = percent > 0 ? `{{width:${percent}% }}` : '';
|
||||
return { changes: { from, to, insert: insert }, range: EditorSelection.range(from, from + insert.length) };
|
||||
}
|
||||
return true;
|
||||
};
|
||||
});
|
||||
|
||||
const makeSpan = (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 } });
|
||||
return true;
|
||||
};
|
||||
|
||||
const makeDiv = (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)
|
||||
: `{{\n${selected}\n}}`;
|
||||
view.dispatch({ changes: { from, to, insert: text } });
|
||||
return true;
|
||||
};
|
||||
|
||||
const makeComment = (view)=>{
|
||||
const { from, to } = view.state.selection.main;
|
||||
const selected = view.state.doc.sliceString(from, to);
|
||||
const isHtmlComment = selected.startsWith('<!--') && selected.endsWith('-->');
|
||||
const text = isHtmlComment
|
||||
? selected.slice(4, -3)
|
||||
: `<!-- ${selected} -->`;
|
||||
view.dispatch({ changes: { from, to, insert: text } });
|
||||
return true;
|
||||
};
|
||||
|
||||
const makeLink = (view)=>{
|
||||
const { from, to } = view.state.selection.main;
|
||||
const selected = view.state.doc.sliceString(from, to).trim();
|
||||
const isLink = /^\[(.*)\]\((.*)\)$/.exec(selected);
|
||||
const text = isLink ? `${isLink[1]} ${isLink[2]}` : `[${selected || 'alt text'}](url)`;
|
||||
view.dispatch({ changes: { from, to, insert: text } });
|
||||
return true;
|
||||
};
|
||||
|
||||
const makeList = (type)=>(view)=>{
|
||||
const { from, to } = view.state.selection.main;
|
||||
const lines = [];
|
||||
for (let l = from; l <= to; l++) {
|
||||
const lineText = view.state.doc.line(l + 1).text;
|
||||
lines.push(lineText);
|
||||
const makeLink = (view)=>forEachSelection(view, (state, from, to, selected)=>{
|
||||
// If the selection is already a Markdown link, unwrap it
|
||||
const existingLink = /^\[(.*)\]\((.*)\)$/.exec(selected);
|
||||
if(existingLink) {
|
||||
const [, text, url] = existingLink;
|
||||
const insert = `${text} ${url}`;
|
||||
return {
|
||||
changes : { from, to, insert },
|
||||
range : EditorSelection.range(
|
||||
from,
|
||||
from + insert.length
|
||||
),
|
||||
};
|
||||
}
|
||||
const joined = lines.join('\n');
|
||||
let newText;
|
||||
if(type === 'UL') newText = joined.replace(/^/gm, '- ');
|
||||
else newText = joined.replace(/^/gm, (m, i)=>`${i + 1}. `);
|
||||
view.dispatch({ changes: { from, to, insert: newText } });
|
||||
return true;
|
||||
};
|
||||
|
||||
const makeHeader = (level)=>(view)=>{
|
||||
const { from, to } = view.state.selection.main;
|
||||
const selected = view.state.doc.sliceString(from, to);
|
||||
const text = `${'#'.repeat(level)} ${selected}`;
|
||||
view.dispatch({ changes: { from, to, insert: text } });
|
||||
return true;
|
||||
};
|
||||
// If the selection is a URL, use its domain as the link text.
|
||||
const isUrl =
|
||||
/^(https?:\/\/|www\.)\S+$/i.test(selected) ||
|
||||
/^[a-z0-9-]+(\.[a-z0-9-]+)+([/?#]\S*)?$/i.test(selected);
|
||||
if(isUrl) {
|
||||
const domain = selected
|
||||
.replace(/^https?:\/\//i, '')
|
||||
.replace(/^www\./i, '')
|
||||
.split(/[/?#]/)[0];
|
||||
|
||||
const newColumn = (view)=>{
|
||||
const { from, to } = view.state.selection.main;
|
||||
view.dispatch({ changes: { from, to, insert: '\n\\column\n\n' } });
|
||||
return true;
|
||||
};
|
||||
const name = domain.split('.')[0];
|
||||
const insert = `[${name}](${selected})`;
|
||||
return {
|
||||
changes : { from, to, insert },
|
||||
range : EditorSelection.range(from, from + insert.length),
|
||||
};
|
||||
}
|
||||
|
||||
const newPage = (view)=>{
|
||||
const { from, to } = view.state.selection.main;
|
||||
view.dispatch({ changes: { from, to, insert: '\n\\page\n\n' } });
|
||||
return true;
|
||||
};
|
||||
// If the selection is just text, use it as alt text and set url as placeholder
|
||||
const insert = `[${selected || 'alt text'}](url)`;
|
||||
return {
|
||||
changes : { from, to, insert },
|
||||
range : EditorSelection.range(from, from + insert.length),
|
||||
};
|
||||
});
|
||||
|
||||
const addPrefixAtLineStart = (prefix)=>(view)=>forEachSelection(view, (state, from, to, selected)=>{
|
||||
const startLine = state.doc.lineAt(from);
|
||||
const endLine = state.doc.lineAt(to);
|
||||
const selectedLines = [];
|
||||
|
||||
for (let lineNumber = startLine.number; lineNumber <= endLine.number; lineNumber++) { selectedLines.push(state.doc.line(lineNumber)); }
|
||||
|
||||
const allLinesHavePrefix = selectedLines.every((line)=>line.text.startsWith(prefix));
|
||||
|
||||
const changes = selectedLines.map((line)=>{
|
||||
const shouldRemovePrefix = allLinesHavePrefix && line.text.startsWith(prefix);
|
||||
|
||||
return {
|
||||
from : line.from,
|
||||
to : shouldRemovePrefix
|
||||
? line.from + prefix.length
|
||||
: line.from,
|
||||
insert : shouldRemovePrefix ? '' : prefix,
|
||||
};
|
||||
});
|
||||
const changeSet = ChangeSet.of(changes, state.doc.length);
|
||||
|
||||
return {
|
||||
changes,
|
||||
range : EditorSelection.range(
|
||||
changeSet.mapPos(from, -1),
|
||||
changeSet.mapPos(to, 1)
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const newBreak = (type)=>(view)=>forEachSelection(view, (state, from, to, selected)=>{
|
||||
const insert = `\n\\${type}\n\n`;
|
||||
return {
|
||||
changes : { from, to, insert },
|
||||
range : EditorSelection.cursor(from + insert.length)
|
||||
};
|
||||
});
|
||||
|
||||
export const generalKeymap = Prec.high(keymap.of([
|
||||
{ key: 'Tab', run: insertTab }, //runs indentMore if multiple lines selected in a single selection
|
||||
{ key: 'Tab', run: insertTab },
|
||||
{ key: 'Shift-Tab', run: indentLess },
|
||||
{ key: 'Mod-z', run: undo }, //it may be unnecessary
|
||||
{ key: 'Mod-Shift-z', run: redo },
|
||||
@@ -247,7 +230,6 @@ export const cssKeymap = Prec.highest(keymap.of([
|
||||
]));
|
||||
|
||||
export const markdownKeymap = Prec.highest(keymap.of([
|
||||
|
||||
{ key: 'Mod-b', run: wrapSelection('**', '**') }, // makeBold
|
||||
{ key: 'Mod-i', run: wrapSelection('*', '*') }, // makeItalic
|
||||
{ key: 'Mod-u', run: wrapSelection('<u>', '</u>') }, // makeUnderline
|
||||
@@ -256,18 +238,18 @@ export const markdownKeymap = Prec.highest(keymap.of([
|
||||
{ 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-m', run: wrapSelection('{{', '}}') },
|
||||
{ key: 'Shift-Mod-m', run: wrapSelection('{{\n', '\n}}') },
|
||||
{ key: 'Mod-/', run: wrapSelection('<!-- ', ' -->') },
|
||||
{ key: 'Mod-Shift-k', run: makeLink },
|
||||
{ key: 'Mod-Shift-u', run: addPrefixAtLineStart('- ') },
|
||||
{ key: 'Mod-Shift-o', run: addPrefixAtLineStart('1. ') },
|
||||
{ key: 'Shift-Mod-1', run: addPrefixAtLineStart('# ') },
|
||||
{ key: 'Shift-Mod-2', run: addPrefixAtLineStart('## ') },
|
||||
{ key: 'Shift-Mod-3', run: addPrefixAtLineStart('### ') },
|
||||
{ key: 'Shift-Mod-4', run: addPrefixAtLineStart('#### ') },
|
||||
{ key: 'Shift-Mod-5', run: addPrefixAtLineStart('##### ') },
|
||||
{ key: 'Shift-Mod-6', run: addPrefixAtLineStart('###### ') },
|
||||
{ key: 'Mod-Enter', run: newBreak('page') },
|
||||
{ key: 'Shift-Mod-Enter', run: newBreak('column') },
|
||||
]));
|
||||
|
||||
@@ -7,6 +7,7 @@ const Combobox = createReactClass({
|
||||
displayName : 'Combobox',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
id : '',
|
||||
className : '',
|
||||
trigger : 'hover',
|
||||
default : '',
|
||||
@@ -75,6 +76,7 @@ const Combobox = createReactClass({
|
||||
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||
{...(this.props.tooltip ? { 'data-tooltip-right': this.props.tooltip } : {})}>
|
||||
<input
|
||||
id={this.props.id}
|
||||
type='text'
|
||||
onChange={(e)=>this.handleInput(e)}
|
||||
value={this.state.value || ''}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
.item i {
|
||||
position : absolute;
|
||||
right : 10px;
|
||||
color : black;
|
||||
color : inherit;
|
||||
}
|
||||
.dropdown-options {
|
||||
position : absolute;
|
||||
@@ -31,15 +31,12 @@
|
||||
font-family : 'Open Sans';
|
||||
font-size : 11px;
|
||||
cursor : default;
|
||||
&:hover {
|
||||
background-color : rgb(163, 163, 163);
|
||||
filter : brightness(120%);
|
||||
}
|
||||
&:hover { background-color : #DDDDDD; }
|
||||
.detail {
|
||||
width : 100%;
|
||||
font-size : 9px;
|
||||
font-style : italic;
|
||||
color : rgb(124, 124, 124);
|
||||
color : #7C7C7C;
|
||||
text-align : left;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* A dropdown menu component that uses the Anchor Positioning API to position the elements. It supports nested submenus as well.
|
||||
* Anchor Positioning is now supported in all major browsers. A polyfill is conditionally loaded for older browsers.
|
||||
*
|
||||
*
|
||||
* As-is, the menus will always open down aligned on left to trigger, submenus open to the right initially.
|
||||
* If no space, menus will still open down, but aligned to the right of the trigger. Submenus will flip to the other side of the top menu.
|
||||
* This could be customized either in more specific CSS, or as a `direction` prop on the component (in future iterations).
|
||||
*
|
||||
*
|
||||
* @param {string} props.groupName - Name of the menu. Appears as the trigger text.
|
||||
* @param {string} [props.icon] - Icon to display in the trigger.
|
||||
* @param {string} [props.color] - Color class to add to the trigger.
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
@property --menuColor {
|
||||
syntax: '<color>';
|
||||
inherits: true;
|
||||
initial-value: #DDD;
|
||||
syntax : '<color>';
|
||||
inherits : true;
|
||||
initial-value : #DDDDDD;
|
||||
}
|
||||
|
||||
@property --activeTriggerColor {
|
||||
syntax: '<color>';
|
||||
inherits: true;
|
||||
initial-value: #DDD;
|
||||
syntax : '<color>';
|
||||
inherits : true;
|
||||
initial-value : #999999;
|
||||
}
|
||||
|
||||
:root{
|
||||
--activeTriggerColor : var(--activeTriggerColor);
|
||||
}
|
||||
:root { --activeTriggerColor : var(--activeTriggerColor); }
|
||||
.menu-list {
|
||||
contain : content;
|
||||
position : fixed;
|
||||
top : anchor(bottom);
|
||||
left : anchor(left);
|
||||
position-try: flip-inline flip-block;
|
||||
color: inherit; // [popover] gets a `canvastext` color value from useragent.
|
||||
background: var(--menuColor);
|
||||
position-try : flip-inline flip-block;
|
||||
contain : content;
|
||||
color : inherit; // [popover] gets a `canvastext` color value from useragent.
|
||||
background : var(--menuColor);
|
||||
li > .menu-list {
|
||||
margin: 0 0px;
|
||||
top : anchor(top);
|
||||
left : anchor(right);
|
||||
position-try: flip-inline;
|
||||
}
|
||||
top : anchor(top);
|
||||
left : anchor(right);
|
||||
margin : 0 0px;
|
||||
position-try : flip-inline;
|
||||
}
|
||||
}
|
||||
.menu-wrapper:has(:popover-open) > button { // if menu is open...
|
||||
background-color: var(--activeTriggerColor, hsl(from var(--menuColor) h s calc(l * .85))); // tint menu triggers based on menu color
|
||||
background-color : var(--activeTriggerColor, hsl(from var(--menuColor) h s calc(l * 0.85))); // tint menu triggers based on menu color
|
||||
}
|
||||
|
||||
.darkMode .menu-wrapper:has(:popover-open) > button { // if menu is open...
|
||||
--activeTriggerColor : #444444; // tint menu triggers based on menu color
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import React, { useState, useRef, useMemo, useEffect } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import MarkdownLegacy from '@shared/markdownLegacy.js';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
import ErrorBar from './errorBar/errorBar.jsx';
|
||||
import ToolBar from './toolBar/toolBar.jsx';
|
||||
|
||||
@@ -23,7 +23,6 @@ import safeHTML from './safeHTML.js';
|
||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||
const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
|
||||
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
|
||||
const PAGE_HEIGHT = 1056;
|
||||
|
||||
const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
|
||||
|
||||
@@ -42,36 +41,29 @@ const BrewPage = (props)=>{
|
||||
props = {
|
||||
contents : '',
|
||||
index : 0,
|
||||
hoisted : false,
|
||||
...props
|
||||
};
|
||||
const pageRef = useRef(null);
|
||||
const cleanText = safeHTML(props.contents);
|
||||
const pageNum = props.index + 1;
|
||||
|
||||
useEffect(()=>{
|
||||
if(!pageRef.current) return;
|
||||
|
||||
// Observer for tracking pages within the `.pages` div
|
||||
// Observer for tracking which pages are at least 30% visible in the iframe
|
||||
const visibleObserver = new IntersectionObserver(
|
||||
(entries)=>{
|
||||
entries.forEach((entry)=>{
|
||||
if(entry.isIntersecting)
|
||||
props.onVisibilityChange(props.index + 1, true, false); // add page to array of visible pages.
|
||||
else
|
||||
props.onVisibilityChange(props.index + 1, false, false);
|
||||
});
|
||||
},
|
||||
(entries)=>entries.forEach((entry)=>{
|
||||
props.onVisibilityChange(pageNum, entry.isIntersecting, false); // add/remove page from array of visible pages.
|
||||
}),
|
||||
{ threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds.
|
||||
);
|
||||
|
||||
// Observer for tracking the page at the center of the iframe.
|
||||
const centerObserver = new IntersectionObserver(
|
||||
(entries)=>{
|
||||
entries.forEach((entry)=>{
|
||||
if(entry.isIntersecting)
|
||||
props.onVisibilityChange(props.index + 1, true, true); // Set this page as the center page
|
||||
});
|
||||
},
|
||||
(entries)=>entries.forEach((entry)=>{
|
||||
if(entry.isIntersecting)
|
||||
props.onVisibilityChange(pageNum, true, true); // Set this page as the center page
|
||||
}),
|
||||
{ threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center
|
||||
);
|
||||
|
||||
@@ -92,7 +84,7 @@ const BrewPage = (props)=>{
|
||||
|
||||
//v=====--------------------< Brew Renderer Component >-------------------=====v//
|
||||
let renderedPages = [];
|
||||
let pageTemplates = [];
|
||||
const pageTemplates = [];
|
||||
let rawPages = [];
|
||||
|
||||
const BrewRenderer = (props)=>{
|
||||
@@ -100,22 +92,23 @@ const BrewRenderer = (props)=>{
|
||||
text : '',
|
||||
style : '',
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB',
|
||||
lang : '',
|
||||
errors : [],
|
||||
currentEditorCursorPageNum : 1,
|
||||
currentEditorViewPageNum : 1,
|
||||
currentBrewRendererPageNum : 1,
|
||||
themeBundle : {},
|
||||
onPageChange : ()=>{},
|
||||
...props
|
||||
};
|
||||
|
||||
const pagesRef = useRef(null);
|
||||
|
||||
const [visiblePages, setVisiblePages] = useState([]);
|
||||
const [centerPage, setCenterPage] = useState(1);
|
||||
const [headerState, setHeaderState] = useState(false);
|
||||
|
||||
const [state, setState] = useState({
|
||||
isMounted : false,
|
||||
visibility : 'hidden',
|
||||
visiblePages : [],
|
||||
centerPage : 1
|
||||
isMounted : false,
|
||||
visibility : 'hidden'
|
||||
});
|
||||
|
||||
const [displayOptions, setDisplayOptions] = useState({
|
||||
@@ -133,12 +126,6 @@ const BrewRenderer = (props)=>{
|
||||
toolbarState && setDisplayOptions(toolbarState);
|
||||
}, []);
|
||||
|
||||
const [headerState, setHeaderState] = useState(false);
|
||||
|
||||
const mainRef = useRef(null);
|
||||
const pagesRef = useRef(null);
|
||||
const urlRef = useRef('');
|
||||
|
||||
if(props.renderer == 'legacy') {
|
||||
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
|
||||
} else {
|
||||
@@ -146,20 +133,16 @@ const BrewRenderer = (props)=>{
|
||||
}
|
||||
|
||||
const handlePageVisibilityChange = (pageNum, isVisible, isCenter)=>{
|
||||
setState((prevState)=>{
|
||||
const updatedVisiblePages = new Set(prevState.visiblePages);
|
||||
if(!isCenter)
|
||||
isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
visiblePages : [...updatedVisiblePages].sort((a, b)=>a - b),
|
||||
centerPage : isCenter ? pageNum : prevState.centerPage
|
||||
};
|
||||
setVisiblePages((prev)=>{
|
||||
const updatedVisiblePages = new Set(prev);
|
||||
isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum);
|
||||
return [...updatedVisiblePages].sort((a, b)=>a - b);
|
||||
});
|
||||
|
||||
if(isCenter)
|
||||
if(isCenter) {
|
||||
setCenterPage(pageNum);
|
||||
props.onPageChange(pageNum);
|
||||
}
|
||||
};
|
||||
|
||||
const isInView = (index)=>{
|
||||
@@ -169,17 +152,15 @@ const BrewRenderer = (props)=>{
|
||||
if(index == props.currentEditorCursorPageNum - 1) //Already rendered before this step
|
||||
return false;
|
||||
|
||||
if(Math.abs(index - props.currentBrewRendererPageNum - 1) <= 3)
|
||||
if(Math.abs(index - centerPage - 1) <= 3)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const renderDummyPage = (index)=>{
|
||||
return <div className='phb page' id={`p${index + 1}`} key={index}>
|
||||
<i className='fas fa-spinner fa-spin' />
|
||||
</div>;
|
||||
};
|
||||
const renderDummyPage = (index)=><div className='phb page' id={`p${index + 1}`} key={index}>
|
||||
<i className='fas fa-spinner fa-spin' />
|
||||
</div>;
|
||||
|
||||
const renderStyle = ()=>{
|
||||
const themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>';
|
||||
@@ -237,9 +218,8 @@ const BrewRenderer = (props)=>{
|
||||
}
|
||||
};
|
||||
|
||||
const renderPages = (checkHoists = false)=>{
|
||||
|
||||
if(props.errors && props.errors.length)
|
||||
const renderPages = ()=>{
|
||||
if(props.errors?.length)
|
||||
return renderedPages;
|
||||
|
||||
if(rawPages.length != renderedPages.length) { // Re-render all pages when page count changes
|
||||
@@ -252,16 +232,10 @@ const BrewRenderer = (props)=>{
|
||||
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
|
||||
|
||||
_.forEach(rawPages, (page, index)=>{
|
||||
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'){
|
||||
if((isInView(index) || !renderedPages[index]) && 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;
|
||||
};
|
||||
|
||||
@@ -300,8 +274,8 @@ const BrewRenderer = (props)=>{
|
||||
|
||||
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(true); //Make sure page is renderable before showing
|
||||
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
|
||||
setState((prevState)=>({
|
||||
...prevState,
|
||||
isMounted : true,
|
||||
@@ -327,7 +301,7 @@ const BrewRenderer = (props)=>{
|
||||
};
|
||||
|
||||
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
|
||||
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);
|
||||
renderedPages = useMemo(()=>renderPages(), [props.text, centerPage, displayOptions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -341,19 +315,19 @@ const BrewRenderer = (props)=>{
|
||||
: null}
|
||||
|
||||
<ErrorBar errors={props.errors} />
|
||||
<div className='popups' ref={mainRef}>
|
||||
<div className='popups'>
|
||||
<RenderWarnings />
|
||||
<NotificationPopup />
|
||||
</div>
|
||||
|
||||
<ToolBar displayOptions={displayOptions} onDisplayOptionsChange={handleDisplayOptionsChange} visiblePages={state.visiblePages.length > 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/>
|
||||
<ToolBar displayOptions={displayOptions} onDisplayOptionsChange={handleDisplayOptionsChange} visiblePages={visiblePages.length > 0 ? visiblePages : [centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/>
|
||||
|
||||
{/*render in iFrame so broken code doesn't crash the site.*/}
|
||||
<Frame id='BrewRenderer' title="Rendered Brew Content" initialContent={INITIAL_CONTENT}
|
||||
<Frame id='BrewRenderer' title='Rendered Brew Content' initialContent={INITIAL_CONTENT}
|
||||
style={{ width: '100%', height: '100%', visibility: state.visibility }}
|
||||
contentDidMount={frameDidMount}
|
||||
onClick={()=>{emitClick();}}
|
||||
sandbox="allow-same-origin allow-modals allow-top-navigation"
|
||||
onClick={emitClick}
|
||||
sandbox='allow-same-origin allow-modals allow-top-navigation'
|
||||
>
|
||||
<div className='brewRenderer'
|
||||
onKeyDown={handleControlKeys}
|
||||
|
||||
@@ -61,9 +61,7 @@
|
||||
}
|
||||
|
||||
@supports (break-after:always) {
|
||||
.columnSplit {
|
||||
margin-bottom: 100vh;
|
||||
}
|
||||
.columnSplit { margin-bottom : 100vh; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,13 +74,11 @@
|
||||
height : 100%;
|
||||
padding : unset;
|
||||
overflow-y : unset;
|
||||
&:has(.facing, .flow) {
|
||||
padding : unset;
|
||||
}
|
||||
&:has(.facing, .flow) { padding : unset; }
|
||||
.pages {
|
||||
display : block;
|
||||
margin : 0px;
|
||||
zoom : 100% !important;
|
||||
display : block;
|
||||
& > .page { box-shadow : unset; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './notificationPopup.less';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
|
||||
import Dialog from '@components/dialog.jsx';
|
||||
|
||||
|
||||
@@ -99,13 +99,13 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
return (
|
||||
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
||||
<div className='toggleButton'>
|
||||
<button data-tooltip-right={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`}
|
||||
aria-label={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`}
|
||||
<button data-tooltip-right={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`}
|
||||
aria-label={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`}
|
||||
onClick={()=>{ setToolsVisible(!toolsVisible); localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible); }}>
|
||||
<i aria-hidden='true' className='fas fa-glasses' />
|
||||
</button>
|
||||
<button data-tooltip-right={`${headerState ? 'Hide' : 'Show'} Header Navigation`}
|
||||
aria-label={`${headerState ? 'Hide' : 'Show'} Header Navigation`}
|
||||
<button data-tooltip-right={`${headerState ? 'Hide' : 'Show'} Header Navigation`}
|
||||
aria-label={`${headerState ? 'Hide' : 'Show'} Header Navigation`}
|
||||
onClick={()=>{setHeaderState(!headerState);}}>
|
||||
<i aria-hidden='true' className='fas fa-rectangle-list' />
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import (less) './client/icons/customIcons.less';
|
||||
@import (less) '../../../icons/customIcons.less';
|
||||
|
||||
.toolBar {
|
||||
position : absolute;
|
||||
@@ -176,9 +176,7 @@
|
||||
transition : all 0.2s ease;
|
||||
}
|
||||
|
||||
.toggleButton button i {
|
||||
filter: drop-shadow(0 0 2px black) drop-shadow(0 0 1px black);
|
||||
}
|
||||
.toggleButton button i { filter : drop-shadow(0 0 2px black) drop-shadow(0 0 1px black); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
import './editor.less';
|
||||
import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import dedent from 'dedent';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
import CodeEditor from '@components/codeEditor/codeEditor.jsx';
|
||||
import SnippetBar from './snippetbar/snippetbar.jsx';
|
||||
import MetadataEditor from './metadataEditor/metadataEditor.jsx';
|
||||
import SettingsEditor from './settingsEditor/settingsEditor.jsx';
|
||||
|
||||
const EDITOR_THEME_KEY = 'HB_editor_theme';
|
||||
const EDITOR_SETTINGS_KEY = 'HB_edit_settings';
|
||||
|
||||
import defaultCM5Theme from '@themes/codeMirror/default.js';
|
||||
import darkbrewery from '@themes/codeMirror/darkbrewery.js';
|
||||
@@ -19,6 +22,20 @@ const EditorThemes = Object.entries(themes)
|
||||
.filter(([name, value])=>Array.isArray(value) && !name.endsWith('Init') && !name.endsWith('Style'))
|
||||
.map(([name])=>name);
|
||||
|
||||
const themeNames = Object.entries(themes)
|
||||
.filter(([name, value])=>Array.isArray(value) &&
|
||||
!name.endsWith('Init') &&
|
||||
!name.endsWith('Style')
|
||||
)
|
||||
.map(([name])=>name);
|
||||
|
||||
const EditorThemeNameList = [
|
||||
'default',
|
||||
...themeNames
|
||||
.filter((name)=>name !== 'default')
|
||||
.sort((a, b)=>a.localeCompare(b))
|
||||
];
|
||||
|
||||
//const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||
//const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
|
||||
const DEFAULT_STYLE_TEXT = dedent`
|
||||
@@ -50,7 +67,6 @@ const Editor = forwardRef(
|
||||
onCursorPageChange = ()=>{},
|
||||
onViewPageChange = ()=>{},
|
||||
|
||||
editorTheme = 'default',
|
||||
renderer = 'legacy',
|
||||
|
||||
moveBrew,
|
||||
@@ -69,9 +85,17 @@ const Editor = forwardRef(
|
||||
},
|
||||
ref,
|
||||
)=>{
|
||||
const [currentEditorTheme, setEditorTheme] = useState(editorTheme);
|
||||
const [view, setView] = useState('text'); // 'text', 'style', 'meta', 'snippet'
|
||||
const [snippetBarHeight, setSnippetBarHeight] = useState(26);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
const [editorSettings, setEditorSettings] = useState({
|
||||
autoCloseBrackets : true,
|
||||
showImagePreviews : true,
|
||||
activeLineShading : true,
|
||||
lineNumbers : true,
|
||||
fontSize : 1,
|
||||
editorTheme : 'default',
|
||||
});
|
||||
|
||||
const editor = useRef(null);
|
||||
const codeEditor = useRef(null);
|
||||
@@ -81,6 +105,7 @@ const Editor = forwardRef(
|
||||
const isStyle = ()=>isView('style');
|
||||
const isMeta = ()=>isView('meta');
|
||||
const isSnip = ()=>isView('snippet');
|
||||
const isSettings = ()=>isView('settings');
|
||||
|
||||
const isView = (name)=>view === name;
|
||||
|
||||
@@ -89,8 +114,12 @@ const Editor = forwardRef(
|
||||
brewRenderer.onload = ()=>brewRenderer.contentDocument?.addEventListener('keydown', handleControlKeys);
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
|
||||
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||
if(editorTheme && EditorThemes.includes(editorTheme)) setEditorTheme(editorTheme); else setEditorTheme('default');
|
||||
const localEditorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||
if(localEditorTheme && EditorThemes.includes(localEditorTheme)) {
|
||||
setEditorSettings({ ...editorSettings, editorTheme: localEditorTheme });
|
||||
} else setEditorSettings({ ...editorSettings, editorTheme: 'default' });
|
||||
const localEditorSettings = window.localStorage.getItem(EDITOR_SETTINGS_KEY);
|
||||
if(localEditorSettings) setEditorSettings(JSON.parse(localEditorSettings));
|
||||
const snippetBar = document.querySelector('.editor > .snippetBar');
|
||||
if(!snippetBar) return;
|
||||
|
||||
@@ -111,7 +140,7 @@ const Editor = forwardRef(
|
||||
useEffect(()=>{ if(liveScroll) brewJump(currentEditorViewPageNum, false); }, [currentEditorViewPageNum, liveScroll]);
|
||||
useEffect(()=>{ if(liveScroll) brewJump(currentEditorCursorPageNum, false); }, [currentEditorCursorPageNum, liveScroll]);
|
||||
|
||||
const handleFormatCode = () => {
|
||||
const handleFormatCode = ()=>{
|
||||
codeEditor.current?.formatCode();
|
||||
};
|
||||
|
||||
@@ -211,7 +240,12 @@ const Editor = forwardRef(
|
||||
|
||||
const updateEditorTheme = (newTheme)=>{
|
||||
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme);
|
||||
setEditorTheme(newTheme);
|
||||
setEditorSettings({ ...editorSettings, editorTheme: newTheme });
|
||||
};
|
||||
|
||||
const updateEditorSettings = (newEditorSettings)=>{
|
||||
window.localStorage.setItem(EDITOR_SETTINGS_KEY, JSON.stringify(newEditorSettings));
|
||||
setEditorSettings(newEditorSettings);
|
||||
};
|
||||
|
||||
const renderEditor = ()=>{
|
||||
@@ -228,9 +262,11 @@ const Editor = forwardRef(
|
||||
onChange={onBrewChange('text')}
|
||||
onCursorChange={(page)=>updateCurrentCursorPage(page)}
|
||||
onViewChange={(page)=>updateCurrentViewPage(page)}
|
||||
editorTheme={currentEditorTheme}
|
||||
editorTheme={editorSettings.editorTheme}
|
||||
onThemeChange={setIsDark}
|
||||
renderer={brew.renderer}
|
||||
style={{ height: `calc(100% - ${snippetBarHeight}px)` }}
|
||||
settings={editorSettings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -246,9 +282,11 @@ const Editor = forwardRef(
|
||||
view={view}
|
||||
value={brew.style ?? DEFAULT_STYLE_TEXT}
|
||||
onChange={onBrewChange('style')}
|
||||
editorTheme={currentEditorTheme}
|
||||
editorTheme={editorSettings.editorTheme}
|
||||
onThemeChange={setIsDark}
|
||||
renderer={brew.renderer}
|
||||
style={{ height: `calc(100% - ${snippetBarHeight}px)` }}
|
||||
settings={editorSettings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -268,9 +306,11 @@ const Editor = forwardRef(
|
||||
value={brew.snippets}
|
||||
onChange={onBrewChange('snippets')}
|
||||
enableFolding={true}
|
||||
editorTheme={currentEditorTheme}
|
||||
editorTheme={editorSettings.editorTheme}
|
||||
onThemeChange={setIsDark}
|
||||
renderer={brew.renderer}
|
||||
style={{ height: `calc(100% - 25px)` }}
|
||||
settings={editorSettings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -278,7 +318,7 @@ const Editor = forwardRef(
|
||||
if(isMeta()) {
|
||||
return (
|
||||
<>
|
||||
<CodeEditor key='codeEditor' view={view} style={{ display: 'none' }} />
|
||||
<CodeEditor key='codeEditor' tab='brewMetadata' editorTheme={editorSettings.editorTheme} onThemeChange={setIsDark} view={view} style={{ display: 'none' }} settings={editorSettings} />
|
||||
<MetadataEditor
|
||||
metadata={brew}
|
||||
themeBundle={themeBundle}
|
||||
@@ -289,6 +329,26 @@ const Editor = forwardRef(
|
||||
</>
|
||||
);
|
||||
}
|
||||
if(isSettings()){
|
||||
return (
|
||||
<>
|
||||
<CodeEditor
|
||||
key='codeEditor'
|
||||
tab='brewSettings' //necessary or the brew object loses its contents, culprit possibly on the tab dependent useEffect in codeEditor.jsx
|
||||
view={view}
|
||||
style={{ display: 'none' }}
|
||||
editorTheme={editorSettings.editorTheme}
|
||||
onThemeChange={setIsDark}
|
||||
settings={editorSettings}
|
||||
/>
|
||||
<SettingsEditor
|
||||
settings={editorSettings}
|
||||
EditorThemeNameList={EditorThemeNameList}
|
||||
updateSettings={updateEditorSettings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const redo = ()=>codeEditor.current?.redo();
|
||||
@@ -308,9 +368,8 @@ const Editor = forwardRef(
|
||||
unfoldCode,
|
||||
historySize,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className='editor' ref={editor}>
|
||||
<div className={`editor${isDark ? ' darkMode' : ''}`} ref={editor}>
|
||||
<SnippetBar
|
||||
brew={brew}
|
||||
view={view}
|
||||
@@ -325,7 +384,7 @@ const Editor = forwardRef(
|
||||
unfoldCode={unfoldCode}
|
||||
formatCode={isStyle() ? handleFormatCode : null}
|
||||
historySize={historySize()}
|
||||
currentEditorTheme={currentEditorTheme}
|
||||
currentEditorTheme={editorSettings.editorTheme}
|
||||
updateEditorTheme={updateEditorTheme}
|
||||
themeBundle={themeBundle}
|
||||
cursorPos={codeEditor.current?.getCursorPosition() || {}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable max-lines */
|
||||
import './metadataEditor.less';
|
||||
import '../uiEditor.less';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import _ from 'lodash';
|
||||
@@ -7,7 +7,6 @@ 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 validations from './validations.js';
|
||||
|
||||
@@ -84,7 +83,6 @@ const MetadataEditor = createReactClass({
|
||||
return `- ${err}`;
|
||||
}).join('\n');
|
||||
|
||||
|
||||
debouncedReportValidity(e.target, errMessage);
|
||||
return false;
|
||||
}
|
||||
@@ -156,11 +154,11 @@ const MetadataEditor = createReactClass({
|
||||
|
||||
renderPublish : function(){
|
||||
if(this.props.metadata.published){
|
||||
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
||||
return <button id='publish-button' className='unpublish' onClick={()=>this.handlePublish(false)}>
|
||||
<i className='fas fa-ban' aria-hidden='true' /> unpublish
|
||||
</button>;
|
||||
} else {
|
||||
return <button className='publish' onClick={()=>this.handlePublish(true)}>
|
||||
return <button id='publish-button' className='publish' onClick={()=>this.handlePublish(true)}>
|
||||
<i className='fas fa-globe' aria-hidden='true' /> publish
|
||||
</button>;
|
||||
}
|
||||
@@ -170,9 +168,9 @@ const MetadataEditor = createReactClass({
|
||||
if(!this.props.metadata.editId) return;
|
||||
|
||||
return <div className='field delete'>
|
||||
<label>delete</label>
|
||||
<label htmlFor='delete-button'>delete</label>
|
||||
<div className='value'>
|
||||
<button className='publish' onClick={this.handleDelete}>
|
||||
<button id='delete-button' onClick={this.handleDelete}>
|
||||
<i className='fas fa-trash-alt' /> delete brew
|
||||
</button>
|
||||
</div>
|
||||
@@ -186,8 +184,8 @@ const MetadataEditor = createReactClass({
|
||||
<label>authors</label>
|
||||
<div className='value'>
|
||||
{authors.length > 0 && (
|
||||
<a href={`/user/${authors[0]}`} className='author-link' target="_blank" title={`Owner - Click to open ${authors[0]}'s profile in a new tab`}>
|
||||
{authors[0]}{authors.length > 1 && ', '}
|
||||
<a href={`/user/${authors[0]}`} className='author-link' target='_blank' title={`Owner - Click to open ${authors[0]}'s profile in a new tab`}>
|
||||
{authors[0]}{authors.length > 1 && ', '}
|
||||
</a>
|
||||
)}
|
||||
{authors.length > 1 && authors.slice(1).map((author, i)=>(
|
||||
@@ -227,7 +225,6 @@ const MetadataEditor = createReactClass({
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
renderThemeDropdown : function(){
|
||||
@@ -264,6 +261,7 @@ const MetadataEditor = createReactClass({
|
||||
dropdown =
|
||||
<div className='value' data-tooltip-top='Select from the list below (built-in themes and brews you have tagged "meta:theme"), or paste in the Share URL or Share ID of any brew.'>
|
||||
<Combobox trigger='click'
|
||||
id='combobox-themes'
|
||||
className='themes-dropdown'
|
||||
default={currentThemeDisplay}
|
||||
placeholder='Select from below, or enter the Share URL or ID of a brew with the meta:theme tag'
|
||||
@@ -284,7 +282,7 @@ const MetadataEditor = createReactClass({
|
||||
}
|
||||
|
||||
return <div className='field themes'>
|
||||
<label>theme</label>
|
||||
<label htmlFor='combobox-themes'>theme</label>
|
||||
{dropdown}
|
||||
</div>;
|
||||
},
|
||||
@@ -303,9 +301,10 @@ const MetadataEditor = createReactClass({
|
||||
};
|
||||
|
||||
return <div className='field language'>
|
||||
<label>language</label>
|
||||
<label htmlFor='combobox-language'>language</label>
|
||||
<div className='value' data-tooltip-right='Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.'>
|
||||
<Combobox trigger='click'
|
||||
id='combobox-language'
|
||||
className='language-dropdown'
|
||||
default={this.props.metadata.lang || ''}
|
||||
placeholder='en'
|
||||
@@ -355,24 +354,24 @@ const MetadataEditor = createReactClass({
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='metadataEditor'>
|
||||
return <div className='metadataEditor uiEditor'>
|
||||
<h1>Properties Editor</h1>
|
||||
|
||||
<div className='field title'>
|
||||
<label for='title_field'>title</label>
|
||||
<label htmlFor='title_field'>title</label>
|
||||
<input type='text' id='title_field' className='value'
|
||||
defaultValue={this.props.metadata.title}
|
||||
onChange={(e)=>this.handleFieldChange('title', e)} />
|
||||
</div>
|
||||
<div className='field-group'>
|
||||
<fieldset className='field-group'>
|
||||
<div className='field-column'>
|
||||
<div className='field description'>
|
||||
<label for='description_field'>description</label>
|
||||
<label htmlFor='description_field'>description</label>
|
||||
<textarea id='description_field' defaultValue={this.props.metadata.description} className='value'
|
||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||
</div>
|
||||
<div className='field thumbnail'>
|
||||
<label for='thumbnail_field'>thumbnail</label>
|
||||
<label htmlFor='thumbnail_field'>thumbnail</label>
|
||||
<input type='text'
|
||||
id='thumbnail_field'
|
||||
defaultValue={this.props.metadata.thumbnail}
|
||||
@@ -380,18 +379,19 @@ const MetadataEditor = createReactClass({
|
||||
className='value'
|
||||
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||
<button className='display' onClick={this.toggleThumbnailDisplay}
|
||||
aria-label={`${this.state.showThumbnail ? 'hide thumbnail' : 'show thumbnail'}`}>
|
||||
aria-label={`${this.state.showThumbnail ? 'hide thumbnail' : 'show thumbnail'}`}>
|
||||
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderThumbnail()}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div className='field tags'>
|
||||
<label>Tags</label>
|
||||
<label htmlFor='combobox-tags'>Tags</label>
|
||||
<div className='value' >
|
||||
<TagInput
|
||||
id='combobox-tags'
|
||||
label='tags'
|
||||
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
|
||||
placeholder='add tag' unique={true}
|
||||
@@ -402,7 +402,6 @@ const MetadataEditor = createReactClass({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{this.renderLanguageDropdown()}
|
||||
|
||||
{this.renderThemeDropdown()}
|
||||
@@ -414,9 +413,10 @@ const MetadataEditor = createReactClass({
|
||||
{this.renderAuthors()}
|
||||
|
||||
<div className='field invitedAuthors'>
|
||||
<label>Invited authors</label>
|
||||
<label htmlFor='combobox-invited-authors'>Invited authors</label>
|
||||
<div className='value'>
|
||||
<TagInput
|
||||
id='combobox-invited-authors'
|
||||
label='invited authors'
|
||||
valuePatterns={/.+/}
|
||||
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
||||
@@ -429,11 +429,10 @@ const MetadataEditor = createReactClass({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<h2>Privacy</h2>
|
||||
|
||||
<div className='field publish'>
|
||||
<label>publish</label>
|
||||
<label htmlFor='publish-button'>publish</label>
|
||||
<div className='value'>
|
||||
{this.renderPublish()}
|
||||
<small>Published brews are searchable in <a href='/vault'>the Vault</a> and visible on your user page. Unpublished brews are not indexed in the Vault or visible on your user page, but can still be shared and indexed by search engines. You can unpublish a brew any time.</small>
|
||||
|
||||
@@ -1,374 +0,0 @@
|
||||
@import '@sharedStyles/core.less';
|
||||
|
||||
.userThemeName {
|
||||
padding-right : 10px;
|
||||
padding-left : 10px;
|
||||
}
|
||||
|
||||
.metadataEditor {
|
||||
position : absolute;
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||
padding : 25px;
|
||||
overflow-y : auto;
|
||||
font-size : 13px;
|
||||
background-color : #999999;
|
||||
|
||||
h1 {
|
||||
margin : 0 0 40px;
|
||||
font-weight : bold;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin : 20px 0;
|
||||
font-weight : bold;
|
||||
color : #555555;
|
||||
border-bottom : 2px solid gray;
|
||||
}
|
||||
|
||||
& > div { margin-bottom : 10px; }
|
||||
|
||||
.field-group {
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
gap : 10px;
|
||||
width : 100%;
|
||||
}
|
||||
|
||||
.field-column {
|
||||
display : flex;
|
||||
flex : 5 0 200px;
|
||||
flex-direction : column;
|
||||
gap : 10px;
|
||||
}
|
||||
|
||||
.field {
|
||||
position : relative;
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
width : 100%;
|
||||
min-width : 200px;
|
||||
& > label {
|
||||
width : 80px;
|
||||
font-size : 0.9em;
|
||||
font-weight : 800;
|
||||
line-height : 1.8em;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
& > .value {
|
||||
flex : 1 1 auto;
|
||||
width : 50px;
|
||||
&[data-tooltip-right] { max-width : 380px; }
|
||||
&:invalid { background : #FFB9B9; }
|
||||
small {
|
||||
display : block;
|
||||
font-size : 0.9em;
|
||||
font-style : italic;
|
||||
line-height : 1.4em;
|
||||
}
|
||||
}
|
||||
input[type='text'], textarea {
|
||||
border : 1px solid gray;
|
||||
&:focus { outline : 1px solid #444444; }
|
||||
}
|
||||
|
||||
&.description {
|
||||
flex : 1;
|
||||
textarea.value {
|
||||
height : auto;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
resize : none;
|
||||
}
|
||||
}
|
||||
|
||||
&.thumbnail, &.themes {
|
||||
label { line-height : 2.0em; }
|
||||
.value {
|
||||
overflow : hidden;
|
||||
text-overflow : ellipsis;
|
||||
}
|
||||
button {
|
||||
.colorButton();
|
||||
padding : 0px 5px;
|
||||
color : white;
|
||||
background-color : black;
|
||||
border : 1px solid #999999;
|
||||
&:hover { background-color : #777777; }
|
||||
}
|
||||
}
|
||||
|
||||
&.tags .tagInput-dropdown {
|
||||
z-index : 400;
|
||||
max-width : 200px;
|
||||
}
|
||||
&.language .value {
|
||||
z-index : 300;
|
||||
max-width : 150px;
|
||||
}
|
||||
|
||||
&.themes {
|
||||
.value {
|
||||
overflow : visible;
|
||||
text-overflow : auto;
|
||||
}
|
||||
button {
|
||||
padding-right : 5px;
|
||||
padding-left : 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.invitedAuthors .value {
|
||||
z-index : 100;
|
||||
|
||||
.tagInput-dropdown { max-width : 200px; }
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail-preview {
|
||||
position : relative;
|
||||
flex : 1 1;
|
||||
justify-self : center;
|
||||
width : 80px;
|
||||
height : min-content;
|
||||
max-height : 115px;
|
||||
aspect-ratio : 1 / 1;
|
||||
object-fit : contain;
|
||||
background-color : #AAAAAA;
|
||||
}
|
||||
|
||||
.renderers.field .value {
|
||||
label {
|
||||
display : inline-flex;
|
||||
align-items : center;
|
||||
margin-right : 15px;
|
||||
font-size : 0.9em;
|
||||
font-weight : 800;
|
||||
vertical-align : middle;
|
||||
white-space : nowrap;
|
||||
cursor : pointer;
|
||||
user-select : none;
|
||||
}
|
||||
input {
|
||||
margin : 3px;
|
||||
vertical-align : middle;
|
||||
cursor : pointer;
|
||||
}
|
||||
}
|
||||
.publish.field .value {
|
||||
position : relative;
|
||||
margin-bottom : 15px;
|
||||
button { width : 100%; }
|
||||
button.publish {
|
||||
.colorButton(@blueLight);
|
||||
}
|
||||
button.unpublish {
|
||||
.colorButton(@silver);
|
||||
}
|
||||
}
|
||||
|
||||
.delete.field .value {
|
||||
button {
|
||||
.colorButton(@red);
|
||||
}
|
||||
}
|
||||
.authors.field {
|
||||
.tag {
|
||||
font-weight:300;
|
||||
transition:background-color 0.2s;
|
||||
|
||||
&.owner {
|
||||
position: relative;
|
||||
background-color:@silverLight;
|
||||
min-width:25px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
font-weight: 900;
|
||||
|
||||
&::after {
|
||||
content: "\f521";
|
||||
font-family: "Font Awesome 6 Free";
|
||||
color:gold;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width:15px;
|
||||
height:15px;
|
||||
rotate:-25deg;
|
||||
translate:-30% -50%;
|
||||
transform: scaleY(0.7);
|
||||
}
|
||||
}
|
||||
&:has(button) a {
|
||||
padding-right:5px;
|
||||
}
|
||||
&:has(button:hover) {
|
||||
background:#d97d7d;
|
||||
}
|
||||
|
||||
button {
|
||||
color:@red;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
a {
|
||||
color:black;
|
||||
text-underline-offset:0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.themes.field {
|
||||
& .dropdown-container {
|
||||
position : relative;
|
||||
z-index : 200;
|
||||
background-color : white;
|
||||
}
|
||||
& .dropdown-options { overflow-y : visible; }
|
||||
.disabled {
|
||||
font-style : italic;
|
||||
color : dimgray;
|
||||
background-color : darkgray;
|
||||
}
|
||||
.item {
|
||||
position : relative;
|
||||
padding : 3px 3px;
|
||||
overflow : visible;
|
||||
background-color : white;
|
||||
border-top : 1px solid rgb(118, 118, 118);
|
||||
.preview {
|
||||
position : absolute;
|
||||
top : 0;
|
||||
right : 0;
|
||||
z-index : 1;
|
||||
display : flex;
|
||||
flex-direction : column;
|
||||
width : 200px;
|
||||
overflow : hidden;
|
||||
color : black;
|
||||
background : #CCCCCC;
|
||||
border-radius : 5px;
|
||||
box-shadow : 0 0 5px black;
|
||||
opacity : 0;
|
||||
transition : opacity 250ms ease;
|
||||
h6 {
|
||||
padding-block : 0.5em;
|
||||
padding-inline : 1em;
|
||||
font-weight : 900;
|
||||
border-bottom : 2px solid hsl(0,0%,40%);
|
||||
}
|
||||
}
|
||||
|
||||
.texture-container {
|
||||
position : absolute;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
min-height : 100%;
|
||||
overflow : hidden;
|
||||
> img {
|
||||
position : absolute;
|
||||
top : 0;
|
||||
right : 0;
|
||||
width : 50%;
|
||||
min-height : 100%;
|
||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color : white;
|
||||
background-color : @blue;
|
||||
filter : unset;
|
||||
}
|
||||
&:hover > .preview { opacity : 1; }
|
||||
}
|
||||
}
|
||||
|
||||
.field .list {
|
||||
display : flex;
|
||||
flex : 1 0;
|
||||
flex-wrap : wrap;
|
||||
|
||||
> * { flex : 0 0 auto; }
|
||||
|
||||
#groupedIcon {
|
||||
#backgroundColors;
|
||||
position : relative;
|
||||
top : -0.3em;
|
||||
right : -0.3em;
|
||||
display : inline-block;
|
||||
min-width : 20px;
|
||||
height : ~'calc(100% + 0.6em)';
|
||||
color : white;
|
||||
text-align : center;
|
||||
cursor : pointer;
|
||||
|
||||
i {
|
||||
position : relative;
|
||||
top : 50%;
|
||||
transform : translateY(-50%);
|
||||
}
|
||||
|
||||
&:not(:last-child) { border-right : 1px solid black; }
|
||||
|
||||
&:last-child { border-radius : 0 0.5em 0.5em 0; }
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding : 0.35em;
|
||||
margin : 2px;
|
||||
font-size : 0.95em;
|
||||
background-color : #DDDDDD;
|
||||
border-radius : 0.5em;
|
||||
|
||||
.icon { #groupedIcon; }
|
||||
|
||||
button {
|
||||
cursor : pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
height : ~'calc(.9em + 4px + .6em)';
|
||||
|
||||
input { border-radius : 0.5em 0 0 0.5em; }
|
||||
|
||||
input:last-child { border-radius : 0.5em; }
|
||||
|
||||
.value {
|
||||
width : 7.5vw;
|
||||
min-width : 75px;
|
||||
height : 100%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
height : ~'calc(.9em + 4px + .6em)';
|
||||
|
||||
input { border-radius : 0.5em 0 0 0.5em; }
|
||||
|
||||
input:last-child { border-radius : 0.5em; }
|
||||
|
||||
.value {
|
||||
width : 7.5vw;
|
||||
min-width : 75px;
|
||||
height : 100%;
|
||||
}
|
||||
|
||||
.invalid:focus { background-color : pink; }
|
||||
|
||||
.icon {
|
||||
#groupedIcon;
|
||||
top : -0.54em;
|
||||
right : 1px;
|
||||
height : 97%;
|
||||
|
||||
i { font-size : 1.125em; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import '../uiEditor.less';
|
||||
import React from 'react';
|
||||
|
||||
const SettingsEditor = ({ settings, updateSettings = ()=>{}, EditorThemeNameList })=>{
|
||||
|
||||
const validations = {};
|
||||
|
||||
const handleFieldChange = (setting, e)=>{
|
||||
const value =
|
||||
e.target.type === 'checkbox'
|
||||
? e.target.checked
|
||||
: e.target.value;
|
||||
|
||||
const inputRules = validations[setting] ?? [];
|
||||
|
||||
const validationErrors = inputRules
|
||||
.map((rule)=>rule(value))
|
||||
.filter(Boolean);
|
||||
|
||||
if(validationErrors.length > 0) {
|
||||
e.target.setCustomValidity(validationErrors.join('\n'));
|
||||
e.target.reportValidity();
|
||||
return;
|
||||
}
|
||||
|
||||
e.target.setCustomValidity('');
|
||||
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
[setting] : e.target.type === 'number'
|
||||
? Number(value)
|
||||
: value,
|
||||
};
|
||||
|
||||
updateSettings(updatedSettings);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='settingsEditor uiEditor'>
|
||||
<h1>Editor Settings</h1>
|
||||
|
||||
<div className='field'>
|
||||
<label htmlFor='changeEditorTheme'>
|
||||
Select your Editor Theme
|
||||
</label>
|
||||
<div className='value'>
|
||||
<select id='changeEditorTheme' value={settings.editorTheme} onChange={(e)=>handleFieldChange('editorTheme', e)} >
|
||||
{EditorThemeNameList.map((theme, key)=>{
|
||||
return <option key={key} value={theme}>{theme}</option>;
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='field'>
|
||||
<label htmlFor='autoCloseBrackets'>
|
||||
Automatically close brackets
|
||||
</label>
|
||||
<div className='value'>
|
||||
<input
|
||||
id='autoCloseBrackets'
|
||||
type='checkbox'
|
||||
name='autoCloseBrackets'
|
||||
checked={settings.autoCloseBrackets}
|
||||
onChange={(e)=>handleFieldChange('autoCloseBrackets', e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='field'>
|
||||
<label htmlFor='showImagePreviews'>
|
||||
Show Image Previews when hovering a link
|
||||
</label>
|
||||
<div className='value'>
|
||||
<input
|
||||
id='showImagePreviews'
|
||||
type='checkbox'
|
||||
name='showImagePreviews'
|
||||
checked={settings.showImagePreviews}
|
||||
onChange={(e)=>handleFieldChange('showImagePreviews', e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='field'>
|
||||
<label htmlFor='activeLineShading'>
|
||||
Background shading of active line
|
||||
</label>
|
||||
<div className='value'>
|
||||
<input
|
||||
id='activeLineShading'
|
||||
type='checkbox'
|
||||
name='activeLineShading'
|
||||
checked={settings.activeLineShading}
|
||||
onChange={(e)=>handleFieldChange('activeLineShading', e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className='field'>
|
||||
<label htmlFor='lineNumbers'>Show Line Numbers</label>
|
||||
<div className='value'>
|
||||
<input
|
||||
id='lineNumbers'
|
||||
type='checkbox'
|
||||
name='lineNumbers'
|
||||
checked={settings.lineNumbers}
|
||||
onChange={(e)=>handleFieldChange('lineNumbers', e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='field'>
|
||||
<label htmlFor='fontSize'>
|
||||
Editor Font Size
|
||||
</label>
|
||||
|
||||
<div className='value'>
|
||||
<small style={{ fontSize: `${settings.fontSize || 1}em` }}>from 7px to 26px</small>
|
||||
<input
|
||||
id='fontSize'
|
||||
type='range'
|
||||
min={.5}
|
||||
step={.1}
|
||||
max={2}
|
||||
list='font-sizes'
|
||||
name='fontSize'
|
||||
title={`${Math.round(settings.fontSize * 13)}px`}
|
||||
value={settings.fontSize || 1}
|
||||
onChange={(e)=>handleFieldChange('fontSize', e)}
|
||||
/>
|
||||
<datalist id='font-sizes'>
|
||||
<option value='1' />
|
||||
</datalist>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsEditor;
|
||||
@@ -10,6 +10,7 @@ import cx from 'classnames';
|
||||
import { loadHistory } from '../../utils/versionHistory.js';
|
||||
import { brewSnippetsToJSON } from '@shared/helpers.js';
|
||||
|
||||
/*eslint-disable camelcase*/
|
||||
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';
|
||||
@@ -23,26 +24,7 @@ const ThemeSnippets = {
|
||||
V3_Journal : V3_Journal,
|
||||
V3_Blank : V3_Blank,
|
||||
};
|
||||
|
||||
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))
|
||||
];
|
||||
/*eslint-enable camelcase */
|
||||
|
||||
const execute = function(val, props){
|
||||
if(_.isFunction(val)) return val(props);
|
||||
@@ -53,30 +35,28 @@ const Snippetbar = createReactClass({
|
||||
displayName : 'SnippetBar',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {},
|
||||
view : 'text',
|
||||
onViewChange : ()=>{},
|
||||
onInject : ()=>{},
|
||||
onToggle : ()=>{},
|
||||
showEditButtons : true,
|
||||
renderer : 'legacy',
|
||||
undo : ()=>{},
|
||||
redo : ()=>{},
|
||||
historySize : ()=>{},
|
||||
foldCode : ()=>{},
|
||||
unfoldCode : ()=>{},
|
||||
formatCode : ()=>{},
|
||||
updateEditorTheme : ()=>{},
|
||||
cursorPos : {},
|
||||
themeBundle : [],
|
||||
updateBrew : ()=>{}
|
||||
brew : {},
|
||||
view : 'text',
|
||||
onViewChange : ()=>{},
|
||||
onInject : ()=>{},
|
||||
onToggle : ()=>{},
|
||||
showEditButtons : true,
|
||||
renderer : 'legacy',
|
||||
undo : ()=>{},
|
||||
redo : ()=>{},
|
||||
historySize : ()=>{},
|
||||
foldCode : ()=>{},
|
||||
unfoldCode : ()=>{},
|
||||
formatCode : ()=>{},
|
||||
cursorPos : {},
|
||||
themeBundle : [],
|
||||
updateBrew : ()=>{}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
renderer : this.props.renderer,
|
||||
themeSelector : false,
|
||||
snippets : [],
|
||||
showHistory : false,
|
||||
historyExists : false,
|
||||
@@ -93,7 +73,6 @@ const Snippetbar = createReactClass({
|
||||
|
||||
componentDidUpdate : async function(prevProps, prevState) {
|
||||
if(prevProps.renderer != this.props.renderer ||
|
||||
prevProps.theme != this.props.theme ||
|
||||
prevProps.themeBundle != this.props.themeBundle ||
|
||||
prevProps.brew.snippets != this.props.brew.snippets) {
|
||||
this.setState({
|
||||
@@ -158,33 +137,6 @@ const Snippetbar = createReactClass({
|
||||
this.props.onInject(injectedText);
|
||||
},
|
||||
|
||||
toggleThemeSelector : function(e){
|
||||
if(e.target.tagName != 'SELECT'){
|
||||
this.setState({
|
||||
themeSelector : !this.state.themeSelector
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
changeTheme : function(e){
|
||||
if(e.target.value == this.props.currentEditorTheme) return;
|
||||
this.props.updateEditorTheme(e.target.value);
|
||||
|
||||
this.setState({
|
||||
themeSelector : false,
|
||||
});
|
||||
},
|
||||
|
||||
renderThemeSelector : function(){
|
||||
return <div className='themeSelector'>
|
||||
<select value={this.props.currentEditorTheme} onChange={this.changeTheme} >
|
||||
{EditorThemes.map((theme, key)=>{
|
||||
return <option key={key} value={theme}>{theme}</option>;
|
||||
})}
|
||||
</select>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderSnippetGroups : function(){
|
||||
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||
if(snippets.length === 0) return null;
|
||||
@@ -246,7 +198,7 @@ const Snippetbar = createReactClass({
|
||||
|
||||
return (
|
||||
<div className='editors'>
|
||||
{this.props.view !== 'meta' && <><div className='historyTools'>
|
||||
{this.props.view !== 'meta' && this.props.view !== 'settings' && <><div className='historyTools'>
|
||||
<button className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
||||
onClick={this.toggleHistoryMenu} >
|
||||
<i className='fas fa-clock-rotate-left' />
|
||||
@@ -274,11 +226,6 @@ const Snippetbar = createReactClass({
|
||||
onClick={this.props.formatCode} >
|
||||
<i className='fas fa-wand-magic-sparkles' />
|
||||
</button>
|
||||
<button className={`editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||
onClick={this.toggleThemeSelector} >
|
||||
<i className='fas fa-palette' />
|
||||
{this.state.themeSelector && this.renderThemeSelector()}
|
||||
</button>
|
||||
</div></>}
|
||||
|
||||
<div className='tabs'>
|
||||
@@ -298,6 +245,10 @@ const Snippetbar = createReactClass({
|
||||
onClick={()=>this.props.onViewChange('meta')}>
|
||||
<i className='fas fa-info-circle' />
|
||||
</button>
|
||||
<button className={cx('settings', { selected: this.props.view === 'settings' })}
|
||||
onClick={()=>this.props.onViewChange('settings')}>
|
||||
<i className='fas fa-gear' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -346,7 +297,7 @@ const SnippetGroup = createReactClass({
|
||||
<Dropdown groupName={snippet.name} icon={snippet.icon} key={snippet.name}>
|
||||
{this.renderSnippets(snippet.subsnippets)}
|
||||
</Dropdown>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
@import '@sharedStyles/core.less';
|
||||
@import (less) './client/icons/customIcons.less';
|
||||
@import (less) '../../../icons/customIcons.less';
|
||||
@import (less) '@themes/fonts/5e/fonts.less';
|
||||
|
||||
.snippetBar {
|
||||
--activeTriggerColor: inherit;
|
||||
--menuColor : #DDDDDD;
|
||||
--textColor : black;
|
||||
--hoverMenuColor : #999999;
|
||||
|
||||
@menuHeight : 25px;
|
||||
position : relative;
|
||||
display : flex;
|
||||
flex-wrap : wrap-reverse;
|
||||
reading-flow : flex-visual;
|
||||
justify-content : space-between;
|
||||
height : auto;
|
||||
color : black;
|
||||
background-color : #DDDDDD;
|
||||
font-size : .65rem;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
text-transform: uppercase;
|
||||
font-weight: 800;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 0.65rem;
|
||||
font-weight : 800;
|
||||
color : var(--textColor);
|
||||
text-transform : uppercase;
|
||||
background-color : var(--menuColor);
|
||||
reading-flow : flex-visual;
|
||||
|
||||
.editors {
|
||||
display : flex;
|
||||
justify-content : flex-end;
|
||||
min-width : 275px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
|
||||
font-size: .85rem;
|
||||
font-size : 0.85rem;
|
||||
&:only-child {min-width : unset; margin-left : auto;}
|
||||
reading-order : 2;
|
||||
|
||||
@@ -44,7 +46,7 @@
|
||||
|
||||
&.editorTool:not(.active) { cursor : not-allowed; }
|
||||
|
||||
&:hover,&.selected { background-color : #999999; }
|
||||
&:hover,&.selected { background-color : var(--hoverMenuColor); }
|
||||
&.text {
|
||||
.tooltipLeft('Brew Editor');
|
||||
}
|
||||
@@ -101,11 +103,6 @@
|
||||
background-color : #999999;
|
||||
}
|
||||
}
|
||||
&.divider {
|
||||
width : 5px;
|
||||
background : linear-gradient(currentColor, currentColor) no-repeat center/1px 100%;
|
||||
&:hover { background-color : inherit; }
|
||||
}
|
||||
}
|
||||
.themeSelector {
|
||||
position : absolute;
|
||||
@@ -138,36 +135,32 @@
|
||||
}
|
||||
|
||||
// removed caret for top level items, by request (makes buttons too wide).
|
||||
.menu-wrapper .menu-item:is(.snippets > .menu-wrapper > .menu-item):first-child .caret { display: none; }
|
||||
.menu-wrapper .menu-item:is(.snippets > .menu-wrapper > .menu-item):first-child .caret { display : none; }
|
||||
|
||||
.menu-item {
|
||||
position : relative;
|
||||
display : flex;
|
||||
justify-content: space-between;
|
||||
align-items : center;
|
||||
min-width : max-content;
|
||||
padding : 5px;
|
||||
cursor : pointer;
|
||||
width: 100%;
|
||||
&:is(.menu-list .menu-item) [class*="name"] {
|
||||
padding-inline: 8px; // additional space between icon and name (helpful in Fonts menu especially).
|
||||
position : relative;
|
||||
display : flex;
|
||||
align-items : center;
|
||||
justify-content : space-between;
|
||||
width : 100%;
|
||||
min-width : max-content;
|
||||
padding : 5px;
|
||||
cursor : pointer;
|
||||
&:is(.menu-list .menu-item) [class*='name'] {
|
||||
padding-inline : 8px; // additional space between icon and name (helpful in Fonts menu especially).
|
||||
}
|
||||
.menu-name {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
text-box-trim: trim-end;
|
||||
flex : 1;
|
||||
text-align : left;
|
||||
text-box-trim : trim-end;
|
||||
}
|
||||
i {
|
||||
min-width : 25px;
|
||||
height : .85rem;
|
||||
height : 0.85rem;
|
||||
font-size : 1.2em;
|
||||
text-align : center;
|
||||
&.caret {
|
||||
margin-right: 0;
|
||||
}
|
||||
&.caret:is(.menu-wrapper .menu-wrapper * ) {
|
||||
text-align: right;
|
||||
}
|
||||
&.caret { margin-right : 0; }
|
||||
&.caret:is(.menu-wrapper .menu-wrapper *) { text-align : right; }
|
||||
/* Fonts */
|
||||
&.font {
|
||||
height : auto;
|
||||
@@ -206,18 +199,16 @@
|
||||
background : grey;
|
||||
border-radius : 12px;
|
||||
}
|
||||
&:hover {
|
||||
background-color : #999999;
|
||||
}
|
||||
&:hover { background-color : var(--hoverMenuColor); }
|
||||
&:disabled {
|
||||
color: gray;
|
||||
cursor: not-allowed;
|
||||
&:hover { background-color: unset; }
|
||||
color : gray;
|
||||
cursor : not-allowed;
|
||||
&:hover { background-color : unset; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@container editor (width < 841px) {
|
||||
.snippetBar {
|
||||
.snippetBar {
|
||||
.editors {
|
||||
flex : 1;
|
||||
justify-content : space-between;
|
||||
@@ -233,3 +224,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.editor.darkMode .snippetBar {
|
||||
--menuColor : #666666;
|
||||
--textColor : #EEEEEE;
|
||||
--hoverMenuColor : #444444;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import Combobox from '@components/combobox.jsx';
|
||||
|
||||
import { tagSuggestionList, canonizationList } from './curatedTagSuggestionList.js';
|
||||
|
||||
const TagInput = ({ tooltip, label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{
|
||||
const TagInput = ({ id, tooltip, label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{
|
||||
const [tagList, setTagList] = useState(
|
||||
values.map((value)=>({
|
||||
value,
|
||||
@@ -128,6 +128,7 @@ const TagInput = ({ tooltip, label, valuePatterns, values = [], unique = true, p
|
||||
return (
|
||||
<div className='tagInputWrap'>
|
||||
<Combobox
|
||||
id={id}
|
||||
trigger='click'
|
||||
className='tagInput-dropdown'
|
||||
default=''
|
||||
@@ -155,6 +156,7 @@ const TagInput = ({ tooltip, label, valuePatterns, values = [], unique = true, p
|
||||
<ul className='list'>
|
||||
{tagList.map((t, i)=>t.editing ? (
|
||||
<input
|
||||
id={`${id}-${i}`}
|
||||
key={i}
|
||||
type='text'
|
||||
value={t.draft} // always use draft
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
.tags {
|
||||
|
||||
.tagInputWrap {
|
||||
display:grid;
|
||||
grid-template-columns: 200px 3fr;
|
||||
gap:10px;
|
||||
display : grid;
|
||||
grid-template-columns : 200px 3fr;
|
||||
gap : 10px;
|
||||
}
|
||||
|
||||
.list input {
|
||||
border-radius: 5px;
|
||||
}
|
||||
.list input { border-radius : 5px; }
|
||||
|
||||
.tagInput-dropdown {
|
||||
.dropdown-options {
|
||||
.item {
|
||||
&.type {
|
||||
background-color: #00800035;
|
||||
}
|
||||
&.group {
|
||||
background-color: #50505035;
|
||||
}
|
||||
&.meta {
|
||||
background-color: #00008035;
|
||||
}
|
||||
&.system {
|
||||
background-color: #80000035;
|
||||
}
|
||||
&.type { background-color : #00800035; }
|
||||
&.group { background-color : #50505035; }
|
||||
&.meta { background-color : #00008035; }
|
||||
&.system { background-color : #80000035; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,438 @@
|
||||
@import '@themes/assets/assets.less';
|
||||
@import '@sharedStyles/core.less';
|
||||
|
||||
.userThemeName {
|
||||
padding-right : 10px;
|
||||
padding-left : 10px;
|
||||
}
|
||||
|
||||
.uiEditor {
|
||||
--bg-clr : white;
|
||||
--bg-image : @backgroundImageAlt;
|
||||
--h1-clr : black;
|
||||
--h2-clr : #000077;
|
||||
--label-clr : black;
|
||||
--input-bg : white;
|
||||
--input-bg-hover : #DDDDDD;
|
||||
--input-text-clr : black;
|
||||
--input-placeholder-clr : grey;
|
||||
|
||||
position : absolute;
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||
padding : 25px;
|
||||
overflow-y : auto;
|
||||
font-size : 13px;
|
||||
color : var(--label-clr);
|
||||
background-color : var(--bg-clr);
|
||||
background-image : var(--bg-image);
|
||||
background-size : cover;
|
||||
|
||||
h1 {
|
||||
margin : 0 0 40px;
|
||||
font-size : 18px;
|
||||
font-weight : bold;
|
||||
color : var(--h1-clr);
|
||||
text-transform : uppercase;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin : 16px 0;
|
||||
font-size : 15px;
|
||||
font-weight : bold;
|
||||
color : var(--h2-clr);
|
||||
border-bottom : 2px solid currentColor;
|
||||
}
|
||||
|
||||
& > div ,& > fieldset { margin-bottom : 10px; }
|
||||
|
||||
.field-group {
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
gap : 10px;
|
||||
width : 100%;
|
||||
}
|
||||
|
||||
.field-column {
|
||||
display : flex;
|
||||
flex : 5 0 250px;
|
||||
flex-direction : column;
|
||||
gap : 10px;
|
||||
}
|
||||
|
||||
.field {
|
||||
position : relative;
|
||||
display : flex;
|
||||
flex-wrap : wrap;
|
||||
width : 100%;
|
||||
min-width : 250px;
|
||||
padding-left : 10px;
|
||||
|
||||
& > label {
|
||||
width : 100px;
|
||||
font-size : 1em;
|
||||
font-weight : 800;
|
||||
line-height : 1.8em;
|
||||
color : inherit;
|
||||
text-transform : capitalize;
|
||||
}
|
||||
& > .value {
|
||||
flex : 1 1 auto;
|
||||
width : 50px;
|
||||
&[data-tooltip-right] { max-width : 380px; }
|
||||
&:invalid { background : #FFB9B9; }
|
||||
small {
|
||||
display : block;
|
||||
width : fit-content;
|
||||
margin-inline : 5px;
|
||||
font-size : 0.9em;
|
||||
font-style : italic;
|
||||
line-height : 1.4em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
input[type='checkbox'], input[type='number'] {
|
||||
float : right;
|
||||
text-align : end;
|
||||
}
|
||||
input[type='text'], textarea, select {
|
||||
color : var(--input-text-clr);
|
||||
background-color : var(--input-bg);
|
||||
border : 1px solid var(--input-placeholder-clr);
|
||||
&:hover, :focus { outline : 1px solid var(--input-placeholder-clr); background-color : var(--input-bg-hover); }
|
||||
&::placeholder { color : var(--input-placeholder-clr); }
|
||||
}
|
||||
|
||||
input[type='range'] { color : var(--input-text-clr); }
|
||||
|
||||
&.description {
|
||||
flex : 1;
|
||||
textarea.value {
|
||||
height : auto;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
resize : none;
|
||||
}
|
||||
}
|
||||
&.thumbnail, &.themes {
|
||||
label { line-height : 2.0em; }
|
||||
.value {
|
||||
overflow : hidden;
|
||||
text-overflow : ellipsis;
|
||||
}
|
||||
button {
|
||||
.colorButton();
|
||||
padding : 0px 5px;
|
||||
color : var(--input-text-clr);
|
||||
background-color : var(--input-bg);
|
||||
border : 1px solid #999999;
|
||||
&:hover { background-color : #777777; }
|
||||
}
|
||||
}
|
||||
&.tags .tagInput-dropdown {
|
||||
z-index : 400;
|
||||
max-width : 200px;
|
||||
|
||||
.dropdown-options {
|
||||
color : var(--input-text-clr);
|
||||
background-color : var(--input-bg);
|
||||
|
||||
.item:hover {
|
||||
color : var(--input-bg);
|
||||
background-color : var(--input-text-clr);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.language .value {
|
||||
z-index : 300;
|
||||
max-width : 150px;
|
||||
}
|
||||
&.themes {
|
||||
.value { overflow : visible; }
|
||||
button {
|
||||
padding-right : 5px;
|
||||
padding-left : 5px;
|
||||
}
|
||||
& .dropdown-container { z-index : 200; }
|
||||
& .dropdown-options { overflow-y : visible; }
|
||||
.disabled {
|
||||
font-style : italic;
|
||||
color : dimgray;
|
||||
background-color : darkgray;
|
||||
}
|
||||
.item.dropdown-input { border : none;}
|
||||
.item {
|
||||
position : relative;
|
||||
overflow : visible;
|
||||
color : inherit;
|
||||
background-color : var(--input-bg);
|
||||
border-top : 1px solid #767676;
|
||||
.preview {
|
||||
position : absolute;
|
||||
top : 0;
|
||||
right : 0;
|
||||
z-index : 10;
|
||||
display : flex;
|
||||
flex-direction : column;
|
||||
width : 200px;
|
||||
overflow : hidden;
|
||||
color : var(--input-text-clr);
|
||||
background : var(--input-bg);
|
||||
border-radius : 5px;
|
||||
box-shadow : 0 0 5px var(--input-text-clr);
|
||||
opacity : 0;
|
||||
transition : opacity 250ms ease;
|
||||
h6 {
|
||||
padding-block : 0.5em;
|
||||
padding-inline : 1em;
|
||||
font-weight : 900;
|
||||
border-bottom : 2px solid #666666;
|
||||
}
|
||||
}
|
||||
|
||||
input { padding-right : 25px; }
|
||||
|
||||
.texture-container {
|
||||
position : absolute;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
min-height : 100%;
|
||||
overflow : hidden;
|
||||
> img {
|
||||
position : absolute;
|
||||
top : 0;
|
||||
right : 0;
|
||||
width : 50%;
|
||||
min-height : 100%;
|
||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .preview { opacity : 1; }
|
||||
}
|
||||
}
|
||||
&.renderers .value {
|
||||
label {
|
||||
display : inline-flex;
|
||||
align-items : center;
|
||||
margin-right : 15px;
|
||||
font-size : 0.9em;
|
||||
font-weight : 800;
|
||||
vertical-align : middle;
|
||||
white-space : nowrap;
|
||||
cursor : pointer;
|
||||
user-select : none;
|
||||
}
|
||||
input {
|
||||
margin : 3px;
|
||||
vertical-align : middle;
|
||||
cursor : pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&.authors {
|
||||
.tag {
|
||||
font-weight : 300;
|
||||
transition : background-color 0.2s;
|
||||
|
||||
&.owner {
|
||||
position : relative;
|
||||
display : grid;
|
||||
place-items : center;
|
||||
min-width : 25px;
|
||||
font-weight : 900;
|
||||
background-color : @silverLight;
|
||||
|
||||
&::after {
|
||||
position : absolute;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 15px;
|
||||
height : 15px;
|
||||
font-family : 'Font Awesome 6 Free';
|
||||
color : gold;
|
||||
content : '\f521';
|
||||
transform : scaleY(0.7);
|
||||
rotate : -25deg;
|
||||
translate : -30% -50%;
|
||||
}
|
||||
}
|
||||
&:has(button) a { padding-right : 5px; }
|
||||
&:has(button:hover) { background : #D97D7D; }
|
||||
|
||||
button { color : @red; }
|
||||
|
||||
}
|
||||
a { text-underline-offset : 0.2em; }
|
||||
}
|
||||
&.invitedAuthors .value {
|
||||
z-index : 100;
|
||||
|
||||
.tagInput-dropdown { max-width : 200px; }
|
||||
}
|
||||
|
||||
&.publish .value {
|
||||
position : relative;
|
||||
margin-bottom : 15px;
|
||||
button { width : 100%; }
|
||||
button.publish {
|
||||
.colorButton(@blueLight);
|
||||
}
|
||||
button.unpublish {
|
||||
.colorButton(@silver);
|
||||
}
|
||||
}
|
||||
&.delete .value {
|
||||
button {
|
||||
.colorButton(@red);
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
display : flex;
|
||||
flex : 1 0;
|
||||
flex-wrap : wrap;
|
||||
|
||||
> * { flex : 0 0 auto; }
|
||||
|
||||
#groupedIcon {
|
||||
#backgroundColors;
|
||||
position : relative;
|
||||
top : -0.3em;
|
||||
right : -0.3em;
|
||||
display : inline-block;
|
||||
min-width : 20px;
|
||||
height : ~'calc(100% + 0.6em)';
|
||||
color : white;
|
||||
text-align : center;
|
||||
cursor : pointer;
|
||||
|
||||
i {
|
||||
position : relative;
|
||||
top : 50%;
|
||||
transform : translateY(-50%);
|
||||
}
|
||||
|
||||
&:not(:last-child) { border-right : 1px solid black; }
|
||||
|
||||
&:last-child { border-radius : 0 0.5em 0.5em 0; }
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding : 0.35em;
|
||||
margin : 2px;
|
||||
font-size : 0.95em;
|
||||
border-radius : 0.5em;
|
||||
|
||||
.icon { #groupedIcon; }
|
||||
|
||||
button {
|
||||
cursor : pointer;
|
||||
|
||||
&:hover { color : @redLight; }
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
height : ~'calc(.9em + 4px + .6em)';
|
||||
|
||||
input { border-radius : 0.5em 0 0 0.5em; }
|
||||
|
||||
input:last-child { border-radius : 0.5em; }
|
||||
|
||||
.value {
|
||||
width : 7.5vw;
|
||||
min-width : 75px;
|
||||
height : 100%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
height : ~'calc(.9em + 4px + .6em)';
|
||||
|
||||
input { border-radius : 0.5em 0 0 0.5em; }
|
||||
|
||||
input:last-child { border-radius : 0.5em; }
|
||||
|
||||
.value {
|
||||
width : 7.5vw;
|
||||
min-width : 75px;
|
||||
height : 100%;
|
||||
}
|
||||
|
||||
.invalid:focus { background-color : pink; }
|
||||
|
||||
.icon {
|
||||
#groupedIcon;
|
||||
top : -0.54em;
|
||||
right : 1px;
|
||||
height : 97%;
|
||||
|
||||
i { font-size : 1.125em; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail-preview {
|
||||
position : relative;
|
||||
flex : 1 1;
|
||||
justify-self : center;
|
||||
width : 80px;
|
||||
height : min-content;
|
||||
max-height : 115px;
|
||||
aspect-ratio : 1 / 1;
|
||||
object-fit : contain;
|
||||
background-color : #AAAAAA;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color : var(--input-bg);
|
||||
background : var(--input-text-clr);
|
||||
}
|
||||
}
|
||||
|
||||
.settingsEditor .field {
|
||||
padding-bottom : 10px;
|
||||
border-bottom : 1px solid black;
|
||||
|
||||
.value {
|
||||
|
||||
display : flex;
|
||||
justify-content : end;
|
||||
}
|
||||
|
||||
> label {
|
||||
width : fit-content;
|
||||
max-width : calc(100% - 200px);
|
||||
}
|
||||
}
|
||||
|
||||
.editor.darkMode .uiEditor {
|
||||
--bg-clr : #555555;
|
||||
--bg-image : @backgroundImageAltDark;
|
||||
--h1-clr : white;
|
||||
--h2-clr : #AAAAFF;
|
||||
--label-clr : #DDDDDD;
|
||||
--input-bg : #333333;
|
||||
--input-bg-hover : #666666;
|
||||
--input-text-clr : #EEEEEE;
|
||||
--input-placeholder-clr : #AAAAAA;
|
||||
|
||||
.field { border-color : var(--h1-clr); }
|
||||
|
||||
a {
|
||||
color : @blueLight;
|
||||
|
||||
&:visited { color : #CB8AD8; }
|
||||
}
|
||||
|
||||
.thumbnail-preview[src='/client/homebrew/thumbnail.png'] { filter : invert(0.8); }
|
||||
button.publish {
|
||||
.colorButton(#3137de) !important;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'core-js/es/string/to-well-formed.js'; // Polyfill for older browsers
|
||||
import './homebrew.less';
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, useParams, useSearchParams } from 'react-router';
|
||||
|
||||
import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js';
|
||||
@@ -47,7 +46,7 @@ const Homebrew = (props)=>{
|
||||
global.enablev4 = enablev4;
|
||||
|
||||
const backgroundObject = ()=>{
|
||||
if(config?.deployment || (config?.local && config?.development)) {
|
||||
if(config?.deployment || config?.developmentStyle) {
|
||||
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>")`
|
||||
@@ -61,7 +60,7 @@ const Homebrew = (props)=>{
|
||||
if(brew.pureError) {
|
||||
return (
|
||||
<Router>
|
||||
<div className={`homebrew${(config?.deployment || config?.local) ? ' deployment' : ''}`} style={backgroundObject()}>
|
||||
<div className={`homebrew${(config?.deployment || config?.developmentStyle) ? ' deployment' : ''}`} style={backgroundObject()}>
|
||||
<Routes>
|
||||
<Route path={brew.originalUrl} element={<WithRoute el={ErrorPage} brew={brew} />} />
|
||||
</Routes>
|
||||
@@ -73,7 +72,7 @@ const Homebrew = (props)=>{
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className={`homebrew${(config?.deployment || config?.local) ? ' deployment' : ''}`} style={backgroundObject()}>
|
||||
<div className={`homebrew${(config?.deployment || config?.developmentStyle) ? ' 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} />} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import '@sharedStyles/core.less';
|
||||
.homebrew {
|
||||
height : 100%;
|
||||
background-color:@steel;
|
||||
height : 100%;
|
||||
background-color : @steel;
|
||||
&.deployment { background-color : darkred; }
|
||||
|
||||
.sitePage {
|
||||
|
||||
@@ -1,46 +1,28 @@
|
||||
import './navbar.less';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
import Nav from './nav.jsx';
|
||||
import PatreonNavItem from './patreon.navitem.jsx';
|
||||
|
||||
const Navbar = createReactClass({
|
||||
displayName : 'Navbar',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
// showNonChromeWarning: false, // uncomment if needed
|
||||
ver : global.version || '0.0.0'
|
||||
};
|
||||
},
|
||||
const Navbar = ({ children })=>{
|
||||
const version = global.version || '0.0.0';
|
||||
|
||||
/*
|
||||
renderChromeWarning : function(){
|
||||
if(!this.state.showNonChromeWarning) return;
|
||||
return <Nav.item className='warning' icon='fa-exclamation-triangle'>
|
||||
Optimized for Chrome
|
||||
<div className='dropdown'>
|
||||
If you are experiencing rendering issues, use Chrome instead
|
||||
</div>
|
||||
</Nav.item>
|
||||
},
|
||||
*/
|
||||
render : function(){
|
||||
return <Nav.base>
|
||||
return (
|
||||
<Nav.base>
|
||||
<Nav.section>
|
||||
<Nav.logo />
|
||||
<Nav.item href='/' className='homebrewLogo'>
|
||||
<div>The Homebrewery</div>
|
||||
</Nav.item>
|
||||
<Nav.item newTab={true} href='/changelog' color='purple' icon='far fa-file-alt'>
|
||||
{`v${this.state.ver}`}
|
||||
{`v${version}`}
|
||||
</Nav.item>
|
||||
<PatreonNavItem />
|
||||
{/*this.renderChromeWarning()*/}
|
||||
{/* this.renderChromeWarning() */}
|
||||
</Nav.section>
|
||||
{this.props.children}
|
||||
</Nav.base>;
|
||||
}
|
||||
});
|
||||
{children}
|
||||
</Nav.base>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
export default Navbar;
|
||||
@@ -39,9 +39,7 @@
|
||||
flex-grow : 1;
|
||||
min-width : 300px;
|
||||
}
|
||||
>.brewTitle {
|
||||
cursor:auto;
|
||||
}
|
||||
>.brewTitle { cursor : auto; }
|
||||
}
|
||||
// "NaturalCrit" logo
|
||||
.navLogo {
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function(){
|
||||
return ()=>{
|
||||
document.removeEventListener('print:startprep', handlePrintStartPrep);
|
||||
document.removeEventListener('print:finishedprep', handlePrintPrepFinished);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlePrintStartPrep = ()=>{ setPrinting(true); };
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
box-shadow : 0px 4px 5px 0px #333333;
|
||||
break-inside : avoid;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
.thumbnail {
|
||||
position : absolute;
|
||||
top : 0;
|
||||
@@ -43,7 +43,7 @@
|
||||
.info {
|
||||
position : initial;
|
||||
bottom : 2px;
|
||||
font-family : "ScalySansRemake";
|
||||
font-family : 'ScalySansRemake';
|
||||
font-size : 1.2em;
|
||||
& > span {
|
||||
margin-right : 12px;
|
||||
@@ -106,26 +106,26 @@
|
||||
a, button {
|
||||
.animate(opacity);
|
||||
display : block;
|
||||
width : 100%;
|
||||
margin : 8px 0px;
|
||||
font-size : 1.3em;
|
||||
color : white;
|
||||
text-decoration : unset;
|
||||
opacity : 0.6;
|
||||
width : 100%;
|
||||
&:hover { opacity : 1; }
|
||||
i { cursor : pointer; }
|
||||
}
|
||||
}
|
||||
.googleDriveIcon {
|
||||
height : 18px;
|
||||
padding : 0px;
|
||||
margin : -5px;
|
||||
height : 18px;
|
||||
}
|
||||
.homebreweryIcon {
|
||||
position : relative;
|
||||
padding : 0px;
|
||||
top : 5px;
|
||||
left : -7.5px;
|
||||
height : 18px;
|
||||
padding : 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
|
||||
.noColumns() {
|
||||
column-count : auto;
|
||||
column-fill : auto;
|
||||
column-gap : normal;
|
||||
-webkit-column-width : auto;
|
||||
-moz-column-width : auto;
|
||||
column-width : auto;
|
||||
-webkit-column-count : auto;
|
||||
-moz-column-count : auto;
|
||||
-webkit-column-width : auto;
|
||||
-moz-column-width : auto;
|
||||
column-count : auto;
|
||||
column-fill : auto;
|
||||
-webkit-column-gap : normal;
|
||||
-moz-column-gap : normal;
|
||||
column-gap : normal;
|
||||
height : auto;
|
||||
min-height : 279.4mm;
|
||||
margin : 20px auto;
|
||||
|
||||
@@ -2,26 +2,27 @@
|
||||
import './editPage.less';
|
||||
|
||||
// Common imports
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useEffectEvent } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
|
||||
|
||||
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx';
|
||||
|
||||
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 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;
|
||||
|
||||
@@ -37,19 +38,13 @@ import LockNotification from './lockNotification/lockNotification.jsx';
|
||||
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
||||
import googleDriveIcon from '../../googleDrive.svg';
|
||||
|
||||
const SAVE_TIMEOUT = 10000;
|
||||
const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes
|
||||
const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds
|
||||
|
||||
|
||||
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn';
|
||||
const BREWKEY = 'HB_newPage_content';
|
||||
const STYLEKEY = 'HB_newPage_style';
|
||||
const SNIPKEY = 'HB_newPage_snippets';
|
||||
const METAKEY = 'HB_newPage_meta';
|
||||
|
||||
const useLocalStorage = false;
|
||||
const neverSaved = false;
|
||||
const sandbox = false;
|
||||
|
||||
const EditPage = (props)=>{
|
||||
props = {
|
||||
@@ -58,8 +53,6 @@ const EditPage = (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(hbfm.validate(props.brew.text));
|
||||
@@ -67,87 +60,13 @@ const EditPage = (props)=>{
|
||||
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 [alertNoGoogleToTransfer, setAlertNoGoogleToTransfer] = useState(false);
|
||||
const [alertOwnershipToTransfer, setAlertOwnershipToTransfer] = 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(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(()=>{
|
||||
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
|
||||
setAutoSaveEnabled(autoSavePref);
|
||||
setWarnUnsavedChanges(!autoSavePref);
|
||||
setHTMLErrors(hbfm.validate(currentBrew.text));
|
||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
const handleControlKeys = (e)=>{
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
if(e.keyCode === 83) trySaveRef.current(true, true, saveGoogle);
|
||||
if(e.keyCode === 80) printCurrentBrew();
|
||||
if([83, 80].includes(e.keyCode)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
window.onbeforeunload = ()=>{
|
||||
if(unsavedChangesRef.current)
|
||||
return 'You have unsaved changes!';
|
||||
};
|
||||
return ()=>{
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
trySaveRef.current = trySave;
|
||||
unsavedChangesRef.current = unsavedChanges;
|
||||
});
|
||||
|
||||
useEffect(()=>{
|
||||
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||
setUnsavedChanges(hasChange);
|
||||
|
||||
if(autoSaveEnabled) trySave(false, hasChange, saveGoogle);
|
||||
}, [currentBrew]);
|
||||
|
||||
const handleSplitMove = ()=>{
|
||||
editorRef.current?.update();
|
||||
};
|
||||
|
||||
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||
if(subfield == 'renderer' || subfield == 'theme')
|
||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||
|
||||
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||
setHTMLErrors(hbfm.validate(value));
|
||||
|
||||
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||
|
||||
if(useLocalStorage) {
|
||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||
if(field == 'style') localStorage.setItem(STYLEKEY, value);
|
||||
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
|
||||
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
|
||||
renderer : value.renderer,
|
||||
theme : value.theme,
|
||||
lang : value.lang
|
||||
}));
|
||||
}
|
||||
};
|
||||
const editorRef = useRef(null);
|
||||
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||
|
||||
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
|
||||
...prevBrew,
|
||||
@@ -156,15 +75,9 @@ const EditPage = (props)=>{
|
||||
snippets : newData.snippets
|
||||
}));
|
||||
|
||||
const resetWarnUnsavedTimer = ()=>{
|
||||
setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
|
||||
clearTimeout(warnUnsavedTimeout.current);
|
||||
warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings
|
||||
};
|
||||
|
||||
const handleGoogleClick = ()=>{
|
||||
if(global.account !== currentBrew.authors[0]) {
|
||||
setalertOwnershipToTransfer(true);
|
||||
if(currentBrew.authors.length > 0 && global.account?.username !== currentBrew.authors[0]) {
|
||||
setAlertOwnershipToTransfer(true);
|
||||
return;
|
||||
}
|
||||
if(!global.account?.googleId) {
|
||||
@@ -181,7 +94,7 @@ const EditPage = (props)=>{
|
||||
setAlertTrashedGoogleBrew(false);
|
||||
setAlertNoGoogleToTransfer(false);
|
||||
setConfirmGoogleTransfer(false);
|
||||
setalertOwnershipToTransfer(false);
|
||||
setAlertOwnershipToTransfer(false);
|
||||
};
|
||||
|
||||
const toggleGoogleStorage = (e)=>{
|
||||
@@ -192,25 +105,6 @@ const EditPage = (props)=>{
|
||||
trySave(true, true, newSaveGoogle);
|
||||
};
|
||||
|
||||
const trySave = (immediate = false, hasChanges = true, saveToGoogle = false)=>{
|
||||
clearTimeout(saveTimeout.current);
|
||||
if(isSaving) return;
|
||||
if(!hasChanges && !immediate) return;
|
||||
const newTimeout = immediate ? 0 : SAVE_TIMEOUT;
|
||||
|
||||
saveTimeout.current = setTimeout(async ()=>{
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
await save(currentBrew, saveToGoogle)
|
||||
.catch((err)=>{
|
||||
setError(err);
|
||||
});
|
||||
setIsSaving(false);
|
||||
setLastSavedTime(new Date());
|
||||
if(!autoSaveEnabled) resetWarnUnsavedTimer();
|
||||
}, newTimeout);
|
||||
};
|
||||
|
||||
const save = async (brew, saveToGoogle)=>{
|
||||
setHTMLErrors(hbfm.validate(brew.text));
|
||||
|
||||
@@ -267,12 +161,11 @@ const EditPage = (props)=>{
|
||||
<Nav.item className='googleDriveStorage' onClick={handleGoogleClick}>
|
||||
<img src={googleDriveIcon} className={saveGoogle ? '' : 'inactive'} alt='Google Drive icon' />
|
||||
|
||||
|
||||
|
||||
{alertOwnershipToTransfer && (
|
||||
<div className='errorContainer'>
|
||||
You must be the Owner to transfer between the Homebrewery and Google Drive!
|
||||
The owner of this file is {currentBrew.authors[0]}
|
||||
The owner of this file is {currentBrew.authors[0]}.
|
||||
<br></br>
|
||||
<div className='confirm' onClick={closeAlerts}> Okay </div>
|
||||
</div>
|
||||
)}
|
||||
@@ -308,60 +201,12 @@ const EditPage = (props)=>{
|
||||
</Nav.item>
|
||||
);
|
||||
|
||||
const renderSaveButton = ()=>{
|
||||
// #1 - Currently saving, show SAVING
|
||||
if(isSaving)
|
||||
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||
|
||||
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
|
||||
if(unsavedChanges && warnUnsavedChanges) {
|
||||
resetWarnUnsavedTimer();
|
||||
const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
|
||||
const text = elapsedTime === 0
|
||||
? 'Autosave is OFF.'
|
||||
: `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
|
||||
Reminder...
|
||||
<div className='errorContainer'>{text}</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||
if(unsavedChanges)
|
||||
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)
|
||||
return <Nav.item className='save saved'>auto-saved</Nav.item>;
|
||||
|
||||
// #5 - No unsaved changes, and has never been saved, hide the button
|
||||
if(neverSaved)
|
||||
return <Nav.item className='save neverSaved' disabled={true}>save now</Nav.item>;
|
||||
|
||||
// DEFAULT - No unsaved changes, show SAVED
|
||||
return <Nav.item className='save saved'>saved</Nav.item>;
|
||||
};
|
||||
|
||||
const toggleAutoSave = ()=>{
|
||||
clearTimeout(warnUnsavedTimeout.current);
|
||||
clearTimeout(saveTimeout.current);
|
||||
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled));
|
||||
setAutoSaveEnabled(!autoSaveEnabled);
|
||||
setWarnUnsavedChanges(autoSaveEnabled);
|
||||
};
|
||||
|
||||
const renderAutoSaveButton = ()=>(
|
||||
<Nav.item onClick={toggleAutoSave}>
|
||||
Autosave <i className={autoSaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
||||
</Nav.item>
|
||||
);
|
||||
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const renderNavbar = ()=>{
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
@@ -387,6 +232,34 @@ const EditPage = (props)=>{
|
||||
</Navbar>;
|
||||
};
|
||||
|
||||
const {
|
||||
handleSplitMove,
|
||||
handleBrewChange,
|
||||
toggleAutoSave,
|
||||
clearError,
|
||||
renderSaveButton,
|
||||
autoSaveEnabled,
|
||||
trySave
|
||||
} = useCommonEditPageFunctions({
|
||||
saveGoogle,
|
||||
setError,
|
||||
setThemeBundle,
|
||||
HTMLErrors,
|
||||
setHTMLErrors,
|
||||
currentBrew,
|
||||
setCurrentBrew,
|
||||
useLocalStorage,
|
||||
BREWKEY,
|
||||
STYLEKEY,
|
||||
SNIPKEY,
|
||||
METAKEY,
|
||||
hbfm,
|
||||
sandbox,
|
||||
lastSavedBrew,
|
||||
editorRef,
|
||||
save,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='editPage sitePage'>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
@@ -416,14 +289,11 @@ const EditPage = (props)=>{
|
||||
text={currentBrew.text}
|
||||
style={currentBrew.style}
|
||||
renderer={currentBrew.renderer}
|
||||
theme={currentBrew.theme}
|
||||
themeBundle={themeBundle}
|
||||
errors={HTMLErrors}
|
||||
lang={currentBrew.lang}
|
||||
onPageChange={setCurrentBrewRendererPageNum}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</SplitPane>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './errorPage.less';
|
||||
import React from 'react';
|
||||
import UIPage from '../basePages/uiPage/uiPage.jsx';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
import ErrorIndex from './errors/errorIndex.js';
|
||||
|
||||
const ErrorPage = ({ brew })=>{
|
||||
|
||||
@@ -2,26 +2,27 @@
|
||||
import './homePage.less';
|
||||
|
||||
// Common imports
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useEffectEvent } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
|
||||
|
||||
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx';
|
||||
|
||||
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 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;
|
||||
|
||||
@@ -30,149 +31,45 @@ const { both: RecentNavItem } = RecentNavItems;
|
||||
import Headtags from '@vitreum/headtags.js';
|
||||
const Meta = Headtags.Meta;
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const SNIPKEY = 'homebrewery-new-snippets';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
const BREWKEY = 'HB_newPage_content';
|
||||
const STYLEKEY = 'HB_newPage_style';
|
||||
const SNIPKEY = 'HB_newPage_snippets';
|
||||
const METAKEY = 'HB_newPage_meta';
|
||||
|
||||
const useLocalStorage = false;
|
||||
const neverSaved = true;
|
||||
const sandbox = true;
|
||||
|
||||
const HomePage =(props)=>{
|
||||
props = {
|
||||
brew : DEFAULT_BREW,
|
||||
ver : '0.0.0',
|
||||
...props
|
||||
};
|
||||
|
||||
const [currentBrew, setCurrentBrew] = useState(props.brew);
|
||||
const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false);
|
||||
const [error, setError] = useState(undefined);
|
||||
const [HTMLErrors, setHTMLErrors] = useState(hbfm.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 editorRef = useRef(null);
|
||||
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||
const unsavedChangesRef = useRef(unsavedChanges);
|
||||
const editorRef = useRef(null);
|
||||
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||
|
||||
useEffect(()=>{
|
||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
const handleControlKeys = (e)=>{
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
if(e.keyCode === 83) trySaveRef.current(true);
|
||||
if(e.keyCode === 80) printCurrentBrew();
|
||||
if([83, 80].includes(e.keyCode)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
window.onbeforeunload = ()=>{
|
||||
if(unsavedChangesRef.current)
|
||||
return 'You have unsaved changes!';
|
||||
};
|
||||
return ()=>{
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
unsavedChangesRef.current = unsavedChanges;
|
||||
}, [unsavedChanges]);
|
||||
|
||||
const save = ()=>{
|
||||
request.post('/api')
|
||||
.send(currentBrew)
|
||||
.end((err, res)=>{
|
||||
if(err) {
|
||||
setError(err);
|
||||
return;
|
||||
}
|
||||
const saved = res.body;
|
||||
window.location = `/edit/${saved.editId}`;
|
||||
const save = async (brew, saveToGoogle)=>{
|
||||
const res = await request
|
||||
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.error('Error Updating Local Brew');
|
||||
setError(err);
|
||||
});
|
||||
};
|
||||
if(!res) return;
|
||||
|
||||
useEffect(()=>{
|
||||
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||
setUnsavedChanges(hasChange);
|
||||
|
||||
if(autoSaveEnabled) trySave(false, hasChange);
|
||||
}, [currentBrew]);
|
||||
|
||||
const handleSplitMove = ()=>{
|
||||
editorRef.current.update();
|
||||
};
|
||||
|
||||
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||
if(subfield == 'renderer' || subfield == 'theme')
|
||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||
|
||||
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||
setHTMLErrors(hbfm.validate(value));
|
||||
|
||||
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||
|
||||
if(useLocalStorage) {
|
||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||
if(field == 'style') localStorage.setItem(STYLEKEY, value);
|
||||
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
|
||||
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
|
||||
renderer : value.renderer,
|
||||
theme : value.theme,
|
||||
lang : value.lang
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const renderSaveButton = ()=>{
|
||||
// #1 - Currently saving, show SAVING
|
||||
if(isSaving)
|
||||
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||
|
||||
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
|
||||
// if(unsavedChanges && warnUnsavedChanges) {
|
||||
// resetWarnUnsavedTimer();
|
||||
// const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
|
||||
// const text = elapsedTime === 0
|
||||
// ? 'Autosave is OFF.'
|
||||
// : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||
|
||||
// return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
|
||||
// Reminder...
|
||||
// <div className='errorContainer'>{text}</div>
|
||||
// </Nav.item>;
|
||||
// }
|
||||
|
||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||
if(unsavedChanges)
|
||||
return <Nav.item className='save' onClick={save} color='blue' icon='fas fa-save'>save now</Nav.item>;
|
||||
|
||||
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
||||
if(autoSaveEnabled)
|
||||
return <Nav.item className='save saved'>auto-saved</Nav.item>;
|
||||
|
||||
// #5 - No unsaved changes, and has never been saved, hide the button
|
||||
if(neverSaved)
|
||||
return <Nav.item className='save neverSaved' disabled={true}>save now</Nav.item>;
|
||||
|
||||
// DEFAULT - No unsaved changes, show SAVED
|
||||
return <Nav.item className='save saved'>saved</Nav.item>;
|
||||
};
|
||||
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
setIsSaving(false);
|
||||
const saved = res.body;
|
||||
window.onbeforeunload = null;
|
||||
window.location = `/edit/${saved.editId}`;
|
||||
};
|
||||
|
||||
const renderNavbar = ()=>{
|
||||
@@ -191,6 +88,33 @@ const HomePage =(props)=>{
|
||||
</Navbar>;
|
||||
};
|
||||
|
||||
const {
|
||||
handleSplitMove,
|
||||
handleBrewChange,
|
||||
clearError,
|
||||
renderSaveButton,
|
||||
unsavedChanges,
|
||||
trySave
|
||||
} = useCommonEditPageFunctions({
|
||||
saveGoogle,
|
||||
setError,
|
||||
setThemeBundle,
|
||||
HTMLErrors,
|
||||
setHTMLErrors,
|
||||
currentBrew,
|
||||
setCurrentBrew,
|
||||
useLocalStorage,
|
||||
BREWKEY,
|
||||
STYLEKEY,
|
||||
SNIPKEY,
|
||||
METAKEY,
|
||||
hbfm,
|
||||
sandbox,
|
||||
lastSavedBrew,
|
||||
editorRef,
|
||||
save,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='homePage sitePage'>
|
||||
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
||||
@@ -214,15 +138,13 @@ const HomePage =(props)=>{
|
||||
text={currentBrew.text}
|
||||
style={currentBrew.style}
|
||||
renderer={currentBrew.renderer}
|
||||
onPageChange={setCurrentBrewRendererPageNum}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
themeBundle={themeBundle}
|
||||
onPageChange={setCurrentBrewRendererPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
<div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={save}>
|
||||
<div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={()=>trySave(true, true, saveGoogle)}>
|
||||
Save current <i className='fas fa-save' />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -37,14 +37,14 @@
|
||||
|
||||
.navItem.save {
|
||||
background-color : @orange;
|
||||
transition:all 0.2s;
|
||||
transition : all 0.2s;
|
||||
&:hover { background-color : @green; }
|
||||
|
||||
&.neverSaved {
|
||||
translate:-100%;
|
||||
opacity: 0;
|
||||
background-color :#333;
|
||||
cursor:auto;
|
||||
cursor : auto;
|
||||
background-color : #333333;
|
||||
opacity : 0;
|
||||
translate : -100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import './newPage.less';
|
||||
|
||||
// Common imports
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useEffectEvent } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
|
||||
|
||||
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx';
|
||||
|
||||
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 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
|
||||
|
||||
const BREWKEY = 'HB_newPage_content';
|
||||
const STYLEKEY = 'HB_newPage_style';
|
||||
const METAKEY = 'HB_newPage_metadata';
|
||||
const SNIPKEY = 'HB_newPage_snippets';
|
||||
const METAKEY = 'HB_newPage_meta';
|
||||
|
||||
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
|
||||
|
||||
const useLocalStorage = true;
|
||||
const neverSaved = true;
|
||||
const sandbox = true;
|
||||
|
||||
const NewPage = (props)=>{
|
||||
props = {
|
||||
@@ -43,7 +45,6 @@ const NewPage = (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(hbfm.validate(props.brew.text));
|
||||
@@ -51,35 +52,12 @@ const NewPage = (props)=>{
|
||||
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 editorRef = useRef(null);
|
||||
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||
// const saveTimeout = useRef(null);
|
||||
// const warnUnsavedTimeout = useRef(null);
|
||||
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
|
||||
const editorRef = useRef(null);
|
||||
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||
|
||||
useEffect(()=>{
|
||||
loadBrew();
|
||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
const handleControlKeys = (e)=>{
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
if(e.keyCode === 83) trySaveRef.current(true);
|
||||
if(e.keyCode === 80) printCurrentBrew();
|
||||
if([83, 80].includes(e.keyCode)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
|
||||
return ()=>{
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadBrew = ()=>{
|
||||
@@ -111,63 +89,22 @@ const NewPage = (props)=>{
|
||||
window.history.replaceState({}, window.location.title, '/new/');
|
||||
};
|
||||
|
||||
useEffect(()=>{
|
||||
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||
setUnsavedChanges(hasChange);
|
||||
|
||||
if(autoSaveEnabled) trySave(false, hasChange);
|
||||
}, [currentBrew]);
|
||||
|
||||
useEffect(()=>{
|
||||
trySaveRef.current = trySave;
|
||||
unsavedChangesRef.current = unsavedChanges;
|
||||
});
|
||||
|
||||
const handleSplitMove = ()=>{
|
||||
editorRef.current.update();
|
||||
};
|
||||
|
||||
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||
if(subfield == 'renderer' || subfield == 'theme')
|
||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||
|
||||
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||
setHTMLErrors(hbfm.validate(value));
|
||||
|
||||
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||
|
||||
if(useLocalStorage) {
|
||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||
if(field == 'style') localStorage.setItem(STYLEKEY, value);
|
||||
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
|
||||
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
|
||||
renderer : value.renderer,
|
||||
theme : value.theme,
|
||||
lang : value.lang
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const trySave = async ()=>{
|
||||
setIsSaving(true);
|
||||
|
||||
const updatedBrew = { ...currentBrew };
|
||||
splitTextStyleAndMetadata(updatedBrew);
|
||||
|
||||
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm;
|
||||
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
|
||||
const save = async (brew, saveToGoogle)=>{
|
||||
//Prepare content to send to server
|
||||
const brewToSave = {
|
||||
...brew,
|
||||
text : brew.text.normalize('NFC'),
|
||||
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm)) || []).length + 1,
|
||||
textBin : undefined
|
||||
};
|
||||
|
||||
const res = await request
|
||||
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||
.send(updatedBrew)
|
||||
.send(brewToSave)
|
||||
.catch((err)=>{
|
||||
setIsSaving(false);
|
||||
console.error('Error Updating Local Brew');
|
||||
setError(err);
|
||||
});
|
||||
|
||||
setIsSaving(false);
|
||||
if(!res) return;
|
||||
|
||||
const savedBrew = res.body;
|
||||
@@ -175,49 +112,10 @@ const NewPage = (props)=>{
|
||||
localStorage.removeItem(BREWKEY);
|
||||
localStorage.removeItem(STYLEKEY);
|
||||
localStorage.removeItem(METAKEY);
|
||||
window.onbeforeunload = null;
|
||||
window.location = `/edit/${savedBrew.editId}`;
|
||||
};
|
||||
|
||||
const renderSaveButton = ()=>{
|
||||
// #1 - Currently saving, show SAVING
|
||||
if(isSaving)
|
||||
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||
|
||||
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
|
||||
// if(unsavedChanges && warnUnsavedChanges) {
|
||||
// resetWarnUnsavedTimer();
|
||||
// const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
|
||||
// const text = elapsedTime === 0
|
||||
// ? 'Autosave is OFF.'
|
||||
// : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||
|
||||
// return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
|
||||
// Reminder...
|
||||
// <div className='errorContainer'>{text}</div>
|
||||
// </Nav.item>;
|
||||
// }
|
||||
|
||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||
if(unsavedChanges)
|
||||
return <Nav.item className='save' onClick={trySave} color='blue' icon='fas fa-save'>save now</Nav.item>;
|
||||
|
||||
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
||||
if(autoSaveEnabled)
|
||||
return <Nav.item className='save saved'>auto-saved</Nav.item>;
|
||||
|
||||
// #5 - No unsaved changes, and has never been saved, hide the button
|
||||
if(neverSaved)
|
||||
return <Nav.item className='save neverSaved' disabled={true}>save now</Nav.item>;
|
||||
|
||||
// DEFAULT - No unsaved changes, show SAVED
|
||||
return <Nav.item className='save saved'>saved</Nav.item>;
|
||||
};
|
||||
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const renderNavbar = ()=>(
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
@@ -238,6 +136,31 @@ const NewPage = (props)=>{
|
||||
</Navbar>
|
||||
);
|
||||
|
||||
const {
|
||||
handleSplitMove,
|
||||
handleBrewChange,
|
||||
clearError,
|
||||
renderSaveButton
|
||||
} = useCommonEditPageFunctions({
|
||||
saveGoogle,
|
||||
setError,
|
||||
setThemeBundle,
|
||||
HTMLErrors,
|
||||
setHTMLErrors,
|
||||
currentBrew,
|
||||
setCurrentBrew,
|
||||
useLocalStorage,
|
||||
BREWKEY,
|
||||
STYLEKEY,
|
||||
SNIPKEY,
|
||||
METAKEY,
|
||||
hbfm,
|
||||
sandbox,
|
||||
lastSavedBrew,
|
||||
editorRef,
|
||||
save,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='newPage sitePage'>
|
||||
{renderNavbar()}
|
||||
@@ -260,14 +183,11 @@ const NewPage = (props)=>{
|
||||
text={currentBrew.text}
|
||||
style={currentBrew.style}
|
||||
renderer={currentBrew.renderer}
|
||||
theme={currentBrew.theme}
|
||||
themeBundle={themeBundle}
|
||||
errors={HTMLErrors}
|
||||
lang={currentBrew.lang}
|
||||
onPageChange={setCurrentBrewRendererPageNum}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</SplitPane>
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
.newPage {
|
||||
.navItem.save {
|
||||
background-color : @orange;
|
||||
transition:all 0.2s;
|
||||
transition : all 0.2s;
|
||||
&:hover { background-color : @green; }
|
||||
|
||||
&.neverSaved {
|
||||
translate:-100%;
|
||||
opacity: 0;
|
||||
background-color :#333;
|
||||
cursor:auto;
|
||||
cursor : auto;
|
||||
background-color : #333333;
|
||||
opacity : 0;
|
||||
translate : -100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,15 @@ const { both: RecentNavItem } = RecentNavItems;
|
||||
import Account from '@navbar/account.navitem.jsx';
|
||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||
|
||||
import request from '../../utils/request-middleware.js';
|
||||
|
||||
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
|
||||
|
||||
const SharePage = (props)=>{
|
||||
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
|
||||
const { disableMeta = false } = props;
|
||||
|
||||
const [currentBrew, setCurrentBrew] = useState(props.brew || DEFAULT_BREW_LOAD);
|
||||
const [themeBundle, setThemeBundle] = useState({});
|
||||
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||
|
||||
@@ -35,9 +38,33 @@ const SharePage = (props)=>{
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUpdatedBrew = async ()=>{
|
||||
const response = await request
|
||||
.get(`/api/fetch/${currentBrew.shareId}`)
|
||||
.catch((error)=>{
|
||||
console.log('error at fetching updated brew: ', error);
|
||||
});
|
||||
if(response.ok && !!response.body.brew) {
|
||||
setCurrentBrew(response.body.brew);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(()=>{
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme);
|
||||
fetchThemeBundle(undefined, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
// listen for changes in the brew version
|
||||
const eventSource = new EventSource('/stream');
|
||||
eventSource.addEventListener('message', (evt)=>{
|
||||
const messageData = JSON.parse(evt.data);
|
||||
|
||||
if(messageData.eventType == 'brewUpdated'){
|
||||
if(messageData.shareId == currentBrew.shareId && messageData.version != currentBrew.version) {
|
||||
console.log('should fetch brew');
|
||||
fetchUpdatedBrew();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return ()=>{
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
@@ -45,13 +72,13 @@ const SharePage = (props)=>{
|
||||
}, []);
|
||||
|
||||
const processShareId = ()=>{
|
||||
return brew.googleId && !brew.stubbed ? brew.googleId + brew.shareId : brew.shareId;
|
||||
return currentBrew.googleId && !currentBrew.stubbed ? currentBrew.googleId + currentBrew.shareId : currentBrew.shareId;
|
||||
};
|
||||
|
||||
const renderEditLink = ()=>{
|
||||
if(!brew.editId) return null;
|
||||
if(!currentBrew.editId) return null;
|
||||
|
||||
const editLink = brew.googleId && ! brew.stubbed ? brew.googleId + brew.editId : brew.editId;
|
||||
const editLink = currentBrew.googleId && ! currentBrew.stubbed ? currentBrew.googleId + currentBrew.editId : currentBrew.editId;
|
||||
|
||||
return (
|
||||
<Nav.item color='orange' icon='fas fa-pencil-alt' href={`/edit/${editLink}`}>
|
||||
@@ -62,7 +89,7 @@ const SharePage = (props)=>{
|
||||
|
||||
const titleEl = (
|
||||
<Nav.item className='brewTitle' style={disableMeta ? { cursor: 'default' } : {}}>
|
||||
{brew.title}
|
||||
{currentBrew.title}
|
||||
</Nav.item>
|
||||
);
|
||||
|
||||
@@ -71,11 +98,11 @@ const SharePage = (props)=>{
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<Navbar>
|
||||
<Nav.section className='titleSection'>
|
||||
{disableMeta ? titleEl : <MetadataNav brew={brew}>{titleEl}</MetadataNav>}
|
||||
{disableMeta ? titleEl : <MetadataNav brew={currentBrew}>{titleEl}</MetadataNav>}
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
{brew.shareId && (
|
||||
{currentBrew.shareId && (
|
||||
<>
|
||||
<PrintNavItem />
|
||||
<Nav.dropdown>
|
||||
@@ -108,21 +135,20 @@ const SharePage = (props)=>{
|
||||
</Nav.dropdown>
|
||||
</>
|
||||
)}
|
||||
<RecentNavItem brew={brew} storageKey='view' />
|
||||
<RecentNavItem brew={currentBrew} storageKey='view' />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<BrewRenderer
|
||||
text={brew.text}
|
||||
style={brew.style}
|
||||
lang={brew.lang}
|
||||
renderer={brew.renderer}
|
||||
theme={brew.theme}
|
||||
text={currentBrew.text}
|
||||
style={currentBrew.style}
|
||||
lang={currentBrew.lang}
|
||||
renderer={currentBrew.renderer}
|
||||
theme={currentBrew.theme}
|
||||
themeBundle={themeBundle}
|
||||
onPageChange={handleBrewRendererPageChange}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
|
||||
*:not(input) { user-select : none; }
|
||||
|
||||
.form {
|
||||
background:white;
|
||||
}
|
||||
.form { background : white; }
|
||||
|
||||
:where(.content .dataGroup) {
|
||||
width : 100%;
|
||||
@@ -174,8 +172,8 @@
|
||||
height : 100%;
|
||||
max-height : 100%;
|
||||
padding : 70px 50px;
|
||||
overflow-y : scroll;
|
||||
container-type : inline-size;
|
||||
overflow-y : scroll;
|
||||
|
||||
h3 { font-size : 25px; }
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import React, { useState, useEffect, useEffectEvent, useRef } from 'react';
|
||||
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
|
||||
import _ from 'lodash';
|
||||
import Nav from '@navbar/nav.jsx';
|
||||
|
||||
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn';
|
||||
|
||||
const SAVE_TIMEOUT = 10000; //Autosave 10 seconds after last change
|
||||
const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes
|
||||
const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds
|
||||
|
||||
export default function useCommonEditPageFunctions(dependencies) {
|
||||
const {
|
||||
saveGoogle,
|
||||
setError,
|
||||
setThemeBundle,
|
||||
HTMLErrors,
|
||||
setHTMLErrors,
|
||||
currentBrew,
|
||||
setCurrentBrew,
|
||||
useLocalStorage,
|
||||
BREWKEY,
|
||||
STYLEKEY,
|
||||
SNIPKEY,
|
||||
METAKEY,
|
||||
hbfm,
|
||||
sandbox,
|
||||
lastSavedBrew,
|
||||
editorRef,
|
||||
save,
|
||||
} = dependencies;
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [lastSavedTime, setLastSavedTime] = useState(new Date());
|
||||
const [autoSaveEnabled, setAutoSaveEnabled] = useState(!sandbox);
|
||||
const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true);
|
||||
const [unsavedChanges, setUnsavedChanges] = useState(false);
|
||||
|
||||
const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges
|
||||
const warnUnsavedTimeout = useRef(null); // timers live outside React and need ref to consistently track time
|
||||
const saveTimeout = useRef(null);
|
||||
|
||||
//==--------- Page setup ----------==//
|
||||
useEffect(()=>{
|
||||
const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
|
||||
setAutoSaveEnabled(autoSavePref);
|
||||
setWarnUnsavedChanges(!autoSavePref);
|
||||
setHTMLErrors(hbfm.validate(currentBrew.text));
|
||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
const handleControlKeys = (e)=>{
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
if(e.keyCode === 83) trySave(true, true, saveGoogle);
|
||||
if(e.keyCode === 80) printCurrentBrew();
|
||||
if([83, 80].includes(e.keyCode)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
window.onbeforeunload = ()=>{
|
||||
if(unsavedChangesRef.current)
|
||||
return 'You have unsaved changes!';
|
||||
};
|
||||
return ()=>{
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
window.onBeforeUnload = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
//======----- Check for unsaved changes and autosave if enabled -----======
|
||||
useEffect(()=>{
|
||||
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||
setUnsavedChanges(hasChange);
|
||||
unsavedChangesRef.current = hasChange;
|
||||
|
||||
if(autoSaveEnabled) trySave(false, hasChange, saveGoogle);
|
||||
}, [currentBrew]);
|
||||
|
||||
const resetWarnUnsavedTimer = ()=>{
|
||||
setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
|
||||
clearTimeout(warnUnsavedTimeout.current);
|
||||
warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings
|
||||
};
|
||||
|
||||
const handleSplitMove = ()=>{
|
||||
editorRef.current.update();
|
||||
};
|
||||
|
||||
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||
if(subfield == 'renderer' || subfield == 'theme')
|
||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||
|
||||
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||
setHTMLErrors(hbfm.validate(value));
|
||||
|
||||
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||
|
||||
if(useLocalStorage) {
|
||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||
if(field == 'style') localStorage.setItem(STYLEKEY, value);
|
||||
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
|
||||
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
|
||||
renderer : value.renderer,
|
||||
theme : value.theme,
|
||||
lang : value.lang
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAutoSave = ()=>{
|
||||
clearTimeout(warnUnsavedTimeout.current);
|
||||
clearTimeout(saveTimeout.current);
|
||||
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled));
|
||||
setAutoSaveEnabled(!autoSaveEnabled);
|
||||
setWarnUnsavedChanges(autoSaveEnabled);
|
||||
};
|
||||
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const trySave = useEffectEvent((forceSave = false, hasChanges = true, saveToGoogle = false)=>{
|
||||
clearTimeout(saveTimeout.current);
|
||||
if(isSaving) return;
|
||||
if(!forceSave && !hasChanges) return;
|
||||
const newTimeout = forceSave ? 0 : SAVE_TIMEOUT;
|
||||
|
||||
saveTimeout.current = setTimeout(async ()=>{
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
await save(currentBrew, saveToGoogle)
|
||||
.catch((err)=>{
|
||||
setError(err);
|
||||
});
|
||||
setIsSaving(false);
|
||||
setLastSavedTime(new Date());
|
||||
if(!autoSaveEnabled) resetWarnUnsavedTimer();
|
||||
}, newTimeout);
|
||||
});
|
||||
|
||||
const renderSaveButton = ()=>{
|
||||
if(isSaving)
|
||||
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||
|
||||
if(unsavedChanges && warnUnsavedChanges) {
|
||||
resetWarnUnsavedTimer();
|
||||
const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
|
||||
const text = elapsedTime === 0
|
||||
? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.`
|
||||
: `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`;
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
|
||||
Reminder...
|
||||
<div className='errorContainer'>{text}</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(unsavedChanges)
|
||||
return <Nav.item className='save' onClick={()=>trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now</Nav.item>;
|
||||
|
||||
if(autoSaveEnabled)
|
||||
return <Nav.item className='save saved'>auto-saved</Nav.item>;
|
||||
|
||||
if(sandbox)
|
||||
return <Nav.item className='save neverSaved' disabled={true}>save now</Nav.item>;
|
||||
|
||||
return <Nav.item className='save saved'>saved</Nav.item>;
|
||||
};
|
||||
|
||||
return {
|
||||
handleSplitMove,
|
||||
handleBrewChange,
|
||||
toggleAutoSave,
|
||||
clearError,
|
||||
trySave,
|
||||
renderSaveButton,
|
||||
autoSaveEnabled,
|
||||
unsavedChanges,
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"development": true,
|
||||
"development_style": false,
|
||||
"host" : "homebrewery.local.naturalcrit.com:8000",
|
||||
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
||||
"secret" : "secret",
|
||||
|
||||
Generated
+1255
-1572
File diff suppressed because it is too large
Load Diff
+33
-45
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.22.0",
|
||||
"version": "3.23.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"npm": ">=10.8 <12",
|
||||
@@ -39,6 +39,7 @@
|
||||
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
|
||||
"test:helpers": "jest tests/html/helpers.test.js --verbose",
|
||||
"phb": "node --experimental-require-module scripts/phb.js",
|
||||
"prod": "set NODE_ENV=production && npm run build",
|
||||
"postinstall": "npm run build",
|
||||
@@ -86,31 +87,29 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^8.0.1",
|
||||
"@babel/plugin-transform-runtime": "^8.0.1",
|
||||
"@babel/preset-env": "^8.0.2",
|
||||
"@babel/core": "^8.0.6",
|
||||
"@babel/plugin-transform-runtime": "^8.0.6",
|
||||
"@babel/preset-env": "^8.0.6",
|
||||
"@babel/preset-react": "^8.0.1",
|
||||
"@babel/runtime": "^8.0.0",
|
||||
"@babel/runtime": "^8.0.5",
|
||||
"@codemirror/autocomplete": "^6.20.3",
|
||||
"@codemirror/commands": "^6.11.0",
|
||||
"@codemirror/highlight": "^0.19.8",
|
||||
"@codemirror/commands": "^6.11.1",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-html": "^6.4.12",
|
||||
"@codemirror/lang-markdown": "^6.5.2",
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.43.9",
|
||||
"@dmsnell/diff-match-patch": "^1.1.0",
|
||||
"@googleapis/drive": "^21.0.0",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@codemirror/state": "^6.7.6",
|
||||
"@codemirror/view": "^6.43.13",
|
||||
"@googleapis/drive": "^26.0.1",
|
||||
"@lezer/highlight": "^1.2.4",
|
||||
"@oddbird/css-anchor-positioning": "^0.10.2",
|
||||
"@sanity/diff-match-patch": "^3.2.0",
|
||||
"@sanity/diff-match-patch": "^3.2.1",
|
||||
"@vitejs/plugin-react": "^6.0.5",
|
||||
"body-parser": "^2.3.0",
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror-5-themes": "^1.5.1",
|
||||
"codemirror-5-themes": "^1.5.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"core-js": "^3.50.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -118,58 +117,47 @@
|
||||
"dedent": "^1.7.2",
|
||||
"express": "^5.1.0",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "3.0.1",
|
||||
"express-static-gzip": "3.0.2",
|
||||
"fflate": "^0.8.3",
|
||||
"fs-extra": "^11.3.5",
|
||||
"fs-extra": "^11.4.1",
|
||||
"hash-wasm": "^4.12.0",
|
||||
"hbmarkedwrapper": "^1.0.0",
|
||||
"idb-keyval": "^6.2.5",
|
||||
"js-yaml": "^5.3.0",
|
||||
"js-yaml": "^5.4.2",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^4.8.1",
|
||||
"less": "^4.9.1",
|
||||
"lodash": "^4.18.1",
|
||||
"marked": "15.0.12",
|
||||
"marked-alignment-paragraphs": "^1.0.0",
|
||||
"marked-definition-lists": "^1.0.1",
|
||||
"marked-diagrams-markdeep": "^1.0.1",
|
||||
"marked-emoji": "^2.0.3",
|
||||
"marked-extended-tables": "^2.0.1",
|
||||
"marked-gfm-heading-id": "^4.1.4",
|
||||
"marked-nonbreaking-spaces": "^1.0.1",
|
||||
"marked-smartypants-lite": "^1.0.3",
|
||||
"marked-subsuper-text": "^1.0.4",
|
||||
"marked-variables": "^1.0.5",
|
||||
"marked-hbfm": "^1.0.1",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.30.1",
|
||||
"mongoose": "^9.9.3",
|
||||
"moment": "^2.31.0",
|
||||
"mongoose": "^9.10.2",
|
||||
"nanoid": "6.0.1",
|
||||
"nconf": "^0.13.0",
|
||||
"node": "^26.7.0",
|
||||
"prettier": "^3.8.1",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"node": "^26.10.0",
|
||||
"prettier": "^3.9.9",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-frame-component": "^5.3.2",
|
||||
"react-router": "^8.3.0",
|
||||
"react-router": "^8.4.0",
|
||||
"sanitize-filename": "1.6.4",
|
||||
"superagent": "^10.2.1"
|
||||
"superagent": "^10.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/stylelint-plugin": "^5.3.0",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-jest": "^30.5.2",
|
||||
"babel-plugin-transform-import-meta": "^3.0.0",
|
||||
"eslint": "9.7",
|
||||
"eslint-plugin-jest": "^29.15.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.4.0",
|
||||
"jest": "^30.4.2",
|
||||
"jest": "^30.5.2",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jsdom": "^30.0.1",
|
||||
"jsdom": "^30.1.1",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^17.11.1",
|
||||
"stylelint": "^17.15.0",
|
||||
"stylelint-config-recess-order": "^7.7.0",
|
||||
"stylelint-config-recommended": "^18.0.0",
|
||||
"supertest": "^7.1.4",
|
||||
"vite": "^8.2.1"
|
||||
"supertest": "^7.3.0",
|
||||
"vite": "^8.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
+9
-12
@@ -13,7 +13,6 @@ const isProd = nodeEnv === 'production';
|
||||
|
||||
import HomebrewAPI from './homebrew.api.js';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
|
||||
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
||||
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
|
||||
@@ -38,14 +37,14 @@ export default function createAdminApi(vite) {
|
||||
throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Search for up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
|
||||
const junkBrewsPipeline = [
|
||||
{ $match : {
|
||||
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
|
||||
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
|
||||
} },
|
||||
{ $project: { _id: 1, textBinSize: { $binarySize: '$textBin' }, updatedAt: 1, lastViewed: 1} },
|
||||
{ $project: { _id: 1, textBinSize: { $binarySize: '$textBin' }, updatedAt: 1, lastViewed: 1 } },
|
||||
{ $match: { textBinSize: { $lt: 140 } } },
|
||||
{ $limit: 300 }
|
||||
];
|
||||
@@ -53,14 +52,14 @@ export default function createAdminApi(vite) {
|
||||
// Search for up to 500 unauthored brews that have not been viewed or updated in two years
|
||||
const lostBrewsPipeline = [
|
||||
{
|
||||
$match: {
|
||||
authors: [],
|
||||
updatedAt: { $lt: Moment().subtract(2, 'years').toDate() },
|
||||
lastViewed: { $lt: Moment().subtract(2, 'years').toDate() }
|
||||
$match : {
|
||||
authors : [],
|
||||
updatedAt : { $lt: Moment().subtract(2, 'years').toDate() },
|
||||
lastViewed : { $lt: Moment().subtract(2, 'years').toDate() }
|
||||
}
|
||||
},
|
||||
{
|
||||
$limit: 500
|
||||
$limit : 500
|
||||
}
|
||||
];
|
||||
|
||||
@@ -71,7 +70,7 @@ export default function createAdminApi(vite) {
|
||||
|
||||
router.get('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{
|
||||
HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||
.then((objs)=>res.json({ count: objs.length, brewCollection : objs }))
|
||||
.then((objs)=>res.json({ count: objs.length, brewCollection: objs }))
|
||||
.catch((error)=>{
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
@@ -94,7 +93,7 @@ export default function createAdminApi(vite) {
|
||||
|
||||
router.get('/admin/cleanupLost', mw.adminOnly, (req, res)=>{
|
||||
HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 })
|
||||
.then((objs)=>res.json({ count: objs.length, brewCollection : objs }))
|
||||
.then((objs)=>res.json({ count: objs.length, brewCollection: objs }))
|
||||
.catch((error)=>{
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
@@ -148,8 +147,6 @@ export default function createAdminApi(vite) {
|
||||
brew[property] = cleanText(brew[property]);
|
||||
});
|
||||
|
||||
splitTextStyleAndMetadata(brew);
|
||||
|
||||
req.body = brew;
|
||||
|
||||
// Remove Account from request to prevent Admin user from being added to brew as an Author
|
||||
|
||||
+52
-356
@@ -1,4 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
|
||||
// Set working directory to project root
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
@@ -14,25 +14,25 @@ import express from 'express';
|
||||
import config from './config.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
|
||||
import api from './homebrew.api.js';
|
||||
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = api;
|
||||
const { homebrewApi, getBrew, getCSS } = api;
|
||||
import adminApi from './admin.api.js';
|
||||
import vaultApi from './vault.api.js';
|
||||
import GoogleActions from './googleActions.js';
|
||||
import pageRoutes from './page-routes.js';
|
||||
|
||||
import serveCompressedStaticAssets from './static-assets.mv.js';
|
||||
import sanitizeFilename from 'sanitize-filename';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { model as HomebrewModel } from './homebrew.model.js';
|
||||
|
||||
import { DEFAULT_BREW } from './brewDefaults.js';
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
|
||||
//==== Middleware Imports ====//
|
||||
import contentNegotiation from './middleware/content-negotiation.js';
|
||||
import bodyParser from 'body-parser';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import forceSSL from './forcessl.mw.js';
|
||||
|
||||
import Stream from './eventStreamSource.js';
|
||||
import dbCheck from './middleware/dbCheck.js';
|
||||
|
||||
import cors from 'cors';
|
||||
@@ -116,12 +116,6 @@ export default async function createApp(vite) {
|
||||
app.use(adminApi(vite));
|
||||
app.use(vaultApi);
|
||||
|
||||
const welcomeText = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||
const welcomeTextLegacy = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
|
||||
const migrateText = fs.readFileSync('./client/homebrew/pages/homePage/migrate.md', 'utf8');
|
||||
const changelogText = fs.readFileSync('changelog.md', 'utf8');
|
||||
const faqText = fs.readFileSync('faq.md', 'utf8');
|
||||
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
|
||||
const defaultMetaTags = {
|
||||
@@ -132,133 +126,25 @@ export default async function createApp(vite) {
|
||||
type : 'website'
|
||||
};
|
||||
|
||||
app.use(pageRoutes({
|
||||
defaultMetaTags,
|
||||
HomebrewModel,
|
||||
sanitizeBrew,
|
||||
}));
|
||||
|
||||
//Robots.txt
|
||||
app.get('/robots.txt', (req, res)=>{
|
||||
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
||||
});
|
||||
|
||||
//Home page
|
||||
app.get('/', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeText,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Homepage',
|
||||
description : 'Homepage'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//Home page Legacy
|
||||
app.get('/legacy', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeTextLegacy,
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Homepage (Legacy)',
|
||||
description : 'Homepage'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//Legacy/Other Document -> v3 Migration Guide
|
||||
app.get('/migrate', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : migrateText,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'v3 Migration Guide',
|
||||
description : 'A brief guide to converting Legacy documents to the v3 renderer.'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//Changelog page
|
||||
app.get('/changelog', async (req, res, next)=>{
|
||||
req.brew = {
|
||||
title : 'Changelog',
|
||||
text : changelogText,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Changelog',
|
||||
description : 'Development changelog.'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//FAQ page
|
||||
app.get('/faq', async (req, res, next)=>{
|
||||
req.brew = {
|
||||
title : 'FAQ',
|
||||
text : faqText,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'FAQ',
|
||||
description : 'Frequently Asked Questions'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//Source page
|
||||
app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||
//serve brew for sharepage rerender
|
||||
app.get('/api/fetch/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res)=>{
|
||||
const { brew } = req;
|
||||
|
||||
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
||||
let text = brew.text;
|
||||
for (const replaceStr in replaceStrings) {
|
||||
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
|
||||
}
|
||||
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
||||
res.status(200).send(text);
|
||||
});
|
||||
|
||||
//Download brew source page
|
||||
app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||
const { brew } = req;
|
||||
sanitizeBrew(brew, 'share');
|
||||
const prefix = 'HB - ';
|
||||
|
||||
const encodeRFC3986ValueChars = (str)=>{
|
||||
return (
|
||||
encodeURIComponent(str)
|
||||
.replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
|
||||
);
|
||||
};
|
||||
|
||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/plain',
|
||||
'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
|
||||
});
|
||||
res.status(200).send(brew.text);
|
||||
});
|
||||
brew.authors.includes(req.account?.username)
|
||||
? sanitizeBrew(brew, 'shareAuthor')
|
||||
: sanitizeBrew(brew, 'share');
|
||||
splitTextStyleAndMetadata(brew);
|
||||
res.json({ brew });
|
||||
}));
|
||||
|
||||
//Serve brew metadata
|
||||
app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||
@@ -280,78 +166,6 @@ export default async function createApp(vite) {
|
||||
//Serve brew styling
|
||||
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
|
||||
|
||||
//User Page
|
||||
app.get('/user/:username', dbCheck, async (req, res, next)=>{
|
||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `${req.params.username}'s Collection`,
|
||||
description : 'View my collection of homebrew on the Homebrewery.'
|
||||
// type : could be 'profile'?
|
||||
};
|
||||
|
||||
const fields = [
|
||||
'googleId',
|
||||
'title',
|
||||
'pageCount',
|
||||
'description',
|
||||
'authors',
|
||||
'lang',
|
||||
'published',
|
||||
'views',
|
||||
'shareId',
|
||||
'editId',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'lastViewed',
|
||||
'thumbnail',
|
||||
'tags'
|
||||
];
|
||||
|
||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
||||
|
||||
if(ownAccount && req?.account?.googleId){
|
||||
const auth = await GoogleActions.authCheck(req.account, res);
|
||||
let googleBrews = await GoogleActions.listGoogleBrews(auth)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
// If stub matches file from Google, use Google metadata over stub metadata
|
||||
if(googleBrews && googleBrews.length > 0) {
|
||||
for (const brew of brews.filter((brew)=>brew.googleId)) {
|
||||
const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
|
||||
if(match !== -1) {
|
||||
brew.googleId = googleBrews[match].googleId;
|
||||
brew.pageCount = googleBrews[match].pageCount;
|
||||
brew.renderer = googleBrews[match].renderer;
|
||||
brew.version = googleBrews[match].version;
|
||||
brew.webViewLink = googleBrews[match].webViewLink;
|
||||
googleBrews.splice(match, 1);
|
||||
}
|
||||
}
|
||||
|
||||
//Remaining unstubbed google brews display current user as author
|
||||
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
||||
brews = _.concat(brews, googleBrews);
|
||||
}
|
||||
}
|
||||
|
||||
req.brews = _.map(brews, (brew)=>{
|
||||
// Clean up brew data
|
||||
brew.title = brew.title?.trim();
|
||||
brew.description = brew.description?.trim();
|
||||
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
//Change author name on brews
|
||||
app.put('/api/user/rename', dbCheck, async (req, res)=>{
|
||||
const { username, newUsername } = req.body;
|
||||
@@ -380,143 +194,26 @@ export default async function createApp(vite) {
|
||||
}
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||
// Create Event Stream source for pages to listen to
|
||||
app.get('/stream', (req, res)=>{
|
||||
res.writeHead(200, {
|
||||
'Content-Type' : 'text/event-stream',
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Connection' : 'keep-alive',
|
||||
'Content-Encoding' : 'none'
|
||||
});
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
Stream.on('sendUpdate', (event, data)=>{
|
||||
console.log('Event:', event, '\nData:', data);
|
||||
res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`);
|
||||
});
|
||||
});
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
locale : req.brew.lang,
|
||||
type : 'article'
|
||||
};
|
||||
// After Stream starts, send initStream event
|
||||
setTimeout(()=>{
|
||||
Stream.emit('sendUpdate', 'initStream', { time: new Date });
|
||||
}, 1000);
|
||||
|
||||
sanitizeBrew(req.brew, 'edit');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||
return next();
|
||||
}));
|
||||
|
||||
//New Page from ID
|
||||
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
|
||||
sanitizeBrew(req.brew, 'share');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
const brew = {
|
||||
shareId : req.brew.shareId,
|
||||
title : `CLONE - ${req.brew.title}`,
|
||||
text : req.brew.text,
|
||||
style : req.brew.style,
|
||||
renderer : req.brew.renderer,
|
||||
theme : req.brew.theme,
|
||||
tags : req.brew.tags,
|
||||
snippets : req.brew.snippets
|
||||
};
|
||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new', asyncHandler(async(req, res, next)=>{
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||
const { brew } = req;
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
// increase visitor view count, do not include visits by author(s)
|
||||
if(!brew.authors.includes(req.account?.username)){
|
||||
if(req.params.id.length > 12 && !brew._id) {
|
||||
const googleId = brew.googleId;
|
||||
const shareId = brew.shareId;
|
||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||
.catch((err)=>{next(err);});
|
||||
} else {
|
||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||
}
|
||||
};
|
||||
|
||||
brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Account Page
|
||||
app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
|
||||
const data = {};
|
||||
data.title = 'Account Information Page';
|
||||
|
||||
if(!req.account) {
|
||||
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
||||
const error = new Error('No valid account');
|
||||
error.status = 401;
|
||||
error.HBErrorCode = '50';
|
||||
error.page = data.title;
|
||||
return next(error);
|
||||
};
|
||||
|
||||
let auth;
|
||||
let googleCount = [];
|
||||
if(req.account) {
|
||||
if(req.account.googleId) {
|
||||
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||
|
||||
googleCount = await GoogleActions.listGoogleBrews(auth)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return 0;
|
||||
});
|
||||
|
||||
data.accountDetails = {
|
||||
username : req.account.username,
|
||||
issued : req.account.issued,
|
||||
googleId : Boolean(req.account.googleId),
|
||||
authCheck : Boolean(req.account.googleId && auth?.credentials.access_token),
|
||||
mongoCount : mongoCount,
|
||||
googleCount : googleCount?.length
|
||||
};
|
||||
}
|
||||
|
||||
req.brew = data;
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `Account Page`,
|
||||
description : null
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
// Local only
|
||||
if(isLocalEnvironment){
|
||||
@@ -534,15 +231,6 @@ export default async function createApp(vite) {
|
||||
app.use('/staticImages', express.static(config.get('hb_images') && fs.existsSync(config.get('hb_images')) ? config.get('hb_images') :'staticImages'));
|
||||
app.use('/staticFonts', express.static(config.get('hb_fonts') && fs.existsSync(config.get('hb_fonts')) ? config.get('hb_fonts'):'staticFonts'));
|
||||
|
||||
//Vault Page
|
||||
app.get('/vault', asyncHandler(async(req, res, next)=>{
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'The Vault',
|
||||
description : 'Search for Brews'
|
||||
};
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Send rendered page
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
||||
@@ -557,11 +245,12 @@ export default async function createApp(vite) {
|
||||
|
||||
// Create configuration object
|
||||
const configuration = {
|
||||
local : isLocalEnvironment,
|
||||
publicUrl : config.get('publicUrl') ?? '',
|
||||
baseUrl : `${req.protocol}://${req.get('host')}`,
|
||||
environment : nodeEnv,
|
||||
deployment : config.get('heroku_app_name') ?? ''
|
||||
local : isLocalEnvironment,
|
||||
publicUrl : config.get('publicUrl') ?? '',
|
||||
baseUrl : `${req.protocol}://${req.get('host')}`,
|
||||
environment : nodeEnv,
|
||||
deployment : config.get('heroku_app_name') ?? '',
|
||||
developmentStyle : config.get('development_style')
|
||||
};
|
||||
const props = {
|
||||
version : version,
|
||||
@@ -591,9 +280,16 @@ export default async function createApp(vite) {
|
||||
html = await vite.transformIndexHtml(req.originalUrl, html);
|
||||
}
|
||||
|
||||
const safeProps = JSON.stringify(props).replace(/<(?=\/?script)/ig, '\\u003c');
|
||||
html = html.replace(
|
||||
'<head>',
|
||||
()=>{ return `<head>\n<script id="props" >window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>\n${ogMetaTags}`; }
|
||||
()=>{ //Replace must use a function instead of string, else brews with `$` in contents are treated as replacement patterns
|
||||
return `<head>\n`
|
||||
+ `<script id="props">`
|
||||
+ `window.__INITIAL_PROPS__ = ${safeProps
|
||||
}</script>\n${
|
||||
ogMetaTags}`;
|
||||
}
|
||||
);
|
||||
|
||||
return html;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
const Stream = new EventEmitter;
|
||||
|
||||
export default {
|
||||
emit : function(event) {return Stream.emit(event, ...([...arguments].slice(1)));}, // Arguments doesn't work for arrow functions
|
||||
on : (event, listener)=>{return Stream.on(event, listener);},
|
||||
off : (event, listener)=>{return Stream.off(event, listener);}
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { model as HomebrewModel } from './homebrew.model.js';
|
||||
import express from 'express';
|
||||
import zlib from 'zlib';
|
||||
import GoogleActions from './googleActions.js';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
import * as yaml from 'js-yaml';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { nanoid } from 'nanoid';
|
||||
@@ -21,6 +21,8 @@ const router = express.Router();
|
||||
import { DEFAULT_BREW, DEFAULT_BREW_LOAD } from './brewDefaults.js';
|
||||
import Themes from '../themes/themes.json' with { type: 'json' };
|
||||
|
||||
import Stream from './eventStreamSource.js';
|
||||
|
||||
const isStaticTheme = (renderer, themeName)=>{
|
||||
return Themes[renderer]?.[themeName] !== undefined;
|
||||
};
|
||||
@@ -168,8 +170,7 @@ const api = {
|
||||
|
||||
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
|
||||
.catch((googleError)=>{
|
||||
const reason = googleError.errors?.[0].reason;
|
||||
if(reason == 'notFound')
|
||||
if(googleError.code === 404 || googleError.status === 404)
|
||||
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
|
||||
else
|
||||
throw { ...googleError, HBErrorCode: '01' };
|
||||
@@ -192,13 +193,13 @@ const api = {
|
||||
|
||||
const fixedStub = migrateSystemsToTags(stub);
|
||||
req.brew = fixedStub;
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
next();
|
||||
};
|
||||
},
|
||||
getCSS : async (req, res)=>{
|
||||
const { brew } = req;
|
||||
if(!brew) return res.status(404).send('');
|
||||
splitTextStyleAndMetadata(brew);
|
||||
if(!brew.style) return res.status(404).send('');
|
||||
|
||||
res.set({
|
||||
@@ -344,7 +345,6 @@ const api = {
|
||||
});
|
||||
|
||||
currentTheme = req.brew;
|
||||
splitTextStyleAndMetadata(currentTheme);
|
||||
if(!currentTheme.tags.some((tag)=>tag === 'meta:theme' || tag === 'meta:Theme'))
|
||||
throw { brewId: req.params.id, name: 'Invalid Theme Selected', message: 'Selected theme does not have the meta:theme tag', status: 422, HBErrorCode: '10' };
|
||||
themeName ??= currentTheme.title;
|
||||
@@ -383,7 +383,6 @@ const api = {
|
||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||
const brewFromServer = req.brew;
|
||||
splitTextStyleAndMetadata(brewFromServer);
|
||||
|
||||
if(brewFromServer?.version !== brewFromClient?.version){
|
||||
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||
@@ -501,6 +500,8 @@ const api = {
|
||||
|
||||
saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth
|
||||
|
||||
Stream.emit('sendUpdate', 'brewUpdated', { time: new Date, shareId: brew.shareId, version: brew.version });
|
||||
|
||||
res.status(200).send(saved);
|
||||
},
|
||||
deleteGoogleBrew : async (account, id, editId, res)=>{
|
||||
@@ -574,9 +575,9 @@ const api = {
|
||||
router.use(dbCheck);
|
||||
|
||||
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); //alt endpoint, unused
|
||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew)); //alt endpoint, unused
|
||||
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||
|
||||
|
||||
+111
-111
@@ -360,25 +360,25 @@ describe('Tests for api', ()=>{
|
||||
trashed : false,
|
||||
updatedAt : undefined,
|
||||
views : 0,
|
||||
bleedSize: {
|
||||
top: '.125in',
|
||||
bottom: '.125in',
|
||||
inner: '.125in',
|
||||
outer: '.125in',
|
||||
bleedSize : {
|
||||
top : '.125in',
|
||||
bottom : '.125in',
|
||||
inner : '.125in',
|
||||
outer : '.125in',
|
||||
},
|
||||
columns: '2',
|
||||
columnGutter: '.125in',
|
||||
legalAuthors: '',
|
||||
license: 'None',
|
||||
safetySpace: {
|
||||
top: '.25in',
|
||||
bottom: '.25in',
|
||||
outer: '.25in',
|
||||
inner: '.5in',
|
||||
columns : '2',
|
||||
columnGutter : '.125in',
|
||||
legalAuthors : '',
|
||||
license : 'None',
|
||||
safetySpace : {
|
||||
top : '.25in',
|
||||
bottom : '.25in',
|
||||
outer : '.25in',
|
||||
inner : '.5in',
|
||||
},
|
||||
trimSize: {
|
||||
width: '8.5in',
|
||||
height: '11in',
|
||||
trimSize : {
|
||||
width : '8.5in',
|
||||
height : '11in',
|
||||
},
|
||||
});
|
||||
expect(next).toHaveBeenCalled();
|
||||
@@ -471,25 +471,25 @@ describe('Tests for api', ()=>{
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
googleId : '12345',
|
||||
bleedSize: {
|
||||
top: '.125in',
|
||||
bottom: '.125in',
|
||||
inner: '.125in',
|
||||
outer: '.125in',
|
||||
bleedSize : {
|
||||
top : '.125in',
|
||||
bottom : '.125in',
|
||||
inner : '.125in',
|
||||
outer : '.125in',
|
||||
},
|
||||
columns: '2',
|
||||
columnGutter: '.125in',
|
||||
legalAuthors: '',
|
||||
license: 'None',
|
||||
safetySpace: {
|
||||
top: '.25in',
|
||||
bottom: '.25in',
|
||||
outer: '.25in',
|
||||
inner: '.5in',
|
||||
columns : '2',
|
||||
columnGutter : '.125in',
|
||||
legalAuthors : '',
|
||||
license : 'None',
|
||||
safetySpace : {
|
||||
top : '.25in',
|
||||
bottom : '.25in',
|
||||
outer : '.25in',
|
||||
inner : '.5in',
|
||||
},
|
||||
trimSize: {
|
||||
width: '8.5in',
|
||||
height: '11in',
|
||||
trimSize : {
|
||||
width : '8.5in',
|
||||
height : '11in',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -534,25 +534,25 @@ brew`);
|
||||
renderer : 'v3',
|
||||
theme : 'phb',
|
||||
googleId : '12345',
|
||||
bleedSize: {
|
||||
top: '.125in',
|
||||
bottom: '.125in',
|
||||
inner: '.125in',
|
||||
outer: '.125in',
|
||||
bleedSize : {
|
||||
top : '.125in',
|
||||
bottom : '.125in',
|
||||
inner : '.125in',
|
||||
outer : '.125in',
|
||||
},
|
||||
columns: '2',
|
||||
columnGutter: '.125in',
|
||||
legalAuthors: '',
|
||||
license: 'None',
|
||||
safetySpace: {
|
||||
top: '.25in',
|
||||
bottom: '.25in',
|
||||
outer: '.25in',
|
||||
inner: '.5in',
|
||||
columns : '2',
|
||||
columnGutter : '.125in',
|
||||
legalAuthors : '',
|
||||
license : 'None',
|
||||
safetySpace : {
|
||||
top : '.25in',
|
||||
bottom : '.25in',
|
||||
outer : '.25in',
|
||||
inner : '.5in',
|
||||
},
|
||||
trimSize: {
|
||||
width: '8.5in',
|
||||
height: '11in',
|
||||
trimSize : {
|
||||
width : '8.5in',
|
||||
height : '11in',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -687,16 +687,16 @@ brew`);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
_id : '1',
|
||||
authors : ['test user'],
|
||||
createdAt : undefined,
|
||||
description : '',
|
||||
editId : expect.any(String),
|
||||
gDrive : false,
|
||||
pageCount : 1,
|
||||
published : false,
|
||||
renderer : 'V3',
|
||||
lang : 'en',
|
||||
_id : '1',
|
||||
authors : ['test user'],
|
||||
createdAt : undefined,
|
||||
description : '',
|
||||
editId : expect.any(String),
|
||||
gDrive : false,
|
||||
pageCount : 1,
|
||||
published : false,
|
||||
renderer : 'V3',
|
||||
lang : 'en',
|
||||
shareId : expect.any(String),
|
||||
style : undefined,
|
||||
tags : [],
|
||||
@@ -708,25 +708,25 @@ brew`);
|
||||
trashed : false,
|
||||
updatedAt : undefined,
|
||||
views : 0,
|
||||
bleedSize: {
|
||||
top: '.125in',
|
||||
bottom: '.125in',
|
||||
inner: '.125in',
|
||||
outer: '.125in',
|
||||
bleedSize : {
|
||||
top : '.125in',
|
||||
bottom : '.125in',
|
||||
inner : '.125in',
|
||||
outer : '.125in',
|
||||
},
|
||||
columns: '2',
|
||||
columnGutter: '.125in',
|
||||
legalAuthors: '',
|
||||
license: 'None',
|
||||
safetySpace: {
|
||||
top: '.25in',
|
||||
bottom: '.25in',
|
||||
outer: '.25in',
|
||||
inner: '.5in',
|
||||
columns : '2',
|
||||
columnGutter : '.125in',
|
||||
legalAuthors : '',
|
||||
license : 'None',
|
||||
safetySpace : {
|
||||
top : '.25in',
|
||||
bottom : '.25in',
|
||||
outer : '.25in',
|
||||
inner : '.5in',
|
||||
},
|
||||
trimSize: {
|
||||
width: '8.5in',
|
||||
height: '11in',
|
||||
trimSize : {
|
||||
width : '8.5in',
|
||||
height : '11in',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -765,16 +765,16 @@ brew`);
|
||||
expect(google.newGoogleBrew).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({
|
||||
_id : '1',
|
||||
authors : ['test user'],
|
||||
bleedSize: {
|
||||
top: '.125in',
|
||||
bottom: '.125in',
|
||||
inner: '.125in',
|
||||
outer: '.125in',
|
||||
_id : '1',
|
||||
authors : ['test user'],
|
||||
bleedSize : {
|
||||
top : '.125in',
|
||||
bottom : '.125in',
|
||||
inner : '.125in',
|
||||
outer : '.125in',
|
||||
},
|
||||
columns: '2',
|
||||
columnGutter: '.125in',
|
||||
columns : '2',
|
||||
columnGutter : '.125in',
|
||||
createdAt : undefined,
|
||||
description : '',
|
||||
editId : expect.any(String),
|
||||
@@ -784,29 +784,29 @@ brew`);
|
||||
renderer : 'V3',
|
||||
lang : 'en',
|
||||
shareId : expect.any(String),
|
||||
legalAuthors: '',
|
||||
license: 'None',
|
||||
safetySpace: {
|
||||
top: '.25in',
|
||||
bottom: '.25in',
|
||||
outer: '.25in',
|
||||
inner: '.5in',
|
||||
legalAuthors : '',
|
||||
license : 'None',
|
||||
safetySpace : {
|
||||
top : '.25in',
|
||||
bottom : '.25in',
|
||||
outer : '.25in',
|
||||
inner : '.5in',
|
||||
},
|
||||
googleId : expect.any(String),
|
||||
style : undefined,
|
||||
tags : [],
|
||||
text : undefined,
|
||||
textBin : undefined,
|
||||
theme : '5ePHB',
|
||||
thumbnail : '',
|
||||
title : 'asdf',
|
||||
trashed : false,
|
||||
trimSize: {
|
||||
width: '8.5in',
|
||||
height: '11in',
|
||||
googleId : expect.any(String),
|
||||
style : undefined,
|
||||
tags : [],
|
||||
text : undefined,
|
||||
textBin : undefined,
|
||||
theme : '5ePHB',
|
||||
thumbnail : '',
|
||||
title : 'asdf',
|
||||
trashed : false,
|
||||
trimSize : {
|
||||
width : '8.5in',
|
||||
height : '11in',
|
||||
},
|
||||
updatedAt : undefined,
|
||||
views : 0
|
||||
updatedAt : undefined,
|
||||
views : 0
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1031,7 +1031,7 @@ brew`);
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('deleteGoogleBrew', ()=>{
|
||||
it('should check auth and delete brew', async ()=>{
|
||||
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
|
||||
@@ -1261,7 +1261,7 @@ brew`);
|
||||
expect(saved.googleId).toEqual(brew.googleId);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Split Text, Style, and Metadata', ()=>{
|
||||
|
||||
it('basic splitting', async ()=>{
|
||||
@@ -1372,5 +1372,5 @@ brew`);
|
||||
expect(testBrew.text).toEqual('text\n');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
// page-routes.js
|
||||
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
process.chdir(`${__dirname}/..`);
|
||||
|
||||
import _ from 'lodash';
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import fs from 'fs';
|
||||
|
||||
//==== Middleware Imports ====//
|
||||
import dbCheck from './middleware/dbCheck.js';
|
||||
import sanitizeFilename from 'sanitize-filename';
|
||||
import { DEFAULT_BREW } from './brewDefaults.js';
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
import GoogleActions from './googleActions.js';
|
||||
|
||||
import api from './homebrew.api.js';
|
||||
const { getBrew, getUsersBrewThemes } = api;
|
||||
|
||||
const welcomeText = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||
const welcomeTextLegacy = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
|
||||
const migrateText = fs.readFileSync('./client/homebrew/pages/homePage/migrate.md', 'utf8');
|
||||
const changelogText = fs.readFileSync('changelog.md', 'utf8');
|
||||
const faqText = fs.readFileSync('faq.md', 'utf8');
|
||||
|
||||
export default function pageRoutes({
|
||||
defaultMetaTags,
|
||||
HomebrewModel,
|
||||
sanitizeBrew,
|
||||
}) {
|
||||
const app = express.Router();
|
||||
|
||||
//Home page
|
||||
app.get('/', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeText,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Homepage',
|
||||
description : 'Homepage'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//Home page Legacy
|
||||
app.get('/legacy', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeTextLegacy,
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Homepage (Legacy)',
|
||||
description : 'Homepage'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//Legacy/Other Document -> v3 Migration Guide
|
||||
app.get('/migrate', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : migrateText,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'v3 Migration Guide',
|
||||
description : 'A brief guide to converting Legacy documents to the v3 renderer.'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//Changelog page
|
||||
app.get('/changelog', async (req, res, next)=>{
|
||||
req.brew = {
|
||||
title : 'Changelog',
|
||||
text : changelogText,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Changelog',
|
||||
description : 'Development changelog.'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//FAQ page
|
||||
app.get('/faq', async (req, res, next)=>{
|
||||
req.brew = {
|
||||
title : 'FAQ',
|
||||
text : faqText,
|
||||
renderer : 'V3',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'FAQ',
|
||||
description : 'Frequently Asked Questions'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//Source page
|
||||
app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||
const { brew } = req;
|
||||
|
||||
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
||||
let text = brew.text;
|
||||
for (const replaceStr in replaceStrings) {
|
||||
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
|
||||
}
|
||||
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
||||
res.status(200).send(text);
|
||||
});
|
||||
|
||||
//Download brew source page
|
||||
app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||
const { brew } = req;
|
||||
sanitizeBrew(brew, 'share');
|
||||
const prefix = 'HB - ';
|
||||
|
||||
const encodeRFC3986ValueChars = (str)=>{
|
||||
return (
|
||||
encodeURIComponent(str)
|
||||
.replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
|
||||
);
|
||||
};
|
||||
|
||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/plain',
|
||||
'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
|
||||
});
|
||||
res.status(200).send(brew.text);
|
||||
});
|
||||
|
||||
//User Page
|
||||
app.get('/user/:username', dbCheck, async (req, res, next)=>{
|
||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `${req.params.username}'s Collection`,
|
||||
description : 'View my collection of homebrew on the Homebrewery.'
|
||||
// type : could be 'profile'?
|
||||
};
|
||||
|
||||
const fields = [
|
||||
'googleId',
|
||||
'title',
|
||||
'pageCount',
|
||||
'description',
|
||||
'authors',
|
||||
'lang',
|
||||
'published',
|
||||
'views',
|
||||
'shareId',
|
||||
'editId',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'lastViewed',
|
||||
'thumbnail',
|
||||
'tags'
|
||||
];
|
||||
|
||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
||||
|
||||
if(ownAccount && req?.account?.googleId){
|
||||
const auth = await GoogleActions.authCheck(req.account, res);
|
||||
let googleBrews = await GoogleActions.listGoogleBrews(auth)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
// If stub matches file from Google, use Google metadata over stub metadata
|
||||
if(googleBrews && googleBrews.length > 0) {
|
||||
for (const brew of brews.filter((brew)=>brew.googleId)) {
|
||||
const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
|
||||
if(match !== -1) {
|
||||
brew.googleId = googleBrews[match].googleId;
|
||||
brew.pageCount = googleBrews[match].pageCount;
|
||||
brew.renderer = googleBrews[match].renderer;
|
||||
brew.version = googleBrews[match].version;
|
||||
brew.webViewLink = googleBrews[match].webViewLink;
|
||||
googleBrews.splice(match, 1);
|
||||
}
|
||||
}
|
||||
|
||||
//Remaining unstubbed google brews display current user as author
|
||||
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
||||
brews = _.concat(brews, googleBrews);
|
||||
}
|
||||
}
|
||||
|
||||
req.brews = _.map(brews, (brew)=>{
|
||||
// Clean up brew data
|
||||
brew.title = brew.title?.trim();
|
||||
brew.description = brew.description?.trim();
|
||||
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
locale : req.brew.lang,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
sanitizeBrew(req.brew, 'edit');
|
||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||
return next();
|
||||
}));
|
||||
|
||||
//New Page from ID
|
||||
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
|
||||
sanitizeBrew(req.brew, 'share');
|
||||
const brew = {
|
||||
shareId : req.brew.shareId,
|
||||
title : `CLONE - ${req.brew.title}`,
|
||||
text : req.brew.text,
|
||||
style : req.brew.style,
|
||||
renderer : req.brew.renderer,
|
||||
theme : req.brew.theme,
|
||||
tags : req.brew.tags,
|
||||
snippets : req.brew.snippets
|
||||
};
|
||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new', asyncHandler(async(req, res, next)=>{
|
||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||
const { brew } = req;
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
// increase visitor view count, do not include visits by author(s)
|
||||
if(!brew.authors.includes(req.account?.username)){
|
||||
if(req.params.id.length > 12 && !brew._id) {
|
||||
const googleId = brew.googleId;
|
||||
const shareId = brew.shareId;
|
||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||
.catch((err)=>{next(err);});
|
||||
} else {
|
||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||
}
|
||||
};
|
||||
|
||||
brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Account Page
|
||||
app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
|
||||
const data = {};
|
||||
data.title = 'Account Information Page';
|
||||
|
||||
if(!req.account) {
|
||||
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
||||
const error = new Error('No valid account');
|
||||
error.status = 401;
|
||||
error.HBErrorCode = '50';
|
||||
error.page = data.title;
|
||||
return next(error);
|
||||
};
|
||||
|
||||
let auth;
|
||||
let googleCount = [];
|
||||
if(req.account) {
|
||||
if(req.account.googleId) {
|
||||
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||
|
||||
googleCount = await GoogleActions.listGoogleBrews(auth)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return 0;
|
||||
});
|
||||
|
||||
data.accountDetails = {
|
||||
username : req.account.username,
|
||||
issued : req.account.issued,
|
||||
googleId : Boolean(req.account.googleId),
|
||||
authCheck : Boolean(req.account.googleId && auth?.credentials.access_token),
|
||||
mongoCount : mongoCount,
|
||||
googleCount : googleCount?.length
|
||||
};
|
||||
}
|
||||
|
||||
req.brew = data;
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `Account Page`,
|
||||
description : null
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Vault Page
|
||||
app.get('/vault', asyncHandler(async(req, res, next)=>{
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'The Vault',
|
||||
description : 'Search for Brews'
|
||||
};
|
||||
return next();
|
||||
}));
|
||||
|
||||
return app;
|
||||
}
|
||||
+3
-2
@@ -146,7 +146,7 @@ const printCurrentBrew = async ()=>{
|
||||
}
|
||||
};
|
||||
|
||||
const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
|
||||
const fetchThemeBundle = async (setError = ()=>{}, setThemeBundle = ()=>{}, renderer, theme)=>{
|
||||
if(!renderer || !theme) return;
|
||||
const res = await request
|
||||
.get(`/api/theme/${renderer}/${theme}`)
|
||||
@@ -229,5 +229,6 @@ export {
|
||||
printCurrentBrew,
|
||||
fetchThemeBundle,
|
||||
brewSnippetsToJSON,
|
||||
debugTextMismatch
|
||||
debugTextMismatch,
|
||||
yamlSnippetsToText
|
||||
};
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
transition-timing-function : @easing;
|
||||
}
|
||||
|
||||
|
||||
.transition (@prop, @duration: @defaultDuration) {
|
||||
-webkit-transition : @prop @duration, -webkit-transform @duration;
|
||||
-moz-transition : @prop @duration, -moz-transform @duration;
|
||||
@@ -58,13 +57,13 @@
|
||||
transform : @transform;
|
||||
}
|
||||
|
||||
|
||||
.delay(@delay) {
|
||||
-webkit-transition-delay : @delay;
|
||||
transition-delay : @delay;
|
||||
-webkit-animation-delay : @delay;
|
||||
animation-delay : @delay;
|
||||
}
|
||||
|
||||
.keep() {
|
||||
-webkit-animation-fill-mode : forwards;
|
||||
-moz-animation-fill-mode : forwards;
|
||||
@@ -73,7 +72,6 @@
|
||||
animation-fill-mode : forwards;
|
||||
}
|
||||
|
||||
|
||||
.sequentialDelay(@delayInc : 0.2s, @initialDelay : 0s) {
|
||||
&:nth-child(1) {.delay(0*@delayInc + @initialDelay); }
|
||||
&:nth-child(2) {.delay(1*@delayInc + @initialDelay); }
|
||||
@@ -97,14 +95,12 @@
|
||||
&:nth-child(20) {.delay(19*@delayInc + @initialDelay); }
|
||||
}
|
||||
|
||||
|
||||
|
||||
.createFrames(@name, @from, @to) {
|
||||
@frames: {
|
||||
from { @from(); }
|
||||
to { @to(); }
|
||||
};
|
||||
@-webkit-keyframes @name {@frames();}
|
||||
@-webkit-keyframes @name {@frames();}
|
||||
@-moz-keyframes @name {@frames();}
|
||||
@-ms-keyframes @name {@frames();}
|
||||
@-o-keyframes @name {@frames();}
|
||||
@@ -126,8 +122,6 @@
|
||||
animation-timing-function : @easing;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************
|
||||
Standard Animations
|
||||
****************************/
|
||||
@@ -136,7 +130,7 @@
|
||||
.createAnimation(fadeIn; @duration; @easing);
|
||||
.createFrames(fadeIn,
|
||||
{ opacity : 0; },
|
||||
{ opacity : 1; }
|
||||
{ opacity : 1; }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,7 +138,7 @@
|
||||
.createAnimation(fadeInDown; @duration; @easing);
|
||||
.createFrames(fadeInDown,
|
||||
{ opacity : 0; .transform(translateY(20px));},
|
||||
{ opacity : 1; .transform(translateY(0px));}
|
||||
{ opacity : 1; .transform(translateY(0px));}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,7 +146,7 @@
|
||||
.createAnimation(fadeInTop; @duration; @easing);
|
||||
.createFrames(fadeInTop,
|
||||
{ opacity : 0; .transform(translateY(-20px)); },
|
||||
{ opacity : 1; .transform(translateY(0px));}
|
||||
{ opacity : 1; .transform(translateY(0px));}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,7 +154,7 @@
|
||||
.createAnimation(fadeInLeft; @duration; @easing);
|
||||
.createFrames(fadeInLeft,
|
||||
{ opacity: 0; .transform(translateX(-20px));},
|
||||
{ opacity: 1; .transform(translateX(0));}
|
||||
{ opacity: 1; .transform(translateX(0));}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,7 +162,7 @@
|
||||
.createAnimation(fadeInRight; @duration; @easing);
|
||||
.createFrames(fadeInRight,
|
||||
{ opacity: 0; .transform(translateX(20px));},
|
||||
{ opacity: 1; .transform(translateX(0));}
|
||||
{ opacity: 1; .transform(translateX(0));}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,7 +170,7 @@
|
||||
.createAnimation(fadeOut; @duration; @easing);
|
||||
.createFrames(fadeOut,
|
||||
{ opacity : 1; },
|
||||
{ opacity : 0; }
|
||||
{ opacity : 0; }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -184,7 +178,7 @@
|
||||
.createAnimation(fadeOutDown; @duration; @easing);
|
||||
.createFrames(fadeOutDown,
|
||||
{ opacity : 1; .transform(translateY(0)); visibility: visible;},
|
||||
{ opacity : 0; .transform(translateY(20px)); visibility: hidden;}
|
||||
{ opacity : 0; .transform(translateY(20px)); visibility: hidden;}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -192,7 +186,7 @@
|
||||
.createAnimation(fadeOutTop; @duration; @easing);
|
||||
.createFrames(fadeOutTop,
|
||||
{ opacity : 1; .transform(translateY(0)); },
|
||||
{ opacity : 0; .transform(translateY(-20px)); }
|
||||
{ opacity : 0; .transform(translateY(-20px)); }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,7 +194,7 @@
|
||||
.createAnimation(fadeOutLeft; @duration; @easing);
|
||||
.createFrames(fadeOutLeft,
|
||||
{ opacity : 1; .transform(translateX(0));},
|
||||
{ opacity : 0; .transform(translateX(-20px));}
|
||||
{ opacity : 0; .transform(translateX(-20px));}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -208,13 +202,10 @@
|
||||
.createAnimation(fadeOutRight; @duration; @easing);
|
||||
.createFrames(fadeOutRight,
|
||||
{ opacity : 1; .transform(translateX(0));},
|
||||
{ opacity : 0; .transform(translateX(20px));}
|
||||
{ opacity : 0; .transform(translateX(20px));}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/***************************
|
||||
Fun Animations
|
||||
****************************/
|
||||
|
||||
@@ -40,7 +40,7 @@ p {
|
||||
color : @copyGrey;
|
||||
}
|
||||
code {
|
||||
font-family : 'Courier', "mono";
|
||||
font-family : 'Courier', 'mono';
|
||||
color : black;
|
||||
white-space : pre;
|
||||
background-color : #F8F8F8;
|
||||
@@ -54,7 +54,7 @@ button {
|
||||
.animate(background-color);
|
||||
display : inline-block;
|
||||
padding : 0.6em 1.2em;
|
||||
font-family : 'Lato', "Helvetica", "Arial", sans-serif;
|
||||
font-family : 'Lato', 'Helvetica', 'Arial', sans-serif;
|
||||
font-size : 15px;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
|
||||
@@ -22,4 +22,4 @@
|
||||
background-color : unset;
|
||||
}
|
||||
|
||||
:where(i){ text-box-trim: trim-both }
|
||||
:where(i) { text-box-trim : trim-both; }
|
||||
|
||||
@@ -3,23 +3,23 @@
|
||||
@arrowSize : 6px;
|
||||
@arrowPosition : 18px;
|
||||
[data-tooltip] {
|
||||
position:relative;
|
||||
position : relative;
|
||||
.tooltip(attr(data-tooltip));
|
||||
}
|
||||
[data-tooltip-top] {
|
||||
position:relative;
|
||||
position : relative;
|
||||
.tooltipTop(attr(data-tooltip-top));
|
||||
}
|
||||
[data-tooltip-bottom] {
|
||||
position:relative;
|
||||
position : relative;
|
||||
.tooltipBottom(attr(data-tooltip-bottom));
|
||||
}
|
||||
[data-tooltip-left] {
|
||||
position:relative;
|
||||
position : relative;
|
||||
.tooltipLeft(attr(data-tooltip-left));
|
||||
}
|
||||
[data-tooltip-right] {
|
||||
position:relative;
|
||||
position : relative;
|
||||
.tooltipRight(attr(data-tooltip-right));
|
||||
}
|
||||
.tooltip(@content) {
|
||||
@@ -33,9 +33,9 @@
|
||||
}
|
||||
&::after { margin-left : -18px; }
|
||||
&::before, &::after {
|
||||
bottom : 100%;
|
||||
left : 50%;
|
||||
translate: -50% 0;
|
||||
bottom : 100%;
|
||||
left : 50%;
|
||||
translate : -50% 0;
|
||||
}
|
||||
&:hover::after, &:hover::before, &:focus::after, &:focus::before {
|
||||
.transform(translateY(-(@arrowSize + 2)));
|
||||
@@ -49,9 +49,9 @@
|
||||
}
|
||||
&::after { margin-left : -18px; }
|
||||
&::before, &::after {
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
translate: -50% 0;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
translate : -50% 0;
|
||||
}
|
||||
&:hover::after, &:hover::before, &:focus::after, &:focus::before {
|
||||
.transform(translateY(@arrowSize + 2));
|
||||
@@ -65,8 +65,8 @@
|
||||
border-left-color : @tooltipColor;
|
||||
}
|
||||
&::after {
|
||||
max-width : 50ch;
|
||||
margin-bottom : -14px;
|
||||
max-width : 50ch;
|
||||
}
|
||||
&::before, &::after {
|
||||
right : 100%;
|
||||
@@ -84,13 +84,13 @@
|
||||
border-right-color : @tooltipColor;
|
||||
}
|
||||
&::after {
|
||||
max-width : 50ch;
|
||||
margin-bottom : -14px;
|
||||
max-width : 50ch;
|
||||
}
|
||||
&::before, &::after {
|
||||
top : 50%;
|
||||
left : 100%;
|
||||
translate:0 -50%;
|
||||
top : 50%;
|
||||
left : 100%;
|
||||
translate : 0 -50%;
|
||||
}
|
||||
&:hover::after, &:hover::before, &:focus::after, &:focus::before {
|
||||
.transform(translateX(@arrowSize + 2));
|
||||
@@ -115,17 +115,17 @@
|
||||
}
|
||||
//Box
|
||||
&::after {
|
||||
visibility : hidden;
|
||||
padding : 8px 10px;
|
||||
font-size : 12px;
|
||||
line-height : 12px;
|
||||
color : white;
|
||||
content : @content;
|
||||
background : @tooltipColor;
|
||||
max-width : 60ch;
|
||||
width :max-content;
|
||||
word-break : break-word;
|
||||
visibility : hidden;
|
||||
width : max-content;
|
||||
max-width : 60ch;
|
||||
padding : 8px 10px;
|
||||
font-size : 12px;
|
||||
line-height : 12px;
|
||||
color : white;
|
||||
word-break : break-word;
|
||||
overflow-wrap : break-word;
|
||||
content : @content;
|
||||
background : @tooltipColor;
|
||||
}
|
||||
&:hover::before, &:hover::after {
|
||||
visibility : visible;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
fetchThemeBundle,
|
||||
brewSnippetsToJSON,
|
||||
debugTextMismatch,
|
||||
yamlSnippetsToText,
|
||||
} from '../../shared/helpers.js';
|
||||
|
||||
import dedent from 'dedent';
|
||||
|
||||
// Marked.js adds line returns after closing tags on some default tokens.
|
||||
// This removes those line returns for comparison sake.
|
||||
String.prototype.trimReturns = function(){
|
||||
return this.replace(/\r?\n|\r/g, '');
|
||||
};
|
||||
|
||||
const emoji = 'df_d12_2';
|
||||
|
||||
const brewSnippetsThemeTest = [
|
||||
{
|
||||
name : 'Test Theme',
|
||||
snippets : dedent `
|
||||
\snippet First Theme Snippet
|
||||
I am the first theme snippet!
|
||||
|
||||
\snippet Second Theme Snippet
|
||||
I am the second theme Snippet!`,
|
||||
}
|
||||
];
|
||||
|
||||
const brewSnippetsBrewTest = dedent`
|
||||
\snippet First Brew Snippet
|
||||
I am the first brew snippet!
|
||||
|
||||
\snippet Second Brew Snippet
|
||||
I am the second brew Snippet!`;
|
||||
|
||||
describe(`brewSnippetsToJSON`, ()=>{
|
||||
it('converts raw brew snippets without theme snippets to JSON', function() {
|
||||
const testMenuObject = {
|
||||
groupName : 'Brew Snippets',
|
||||
icon : 'fas fa-th-list',
|
||||
view : 'text',
|
||||
snippets : [{
|
||||
name : 'Test Snippets JSON without theme snippets',
|
||||
subsnippets : [
|
||||
{
|
||||
gen : 'I am the first brew snippet!\n',
|
||||
name : 'First Brew Snippet'
|
||||
}, {
|
||||
gen : 'I am the second brew Snippet!',
|
||||
name : 'Second Brew Snippet'
|
||||
}
|
||||
] }]
|
||||
};
|
||||
const rendered = brewSnippetsToJSON(`Test Snippets JSON without theme snippets`, brewSnippetsBrewTest, null, true);
|
||||
expect(rendered).toStrictEqual(testMenuObject);
|
||||
});
|
||||
|
||||
it('converts raw brew snippets with theme snippets to JSON', function() {
|
||||
const testMenuObject = {
|
||||
groupName : 'Brew Snippets',
|
||||
icon : 'fas fa-th-list',
|
||||
view : 'text',
|
||||
snippets : [{
|
||||
gen : '',
|
||||
icon : '',
|
||||
name : 'Test Theme',
|
||||
subsnippets : [
|
||||
{
|
||||
gen : 'I am the first theme snippet!\n',
|
||||
icon : '',
|
||||
name : 'First Theme Snippet',
|
||||
},
|
||||
{
|
||||
gen : 'I am the second theme Snippet!',
|
||||
icon : '',
|
||||
name : 'Second Theme Snippet',
|
||||
},
|
||||
] },
|
||||
{
|
||||
name : 'Test Snippets JSON with theme snippets',
|
||||
subsnippets : [
|
||||
{
|
||||
gen : 'I am the first brew snippet!\n',
|
||||
name : 'First Brew Snippet'
|
||||
},
|
||||
{
|
||||
gen : 'I am the second brew Snippet!',
|
||||
name : 'Second Brew Snippet'
|
||||
}
|
||||
]
|
||||
}] };
|
||||
const rendered = brewSnippetsToJSON(`Test Snippets JSON with theme snippets`, brewSnippetsBrewTest, brewSnippetsThemeTest, true);
|
||||
expect(rendered).toStrictEqual(testMenuObject);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`YAMLSnippetsToText`, ()=>{
|
||||
it('converts brew snippet YAML to a string ', function() {
|
||||
const brewSnippetsYAML = [{
|
||||
subsnippets : [
|
||||
{
|
||||
gen : 'I am the first brew snippet!\n',
|
||||
name : 'First Brew Snippet'
|
||||
}, {
|
||||
gen : 'I am the second brew Snippet!',
|
||||
name : 'Second Brew Snippet'
|
||||
}
|
||||
]
|
||||
}];
|
||||
const rendered = yamlSnippetsToText(brewSnippetsYAML);
|
||||
expect(rendered).toBe(`${brewSnippetsBrewTest}\n`);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
|
||||
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
||||
const source = '<div>*Bold text*</div>';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
|
||||
describe('Inline Definition Lists', ()=>{
|
||||
test('No Term 1 Definition', function() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
import dedent from 'dedent';
|
||||
|
||||
// Marked.js adds line returns after closing tags on some default tokens.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
|
||||
describe('Hard Breaks', ()=>{
|
||||
test('Single Break', function() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import dedent from 'dedent';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
|
||||
// Marked.js adds line returns after closing tags on some default tokens.
|
||||
// This removes those line returns for comparison sake.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
import {hbfm} from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
|
||||
describe('Non-Breaking Spaces Interactions', ()=>{
|
||||
test('I am actually a single-line definition list!', function() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
|
||||
describe('Justification', ()=>{
|
||||
test('Left Justify', function() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import dedent from 'dedent';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
|
||||
// Marked.js adds line returns after closing tags on some default tokens.
|
||||
// This removes those line returns for comparison sake.
|
||||
|
||||
@@ -25,16 +25,16 @@ body { counter-reset : phb-page-numbers; }
|
||||
}
|
||||
}
|
||||
.useColumns(@multiplier : 1) {
|
||||
column-count : 2;
|
||||
column-fill : auto;
|
||||
column-gap : 1cm;
|
||||
-webkit-column-width : 8cm * @multiplier;
|
||||
-moz-column-width : 8cm * @multiplier;
|
||||
column-width : 8cm * @multiplier;
|
||||
-webkit-column-count : 2;
|
||||
-moz-column-count : 2;
|
||||
-webkit-column-width : 8cm * @multiplier;
|
||||
-moz-column-width : 8cm * @multiplier;
|
||||
column-count : 2;
|
||||
column-fill : auto;
|
||||
-webkit-column-gap : 1cm;
|
||||
-moz-column-gap : 1cm;
|
||||
column-gap : 1cm;
|
||||
}
|
||||
.phb, .page {
|
||||
.useColumns();
|
||||
@@ -43,8 +43,11 @@ body { counter-reset : phb-page-numbers; }
|
||||
box-sizing : border-box;
|
||||
width : 215.9mm;
|
||||
height : 279.4mm;
|
||||
contain-intrinsic-size : auto none;
|
||||
padding : 1.0cm 1.7cm;
|
||||
padding-bottom : 1.5cm;
|
||||
contain : strict;
|
||||
content-visibility : auto;
|
||||
overflow : hidden;
|
||||
font-family : 'BookSanity';
|
||||
font-size : 0.317cm;
|
||||
@@ -52,11 +55,8 @@ body { counter-reset : phb-page-numbers; }
|
||||
background-color : @background;
|
||||
background-image : @backgroundImage;
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
contain : strict;
|
||||
content-visibility : auto;
|
||||
contain-intrinsic-size : auto none;
|
||||
break-before : page;
|
||||
break-after : page;
|
||||
}
|
||||
|
||||
.phb {
|
||||
@@ -111,10 +111,10 @@ body { counter-reset : phb-page-numbers; }
|
||||
color : @headerText;
|
||||
}
|
||||
h1 {
|
||||
column-span : all;
|
||||
font-size : 0.987cm;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
column-span : all;
|
||||
font-size : 0.987cm;
|
||||
& + p::first-letter {
|
||||
float : left;
|
||||
font-family : 'Solberry';
|
||||
@@ -308,7 +308,7 @@ body { counter-reset : phb-page-numbers; }
|
||||
p,blockquote,table {
|
||||
z-index : 15;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
//Better spacing for spell blocks
|
||||
@@ -322,7 +322,7 @@ body { counter-reset : phb-page-numbers; }
|
||||
}
|
||||
li {
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
}
|
||||
@@ -347,7 +347,7 @@ body { counter-reset : phb-page-numbers; }
|
||||
list-style-type : none;
|
||||
break-inside : auto;
|
||||
-webkit-column-break-inside : auto;
|
||||
page-break-inside : auto;
|
||||
break-inside : auto;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
@@ -436,7 +436,7 @@ body { counter-reset : phb-page-numbers; }
|
||||
// *****************************/
|
||||
.phb .toc {
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
a {
|
||||
color : black;
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
.note table tbody tr:nth-child(odd) { background : #FFFFFF; }
|
||||
|
||||
/* DROP CAP */
|
||||
p.first-letter::first-letter,
|
||||
p.drop-cap::first-letter,
|
||||
h1 + p::first-letter {
|
||||
color : black;
|
||||
background-image : unset;
|
||||
|
||||
@@ -30,6 +30,8 @@ export default [
|
||||
name : 'Tweak Drop Cap',
|
||||
icon : 'fas fa-sliders-h',
|
||||
gen : dedent`/* Drop Cap settings */
|
||||
.page p.first-letter::first-letter,
|
||||
.page p.drop-cap::first-letter,
|
||||
.page h1 + p::first-letter {
|
||||
font-family: SolberaImitationRemake;
|
||||
font-size: 3.5cm;
|
||||
|
||||
+45
-43
@@ -27,16 +27,16 @@
|
||||
h5 + * { margin-top : 0.1cm; }
|
||||
}
|
||||
.useColumns(@multiplier : 1, @fillMode: auto) {
|
||||
column-count : 2;
|
||||
column-fill : @fillMode;
|
||||
column-gap : 0.9cm;
|
||||
-webkit-column-width : 8cm * @multiplier;
|
||||
-moz-column-width : 8cm * @multiplier;
|
||||
column-width : 8cm * @multiplier;
|
||||
-webkit-column-count : 2;
|
||||
-moz-column-count : 2;
|
||||
-webkit-column-width : 8cm * @multiplier;
|
||||
-moz-column-width : 8cm * @multiplier;
|
||||
column-count : 2;
|
||||
column-fill : @fillMode;
|
||||
-webkit-column-gap : 0.9cm;
|
||||
-moz-column-gap : 0.9cm;
|
||||
column-gap : 0.9cm;
|
||||
}
|
||||
.page {
|
||||
.useColumns();
|
||||
@@ -79,29 +79,31 @@
|
||||
color : var(--HB_Color_HeaderText);
|
||||
}
|
||||
h1 {
|
||||
margin-bottom : 0.18cm; //Margin-bottom only because this is WIDE
|
||||
column-span : all;
|
||||
font-size : 0.89cm;
|
||||
line-height : 1em;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
& + p::first-letter {
|
||||
float : left;
|
||||
padding-bottom : 2px;
|
||||
padding-left : 40px; //Allow background color to extend into margins
|
||||
margin-top : -0.3cm;
|
||||
margin-bottom : -20px;
|
||||
margin-left : -40px;
|
||||
font-family : 'SolberaImitationRemake';
|
||||
font-size : 3.5cm;
|
||||
line-height : 1em;
|
||||
color : rgba(0, 0, 0, 0);
|
||||
background-image : linear-gradient(-45deg, #322814, #998250, #322814);
|
||||
-webkit-background-clip : text;
|
||||
background-clip : text;
|
||||
}
|
||||
column-span : all;
|
||||
margin-bottom : 0.18cm; //Margin-bottom only because this is WIDE
|
||||
font-size : 0.89cm;
|
||||
line-height : 1em;
|
||||
& + p::first-line { font-variant : small-caps; }
|
||||
}
|
||||
p.first-letter::first-letter,
|
||||
p.drop-cap::first-letter,
|
||||
h1 + p::first-letter {
|
||||
float : left;
|
||||
padding-bottom : 2px;
|
||||
padding-left : 40px; //Allow background color to extend into margins
|
||||
margin-top : -0.3cm;
|
||||
margin-bottom : -20px;
|
||||
margin-left : -40px;
|
||||
font-family : 'SolberaImitationRemake';
|
||||
font-size : 3.5cm;
|
||||
line-height : 1em;
|
||||
color : rgba(0, 0, 0, 0);
|
||||
background-image : linear-gradient(-45deg, #322814, #998250, #322814);
|
||||
-webkit-background-clip : text;
|
||||
background-clip : text;
|
||||
}
|
||||
h2 {
|
||||
//margin-top : 0px; //Font is misaligned. Shift up slightly
|
||||
//margin-bottom : 0.05cm;
|
||||
@@ -196,7 +198,7 @@
|
||||
}
|
||||
|
||||
& + * { margin-top : 0.54cm; }
|
||||
|
||||
|
||||
}
|
||||
|
||||
// *****************************
|
||||
@@ -279,7 +281,7 @@
|
||||
.watermark { color : black; }
|
||||
|
||||
/* Watercolor */
|
||||
|
||||
|
||||
.watercolor1 { --wc : @watercolor1; }
|
||||
.watercolor2 { --wc : @watercolor2; }
|
||||
.watercolor3 { --wc : @watercolor3; }
|
||||
@@ -361,13 +363,13 @@
|
||||
|
||||
// Monster Ability table
|
||||
hr + table:first-of-type {
|
||||
margin : 0;
|
||||
-webkit-column-span : none;
|
||||
column-span : none;
|
||||
margin : 0;
|
||||
color : var(--HB_Color_HeaderText);
|
||||
background-color : transparent;
|
||||
border-style : none;
|
||||
border-image : none;
|
||||
-webkit-column-span : none;
|
||||
tr { background-color : transparent; }
|
||||
td,th { padding : 0px; }
|
||||
}
|
||||
@@ -418,15 +420,15 @@
|
||||
color : var(--HB_Color_Footnotes);
|
||||
}
|
||||
.footnote {
|
||||
text-transform: uppercase;
|
||||
position : absolute;
|
||||
right : 80px;
|
||||
bottom : 32px;
|
||||
z-index : 150;
|
||||
width : 200px;
|
||||
font-size : 0.8em;
|
||||
color : var(--HB_Color_Footnotes);
|
||||
text-align : right;
|
||||
position : absolute;
|
||||
right : 80px;
|
||||
bottom : 32px;
|
||||
z-index : 150;
|
||||
width : 200px;
|
||||
font-size : 0.8em;
|
||||
color : var(--HB_Color_Footnotes);
|
||||
text-align : right;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
// ************************************
|
||||
// * CODE BLOCKS
|
||||
@@ -486,7 +488,7 @@
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
-webkit-column-break-inside : auto;
|
||||
page-break-inside : auto;
|
||||
break-inside : auto;
|
||||
break-inside : auto;
|
||||
}
|
||||
&.wide { column-count : 4; }
|
||||
@@ -613,9 +615,9 @@
|
||||
font-size : 0.496cm;
|
||||
color : white;
|
||||
text-align : center;
|
||||
-webkit-text-stroke : 0.1cm black;
|
||||
text-transform : none;
|
||||
paint-order : stroke;
|
||||
text-transform : none;
|
||||
-webkit-text-stroke : 0.1cm black;
|
||||
}
|
||||
.logo {
|
||||
position : absolute;
|
||||
@@ -673,10 +675,10 @@
|
||||
// * BACK COVER
|
||||
// *****************************/
|
||||
.page:has(.backCover) {
|
||||
columns : 1;
|
||||
padding : 2.25cm 1.3cm 2cm 1.3cm;
|
||||
line-height : 1.4em;
|
||||
color : #FFFFFF;
|
||||
columns : 1;
|
||||
&::after { display : none; }
|
||||
.columnWrapper { width : 7.6cm; }
|
||||
.backCover {
|
||||
@@ -754,9 +756,9 @@
|
||||
// * PART COVER
|
||||
// *****************************/
|
||||
.page:has(.partCover) {
|
||||
columns : 1;
|
||||
padding-top : 0;
|
||||
text-align : center;
|
||||
columns : 1;
|
||||
|
||||
.partCover {
|
||||
position : absolute;
|
||||
@@ -805,7 +807,7 @@
|
||||
&:has(.toc)::after { display : none; }
|
||||
.toc {
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
h1 {
|
||||
margin-bottom : 0.3cm;
|
||||
|
||||
@@ -329,7 +329,7 @@ export default [
|
||||
},
|
||||
{
|
||||
name : 'DTRPG Community Content',
|
||||
icon : null,
|
||||
icon : null,
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Chronicle System Guild Colophon',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import hbfm from 'hbmarkedwrapper';
|
||||
import { hbfm } from 'marked-hbfm';
|
||||
|
||||
export default {
|
||||
createFooterFunc : function(headerSize=1){
|
||||
|
||||
+45
-57
@@ -34,15 +34,15 @@ body { counter-reset : page-numbers 0; }
|
||||
}
|
||||
|
||||
.useColumns(@multiplier : 1, @fillMode: auto) {
|
||||
column-fill : @fillMode;
|
||||
column-count : 2;
|
||||
column-fill : @fillMode;
|
||||
}
|
||||
.columnWrapper {
|
||||
columns : inherit;
|
||||
column-span : all;
|
||||
column-fill : inherit;
|
||||
column-gap : inherit;
|
||||
max-height : 100%;
|
||||
column-span : all;
|
||||
columns : inherit;
|
||||
column-fill : inherit;
|
||||
}
|
||||
.page {
|
||||
.useColumns();
|
||||
@@ -51,13 +51,13 @@ body { counter-reset : page-numbers 0; }
|
||||
box-sizing : border-box;
|
||||
width : 215.9mm;
|
||||
height : 279.4mm;
|
||||
contain-intrinsic-size : auto none;
|
||||
padding : 1.4cm 1.9cm 1.7cm;
|
||||
contain : strict;
|
||||
content-visibility : auto;
|
||||
overflow : clip;
|
||||
background-color : var(--HB_Color_Background);
|
||||
text-rendering : optimizeLegibility;
|
||||
contain : strict;
|
||||
content-visibility : auto;
|
||||
contain-intrinsic-size : auto none;
|
||||
}
|
||||
//*****************************
|
||||
// * BASE
|
||||
@@ -145,7 +145,7 @@ body { counter-reset : page-numbers 0; }
|
||||
blockquote,table {
|
||||
z-index : 15;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
// Nested lists
|
||||
@@ -155,20 +155,20 @@ body { counter-reset : page-numbers 0; }
|
||||
}
|
||||
li {
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
|
||||
/* Wrap Text */
|
||||
.wrapLeft {
|
||||
shape-outside : var(--HB_src);
|
||||
float : right;
|
||||
shape-outside : var(--HB_src);
|
||||
shape-margin : 0.2cm;
|
||||
}
|
||||
|
||||
.wrapRight {
|
||||
shape-outside : var(--HB_src);
|
||||
float : left;
|
||||
shape-outside : var(--HB_src);
|
||||
shape-margin : 0.2cm;
|
||||
}
|
||||
|
||||
@@ -433,8 +433,8 @@ body { counter-reset : page-numbers 0; }
|
||||
// *****************************/
|
||||
.page {
|
||||
.wide {
|
||||
column-span : all;
|
||||
display : block;
|
||||
column-span : all;
|
||||
margin-bottom : 1em;
|
||||
& + * { margin-top : 0; }
|
||||
}
|
||||
@@ -502,66 +502,54 @@ body { counter-reset : page-numbers 0; }
|
||||
// * Ordered List Type Overrides
|
||||
// *****************************/
|
||||
@counter-style lower-alpha-trailing-paren {
|
||||
system: extends lower-alpha;
|
||||
prefix: "";
|
||||
suffix: ") ";
|
||||
}
|
||||
system : extends lower-alpha;
|
||||
prefix : '';
|
||||
suffix : ') ';
|
||||
}
|
||||
|
||||
.page{
|
||||
.ol-ccby ol {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
.page {
|
||||
.ol-ccby ol { list-style-type : lower-alpha; }
|
||||
|
||||
.ol-ccby ol ol{
|
||||
padding-left: 0px !important;
|
||||
list-style-type: greek;
|
||||
.ol-ccby ol ol {
|
||||
padding-left : 0px !important;
|
||||
list-style-type : greek;
|
||||
}
|
||||
|
||||
.ol-ccby ol ol ol {
|
||||
padding-left: 0px !important;
|
||||
list-style-type: upper-alpha;
|
||||
padding-left : 0px !important;
|
||||
list-style-type : upper-alpha;
|
||||
}
|
||||
|
||||
.ol-ccby ol ol ol ol {
|
||||
padding-left: 0px !important;
|
||||
list-style-type: lower-roman;
|
||||
padding-left : 0px !important;
|
||||
list-style-type : lower-roman;
|
||||
}
|
||||
|
||||
.ol-ccby p {
|
||||
padding-left: 25px;
|
||||
text-indent: 0em !important;
|
||||
padding-left : 25px;
|
||||
text-indent : 0em !important;
|
||||
}
|
||||
|
||||
// Non-indented, numerical lists with slight gap between paragraphs.
|
||||
.ol-lowerLetters ol { list-style-type : lower-alpha-trailing-paren; }
|
||||
|
||||
.ol-lowerLetters ol li p { display : inline; }
|
||||
|
||||
.ol-lowerLetters ol {
|
||||
list-style-type: lower-alpha-trailing-paren;
|
||||
padding : 0px;
|
||||
list-style-position : inside;
|
||||
}
|
||||
|
||||
.ol-lowerLetters ol li p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.ol-lowerLetters ol {
|
||||
list-style-position: inside;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.ol-lowerLetters ol li:not(:last-child) {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.ol-lowerLetters ol li:not(:last-child) { margin-bottom : 0.5em; }
|
||||
|
||||
.ol-noIndent ol {
|
||||
list-style-position: inside;
|
||||
padding: 0px;
|
||||
padding : 0px;
|
||||
list-style-position : inside;
|
||||
}
|
||||
|
||||
.ol-noIndent ol li p {
|
||||
display: inline;
|
||||
}
|
||||
.ol-noIndent ol li p { display : inline; }
|
||||
|
||||
.ol-noIndent ol li:not(:last-child) {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.ol-noIndent ol li:not(:last-child) { margin-bottom : 0.5em; }
|
||||
}
|
||||
|
||||
// *****************************
|
||||
@@ -617,7 +605,7 @@ h6,
|
||||
&:has(.toc)::after { display : none; }
|
||||
.toc {
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
h1 {
|
||||
margin-bottom : 0.3cm;
|
||||
@@ -684,10 +672,10 @@ Firefox endruns
|
||||
**********************************/
|
||||
|
||||
@supports (-moz-user-select: none) { // This section will only apply to Firefox; it's the only browser that supports `-mos-xyz...`
|
||||
.page {
|
||||
blockquote, table {
|
||||
page-break-inside: auto;
|
||||
break-inside: auto;
|
||||
}
|
||||
}
|
||||
.page {
|
||||
blockquote, table {
|
||||
break-inside : auto;
|
||||
break-inside : auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,21 +80,23 @@
|
||||
font-size : 0.89cm;
|
||||
font-variant : small-caps;
|
||||
line-height : 1em;
|
||||
& + p::first-letter {
|
||||
float : left;
|
||||
padding-top : 0.3em;
|
||||
padding-bottom : 2px;
|
||||
padding-left : 40px; //Allow background color to extend into margins
|
||||
margin-top : -0.3cm;
|
||||
margin-right : 0.1em;
|
||||
margin-bottom : -20px;
|
||||
margin-left : -40px;
|
||||
font-family : 'FrederickaTheGreat';
|
||||
font-size : 1.9em;
|
||||
line-height : 1em;
|
||||
}
|
||||
& + p::first-line { font-variant : small-caps; }
|
||||
}
|
||||
p.first-letter::first-letter,
|
||||
p.drop-cap::first-letter,
|
||||
h1 + p::first-letter {
|
||||
float : left;
|
||||
padding-top : 0.3em;
|
||||
padding-bottom : 2px;
|
||||
padding-left : 40px; //Allow background color to extend into margins
|
||||
margin-top : -0.3cm;
|
||||
margin-right : 0.1em;
|
||||
margin-bottom : -20px;
|
||||
margin-left : -40px;
|
||||
font-family : 'FrederickaTheGreat';
|
||||
font-size : 1.9em;
|
||||
line-height : 1em;
|
||||
}
|
||||
h2 {
|
||||
font-size : 0.62cm;
|
||||
line-height : 0.988em; //Font is misaligned. Shift up slightly
|
||||
@@ -273,12 +275,12 @@
|
||||
|
||||
// Monster Ability table
|
||||
hr + table:first-of-type {
|
||||
column-span : none;
|
||||
margin : 0;
|
||||
color : inherit;
|
||||
background-image : none;
|
||||
border-style : none;
|
||||
border-image : none;
|
||||
column-span : none;
|
||||
tr { background-image : none; }
|
||||
td,th { padding : 0px; }
|
||||
}
|
||||
@@ -370,8 +372,8 @@
|
||||
|
||||
.page .spellList {
|
||||
.useSansSerif();
|
||||
font-family : 'PermanentMarker';
|
||||
column-count : 2;
|
||||
font-family : 'PermanentMarker';
|
||||
ul + h5 { margin-top : 15px; }
|
||||
ul {
|
||||
padding-left : 1em;
|
||||
@@ -380,7 +382,7 @@
|
||||
list-style-type : none;
|
||||
break-inside : auto;
|
||||
-webkit-column-break-inside : auto;
|
||||
page-break-inside : auto;
|
||||
break-inside : auto;
|
||||
}
|
||||
&.wide { column-count : 4; }
|
||||
}
|
||||
@@ -397,7 +399,7 @@
|
||||
// *****************************/
|
||||
.page .toc {
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
h1 {
|
||||
margin-bottom : 0.3cm;
|
||||
|
||||
@@ -8,31 +8,23 @@
|
||||
}
|
||||
|
||||
.page {
|
||||
font-family: Cambria,Georgia,serif;
|
||||
font-size: 14px;
|
||||
font-family : "Cambria","Georgia",serif;
|
||||
font-size : 14px;
|
||||
h1, h2, h3, h4 {
|
||||
font-variant: small-caps;
|
||||
font-weight: normal;
|
||||
font-weight : normal;
|
||||
font-variant : small-caps;
|
||||
}
|
||||
h1 {
|
||||
column-span: all;
|
||||
-webkit-column-span: all;
|
||||
font-size: 40px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 26px;
|
||||
-webkit-column-span : all;
|
||||
column-span : all;
|
||||
font-size : 40px;
|
||||
}
|
||||
h2 { font-size : 26px; }
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
border-bottom: 2px solid black;
|
||||
}
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
}
|
||||
h6 {
|
||||
font-size: 14px;
|
||||
font-size : 20px;
|
||||
border-bottom : 2px solid black;
|
||||
}
|
||||
h4 { font-size : 18px; }
|
||||
h5 { font-size : 16px; }
|
||||
h6 { font-size : 14px; }
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
@footerAccentImage : url('/assets/PHB_footerAccent.png');
|
||||
@frameBorderImage : url('/assets/frameBorder.png');
|
||||
@backgroundImage : url('/assets/parchmentBackground.jpg');
|
||||
@backgroundImageAlt : url('/assets/fluffy_background.webp');
|
||||
@backgroundImageAltDark : url('/assets/fluffy_background_dark.webp');
|
||||
@redTriangleImage : url('/assets/redTriangle.png');
|
||||
@monsterBorderImageLegacy : url('/assets/monsterBorderLegacy.png');
|
||||
@noteBorderImage : url('/assets/noteBorder.png');
|
||||
@descriptiveBoxImage : url('/assets/descriptiveBorder.png');
|
||||
@monsterBlockBackground : url('/assets/parchmentBackgroundGrayscale.jpg');
|
||||
@monsterBlockOverlay : url('/assets/parchmentBackgroundOverlayed.jpg');
|
||||
@monsterBlockOverlay : url('/assets/parchmentBackgroundOverlayed.jpg');
|
||||
@monsterBorderImage : url('/assets/monsterBorderFancy.png');
|
||||
@codeBorderImage : url('/assets/codeBorder.png');
|
||||
@classTableDecoration : url('/assets/classTableDecoration.png');
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
@@ -1,24 +1,24 @@
|
||||
/* Main Font, serif */
|
||||
@font-face {
|
||||
font-family : "BookSanity";
|
||||
font-family : 'BookSanity';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e legacy/Bookinsanity.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "BookSanity";
|
||||
font-family : 'BookSanity';
|
||||
font-style : normal;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/5e legacy/Bookinsanity Bold.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "BookSanity";
|
||||
font-family : 'BookSanity';
|
||||
font-style : italic;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e legacy/Bookinsanity Italic.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "BookSanity";
|
||||
font-family : 'BookSanity';
|
||||
font-style : italic;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/5e legacy/Bookinsanity Bold Italic.woff2');
|
||||
@@ -26,19 +26,19 @@
|
||||
|
||||
/* Notes and Tables, sans-serif */
|
||||
@font-face {
|
||||
font-family : "ScalySans";
|
||||
font-family : 'ScalySans';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e legacy/Scaly Sans.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "ScalySansSmallCaps";
|
||||
font-family : 'ScalySansSmallCaps';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e legacy/Scaly Sans Caps.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "WalterTurncoat";
|
||||
font-family : 'WalterTurncoat';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e legacy/WalterTurncoat-Regular.woff2');
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
/* Headers */
|
||||
@font-face {
|
||||
font-family : "MrJeeves";
|
||||
font-family : 'MrJeeves';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e legacy/Mr Eaves Small Caps.woff2');
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
/* Fancy Drop Cap */
|
||||
@font-face {
|
||||
font-family : "Solberry";
|
||||
font-family : 'Solberry';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e legacy/Solbera Imitation.woff2');
|
||||
|
||||
+21
-21
@@ -1,24 +1,24 @@
|
||||
/* Main Font, serif */
|
||||
@font-face {
|
||||
font-family : "BookInsanityRemake";
|
||||
font-family : 'BookInsanityRemake';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/Bookinsanity.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "BookInsanityRemake";
|
||||
font-family : 'BookInsanityRemake';
|
||||
font-style : normal;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/5e/Bookinsanity Bold.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "BookInsanityRemake";
|
||||
font-family : 'BookInsanityRemake';
|
||||
font-style : italic;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/Bookinsanity Italic.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "BookInsanityRemake";
|
||||
font-family : 'BookInsanityRemake';
|
||||
font-style : italic;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/5e/Bookinsanity Bold Italic.woff2');
|
||||
@@ -26,37 +26,37 @@
|
||||
|
||||
/* Notes and Tables, sans-serif */
|
||||
@font-face {
|
||||
font-family : "ScalySansRemake";
|
||||
font-family : 'ScalySansRemake';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/Scaly Sans.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "ScalySansRemake";
|
||||
font-family : 'ScalySansRemake';
|
||||
font-style : normal;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/5e/Scaly Sans Bold.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "ScalySansRemake";
|
||||
font-family : 'ScalySansRemake';
|
||||
font-style : italic;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/Scaly Sans Italic.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "ScalySansRemake";
|
||||
font-family : 'ScalySansRemake';
|
||||
font-style : italic;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/5e/Scaly Sans Bold Italic.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "ScalySansSmallCapsRemake";
|
||||
font-family : 'ScalySansSmallCapsRemake';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/Scaly Sans Caps.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "WalterTurncoat";
|
||||
font-family : 'WalterTurncoat';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/WalterTurncoat-Regular.woff2');
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
/* Headers */
|
||||
@font-face {
|
||||
font-family : "MrEavesRemake";
|
||||
font-family : 'MrEavesRemake';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/Mr Eaves Small Caps.woff2');
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
/* Fancy Drop Cap */
|
||||
@font-face {
|
||||
font-family : "SolberaImitationRemake"; //Tweaked 5e version
|
||||
font-family : 'SolberaImitationRemake'; //Tweaked 5e version
|
||||
font-style : normal;
|
||||
font-style : italic;
|
||||
font-weight : 100 1000;
|
||||
@@ -81,62 +81,62 @@
|
||||
|
||||
/* Cover Page */
|
||||
@font-face {
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-style : normal;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed Bold.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-style : italic;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed Italic.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-style : italic;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed Bold Italic.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "NodestoCapsWide";
|
||||
font-family : 'NodestoCapsWide';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/Nodesto Caps Wide.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "Overpass";
|
||||
font-family : 'Overpass';
|
||||
font-style : normal;
|
||||
font-weight : 500;
|
||||
src : url('../../../fonts/5e/Overpass Medium.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "Davek";
|
||||
font-family : 'Davek';
|
||||
font-style : normal;
|
||||
font-weight : 500;
|
||||
src : url('../../../fonts/5e/Davek.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "Iokharic";
|
||||
font-family : 'Iokharic';
|
||||
font-style : normal;
|
||||
font-weight : 500;
|
||||
src : url('../../../fonts/5e/Iokharic.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "Rellanic";
|
||||
font-family : 'Rellanic';
|
||||
font-style : normal;
|
||||
font-weight : 500;
|
||||
src : url('../../../fonts/5e/Rellanic.woff2');
|
||||
|
||||
@@ -18,28 +18,28 @@ License:
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family : "Pagella";
|
||||
font-family : 'Pagella';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/Blank/texgyrepagella-regular.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "Pagella";
|
||||
font-family : 'Pagella';
|
||||
font-style : normal;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/Blank/texgyrepagella-bold.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "Pagella";
|
||||
font-family : 'Pagella';
|
||||
font-style : italic;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/Blank/texgyrepagella-italic.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "Pagella";
|
||||
font-family : 'Pagella';
|
||||
font-style : italic;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/Blank/texgyrepagella-bolditalic.woff2');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* Main Font, serif */
|
||||
@font-face {
|
||||
font-family : "ReenieBeanie";
|
||||
font-family : 'ReenieBeanie';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/Journal/ReenieBeanie-Regular.woff2');
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
/* Notes and Tables, sans-serif */
|
||||
@font-face {
|
||||
font-family : "PermanentMarker";
|
||||
font-family : 'PermanentMarker';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/Journal/PermanentMarker-Regular.woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family : "WalterTurncoat";
|
||||
font-family : 'WalterTurncoat';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/WalterTurncoat-Regular.woff2');
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
/* Headers */
|
||||
@font-face {
|
||||
font-family : "FrederickaTheGreat";
|
||||
font-family : 'FrederickaTheGreat';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/Journal/FrederickaTheGreat-Regular.woff2');
|
||||
@@ -30,28 +30,28 @@
|
||||
|
||||
/* Cover Page */
|
||||
@font-face {
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-style : normal;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed Bold.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-style : italic;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed Italic.woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family : "NodestoCapsCondensed";
|
||||
font-family : 'NodestoCapsCondensed';
|
||||
font-style : italic;
|
||||
font-weight : bold;
|
||||
src : url('../../../fonts/5e/Nodesto Caps Condensed Bold Italic.woff2');
|
||||
|
||||
@@ -25,19 +25,19 @@
|
||||
&.F-minus::before { content : '\f191'; }
|
||||
&.F-plus::before { content : '\f192'; }
|
||||
&.F-zero::before { content : '\f193'; }
|
||||
&.d10::before { content : '\f194'; }
|
||||
&.d10-0::before { content : '\f100'; }
|
||||
&.d10-1:before { content : '\f101'; }
|
||||
&.d10::before { content : '\f194'; }
|
||||
&.d10-0::before { content : '\f100'; }
|
||||
&.d10-1::before { content : '\f101'; }
|
||||
&.d10-10::before { content : '\f102'; }
|
||||
&.d10-2:before { content : '\f103'; }
|
||||
&.d10-3::before { content : '\f104'; }
|
||||
&.d10-4::before { content : '\f105'; }
|
||||
&.d10-5::before { content : '\f106'; }
|
||||
&.d10-6::before { content : '\f107'; }
|
||||
&.d10-7::before { content : '\f108'; }
|
||||
&.d10-8::before { content : '\f109'; }
|
||||
&.d10-9::before { content : '\f10A'; }
|
||||
&.d12::before { content : '\f195'; }
|
||||
&.d10-2::before { content : '\f103'; }
|
||||
&.d10-3::before { content : '\f104'; }
|
||||
&.d10-4::before { content : '\f105'; }
|
||||
&.d10-5::before { content : '\f106'; }
|
||||
&.d10-6::before { content : '\f107'; }
|
||||
&.d10-7::before { content : '\f108'; }
|
||||
&.d10-8::before { content : '\f109'; }
|
||||
&.d10-9::before { content : '\f10A'; }
|
||||
&.d12::before { content : '\f195'; }
|
||||
&.d12-1::before { content : '\f10b'; }
|
||||
&.d12-10::before { content : '\f10c'; }
|
||||
&.d12-11::before { content : '\f10d'; }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import (less) "./themes/fonts/iconFonts/fontawesome-free.less";
|
||||
@import (less) "./themes/fonts/iconFonts/fontawesome-solid.less";
|
||||
@import (less) "./themes/fonts/iconFonts/fontawesome-brands.less";
|
||||
@import (less) "./themes/fonts/iconFonts/fontawesome-regular.less";
|
||||
@import (less) './themes/fonts/iconFonts/fontawesome-free.less';
|
||||
@import (less) './themes/fonts/iconFonts/fontawesome-solid.less';
|
||||
@import (less) './themes/fonts/iconFonts/fontawesome-brands.less';
|
||||
@import (less) './themes/fonts/iconFonts/fontawesome-regular.less';
|
||||
|
||||
|
||||
/* Icon Font: Font Awesome */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,15 @@
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
:root, :host {
|
||||
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
|
||||
--fa-style-family-classic : 'Font Awesome 6 Free';
|
||||
--fa-font-regular : normal 400 1em/1 'Font Awesome 6 Free'; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url("/fonts/iconFonts/fa-regular-400.woff2") format("woff2"), url("/fonts/iconFonts/fa-regular-400.ttf") format("truetype"); }
|
||||
font-family : 'Font Awesome 6 Free';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('/fonts/iconFonts/fa-regular-400.woff2') format('woff2'), url('/fonts/iconFonts/fa-regular-400.ttf') format('truetype');
|
||||
font-display : block; }
|
||||
|
||||
.far,
|
||||
.fa-regular {
|
||||
font-weight: 400; }
|
||||
.fa-regular { font-weight : normal; }
|
||||
|
||||
@@ -4,16 +4,15 @@
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
:root, :host {
|
||||
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
|
||||
--fa-style-family-classic : 'Font Awesome 6 Free';
|
||||
--fa-font-solid : normal 900 1em/1 'Font Awesome 6 Free'; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: block;
|
||||
src: url("/fonts/iconFonts/fa-solid-900.woff2") format("woff2"), url("/fonts/iconFonts/fa-solid-900.ttf") format("truetype"); }
|
||||
font-family : 'Font Awesome 6 Free';
|
||||
font-style : normal;
|
||||
font-weight : 900;
|
||||
src : url('/fonts/iconFonts/fa-solid-900.woff2') format('woff2'), url('/fonts/iconFonts/fa-solid-900.ttf') format('truetype');
|
||||
font-display : block; }
|
||||
|
||||
.fas,
|
||||
.fa-solid {
|
||||
font-weight: 900; }
|
||||
.fa-solid { font-weight : 900; }
|
||||
|
||||
+6
-6
@@ -8,12 +8,12 @@ export default defineConfig({
|
||||
plugins : [react(), generateAssetsPlugin()],
|
||||
resolve : {
|
||||
alias : {
|
||||
'@vitreum' : path.resolve(__dirname, './vitreum'),
|
||||
'@shared' : path.resolve(__dirname, './shared'),
|
||||
'@sharedStyles' : path.resolve(__dirname, './shared/naturalcrit/styles'),
|
||||
'@navbar' : path.resolve(__dirname, './client/homebrew/navbar'),
|
||||
'@themes' : path.resolve(__dirname, './themes'),
|
||||
'@components' : path.resolve(__dirname, './client/components')
|
||||
'@vitreum' : path.resolve(import.meta.dirname, './vitreum'),
|
||||
'@shared' : path.resolve(import.meta.dirname, './shared'),
|
||||
'@sharedStyles' : path.resolve(import.meta.dirname, './shared/naturalcrit/styles'),
|
||||
'@navbar' : path.resolve(import.meta.dirname, './client/homebrew/navbar'),
|
||||
'@themes' : path.resolve(import.meta.dirname, './themes'),
|
||||
'@components' : path.resolve(import.meta.dirname, './client/components')
|
||||
},
|
||||
},
|
||||
build : {
|
||||
|
||||
+12
-75
@@ -1,91 +1,28 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
//old vitreum file, still imported in some pages
|
||||
|
||||
const injectTag = (tag, props, children)=>{
|
||||
const injectNode = document.createElement(tag);
|
||||
Object.entries(props).forEach(([key, val])=>injectNode[key] = val);
|
||||
if(children) injectNode.appendChild(document.createTextNode(children));
|
||||
document.getElementsByTagName('head')[0].appendChild(injectNode);
|
||||
};
|
||||
|
||||
const obj2props = (obj)=>Object.entries(obj)
|
||||
.map(([k, v])=>`${k}="${v}"`)
|
||||
const obj2props = (obj) =>
|
||||
Object.entries(obj)
|
||||
.map(([k, v]) => `${k}="${v}"`)
|
||||
.join(' ');
|
||||
const toStr = (chld)=>(Array.isArray(chld) ? chld.join('') : chld);
|
||||
const onServer = typeof window === 'undefined';
|
||||
|
||||
let NamedTags = {};
|
||||
let UnnamedTags = [];
|
||||
|
||||
export const HeadComponents = {
|
||||
Title({ children }) {
|
||||
if(onServer) NamedTags.title = `<title>${toStr(children)}</title>`;
|
||||
useEffect(()=>{
|
||||
document.title = toStr(children);
|
||||
}, [children]);
|
||||
return null;
|
||||
},
|
||||
Favicon({ type = 'image/png', href = '', rel = 'icon', id = 'favicon' }) {
|
||||
if(onServer) NamedTags.favicon = `<link rel='shortcut icon' type="${type}" id="${id}" href="${href}" />`;
|
||||
useEffect(()=>{
|
||||
document.getElementById(id).href = href;
|
||||
}, [id, href]);
|
||||
return null;
|
||||
},
|
||||
Description({ children }) {
|
||||
if(onServer) NamedTags.description = `<meta name='description' content='${toStr(children)}' />`;
|
||||
return null;
|
||||
},
|
||||
Noscript({ children }) {
|
||||
if(onServer) UnnamedTags.push(`<noscript>${toStr(children)}</noscript>`);
|
||||
return null;
|
||||
},
|
||||
Script({ children = [], ...props }) {
|
||||
if(onServer) {
|
||||
UnnamedTags.push(
|
||||
children.length
|
||||
? `<script ${obj2props(props)}>${toStr(children)}</script>`
|
||||
: `<script ${obj2props(props)} />`,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
const HeadComponents = {
|
||||
Meta(props) {
|
||||
const tag = `<meta ${obj2props(props)} />`;
|
||||
props.property || props.name ? (NamedTags[props.property || props.name] = tag) : UnnamedTags.push(tag);
|
||||
useEffect(()=>{
|
||||
|
||||
props.property || props.name
|
||||
? (NamedTags[props.property || props.name] = tag)
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
document
|
||||
.getElementsByTagName('head')[0]
|
||||
.insertAdjacentHTML('beforeend', Object.values(NamedTags).join('\n'));
|
||||
}, [NamedTags]);
|
||||
return null;
|
||||
},
|
||||
Style({ children, type = 'text/css' }) {
|
||||
if(onServer) UnnamedTags.push(`<style type="${type}">${toStr(children)}</style>`);
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
export const Inject = ({ tag, children, ...props })=>{
|
||||
useEffect(()=>{
|
||||
injectTag(tag, props, children);
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const generate = ()=>Object.values(NamedTags).concat(UnnamedTags).join('\n');
|
||||
|
||||
export const flush = ()=>{
|
||||
NamedTags = {};
|
||||
UnnamedTags = [];
|
||||
};
|
||||
|
||||
export const Meta = HeadComponents.Meta;
|
||||
|
||||
export default {
|
||||
Inject,
|
||||
...HeadComponents,
|
||||
generate,
|
||||
flush,
|
||||
};
|
||||
export default HeadComponents;
|
||||
Reference in New Issue
Block a user