0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-24 20:42:43 +00:00

Merge branch 'master' into issue_3974

This commit is contained in:
David Bolack
2025-01-10 11:19:13 -06:00
6 changed files with 175 additions and 6 deletions

View File

@@ -16,8 +16,11 @@ const Frame = require('react-frame-component').default;
const dedent = require('dedent-tabs').default;
const { printCurrentBrew } = require('../../../shared/helpers.js');
import HeaderNav from './headerNav/headerNav.jsx';
import { safeHTML } from './safeHTML.js';
const PAGE_HEIGHT = 1056;
const INITIAL_CONTENT = dedent`
@@ -115,7 +118,10 @@ const BrewRenderer = (props)=>{
pageShadows : true
});
const [headerState, setHeaderState] = useState(false);
const mainRef = useRef(null);
const pagesRef = useRef(null);
if(props.renderer == 'legacy') {
rawPages = props.text.split('\\page');
@@ -287,7 +293,7 @@ const BrewRenderer = (props)=>{
<NotificationPopup />
</div>
<ToolBar displayOptions={displayOptions} onDisplayOptionsChange={handleDisplayOptionsChange} visiblePages={state.visiblePages.length > 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length}/>
<ToolBar displayOptions={displayOptions} onDisplayOptionsChange={handleDisplayOptionsChange} visiblePages={state.visiblePages.length > 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/>
{/*render in iFrame so broken code doesn't crash the site.*/}
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
@@ -306,12 +312,13 @@ const BrewRenderer = (props)=>{
&&
<>
{renderedStyle}
<div className={`pages ${displayOptions.startOnRight ? 'recto' : 'verso'} ${displayOptions.spread}`} lang={`${props.lang || 'en'}`} style={pagesStyle}>
<div className={`pages ${displayOptions.startOnRight ? 'recto' : 'verso'} ${displayOptions.spread}`} lang={`${props.lang || 'en'}`} style={pagesStyle} ref={pagesRef}>
{renderedPages}
</div>
</>
}
</div>
{headerState ? <HeaderNav ref={pagesRef} /> : <></>}
</Frame>
</>
);

View File

@@ -70,6 +70,7 @@
.pane { position : relative; }
@media print {
.toolBar { display : none; }
.brewRenderer {
@@ -82,4 +83,7 @@
& > .page { box-shadow : unset; }
}
}
.headerNav {
visibility: hidden;
}
}

View File

@@ -0,0 +1,106 @@
require('./headerNav.less');
import * as React from 'react';
import * as _ from 'lodash';
const MAX_TEXT_LENGTH = 40;
const HeaderNav = React.forwardRef(({}, pagesRef)=>{
const renderHeaderLinks = ()=>{
if(!pagesRef.current) return;
const selector = [
'.pages > .page', // All page elements, which by definition have IDs
'.page:not(:has(.toc)) > [id]', // All direct children of non-ToC .page with an ID (Legacy)
'.page:not(:has(.toc)) > .columnWrapper > [id]', // All direct children of non-ToC .page > .columnWrapper with an ID (V3)
'.page:not(:has(.toc)) h2', // All non-ToC H2 titles, like Monster frame titles
];
const elements = pagesRef.current.querySelectorAll(selector.join(','));
if(!elements) return;
const navList = [];
// navList is a list of objects which have the following structure:
// {
// depth : how deeply indented the item should be
// text : the text to display in the nav link
// link : the hyperlink to navigate to when clicked
// className : [optional] the class to apply to the nav link for styling
// }
elements.forEach((el)=>{
if(el.className.match(/\bpage\b/)) {
let text = `Page ${el.id.slice(1)}`; // The ID of a page *should* always be equal to `p` followed by the page number
if(el.querySelector('.toc')){ // If the page contains a table of contents, add "- Contents" to the display text
text += ' - Contents';
};
navList.push({
depth : 0, // Pages are always at the least indented level
text : text,
link : el.id,
className : 'pageLink'
});
return;
}
if(el.localName.match(/^h[1-6]/)){ // Header elements H1 through H6
navList.push({
depth : el.localName[1], // Depth is set by the header level
text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
link : el.id
});
return;
}
navList.push({
depth : 7, // All unmatched elements with IDs are set to the maximum depth (7)
text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
link : el.id
});
});
return _.map(navList, (navItem, index)=>{
return <HeaderNavItem {...navItem} key={index} />;
});
};
return <nav className='headerNav'>
<ul>
{renderHeaderLinks()}
</ul>
</nav>;
}
);
const HeaderNavItem = ({ link, text, depth, className })=>{
const trimString = (text, prefixLength = 0)=>{
// Sanity check nav link strings
let output = text;
// If the string has a line break, only use the first line
if(text.indexOf('\n')){
output = text.split('\n')[0];
}
// Trim unecessary spaces from string
output = output.trim();
// Reduce excessively long strings
const maxLength = MAX_TEXT_LENGTH - prefixLength;
if(output.length > maxLength){
return `${output.slice(0, maxLength).trim()}...`;
}
return output;
};
if(!link || !text) return;
return <li>
<a href={`#${link}`} target='_self' className={`depth-${depth} ${className ?? ''}`}>
{trimString(text, depth)}
</a>
</li>;
};
export default HeaderNav;

View File

@@ -0,0 +1,47 @@
.headerNav {
position: fixed;
top: 32px;
left: 0px;
padding: 5px 10px;
background-color: #ccc;
border-radius: 5px;
max-height: calc(100vh - 32px);
max-width: 40vw;
overflow-y: auto;
&.active {
padding-bottom: 10px;
.navIcon {
padding-bottom: 10px;
}
}
.navIcon {
cursor: pointer;
}
li {
list-style-type: none;
a {
display: inline-block;
width: 100%;
font-family: 'Open Sans';
font-size: 12px;
padding: 2px;
color: inherit;
text-decoration: none;
cursor: pointer;
&:hover {
text-decoration: underline;
}
&.pageLink {
font-weight: 900;
}
@depths: 1,2,3,4,5,6,7;
each(@depths, {
&.depth-@{value} {
padding-left: ((@value - 1) * 0.5em);
}
});
}
}
}

View File

@@ -9,7 +9,7 @@ import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anch
const MAX_ZOOM = 300;
const MIN_ZOOM = 10;
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages })=>{
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{
const [pageNum, setPageNum] = useState(1);
const [toolsVisible, setToolsVisible] = useState(true);
@@ -76,7 +76,10 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
return (
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
<div className='toggleButton'>
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
</div>
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
<button

View File

@@ -166,7 +166,7 @@
&.hidden {
flex-wrap : nowrap;
width : 32px;
width : 92px;
overflow : hidden;
background-color : unset;
opacity : 0.5;
@@ -178,10 +178,12 @@
}
}
button.toggleButton {
.toggleButton {
position : absolute;
left : 0;
z-index : 5;
width : 32px;
min-width : unset;
height : 100%;
display : flex;
}