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:
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
|
||||
.pane { position : relative; }
|
||||
|
||||
|
||||
@media print {
|
||||
.toolBar { display : none; }
|
||||
.brewRenderer {
|
||||
@@ -82,4 +83,7 @@
|
||||
& > .page { box-shadow : unset; }
|
||||
}
|
||||
}
|
||||
.headerNav {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
106
client/homebrew/brewRenderer/headerNav/headerNav.jsx
Normal file
106
client/homebrew/brewRenderer/headerNav/headerNav.jsx
Normal 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;
|
||||
47
client/homebrew/brewRenderer/headerNav/headerNav.less
Normal file
47
client/homebrew/brewRenderer/headerNav/headerNav.less
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user