mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-02 10:42:44 +00:00
Merge pull request #3589 from G-Ambatte/experimentalHeaderNavigation
Experimental Header Navigation
This commit is contained in:
@@ -16,8 +16,11 @@ const Frame = require('react-frame-component').default;
|
|||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||||
|
|
||||||
|
import HeaderNav from './headerNav/headerNav.jsx';
|
||||||
|
|
||||||
import { safeHTML } from './safeHTML.js';
|
import { safeHTML } from './safeHTML.js';
|
||||||
|
|
||||||
|
|
||||||
const PAGE_HEIGHT = 1056;
|
const PAGE_HEIGHT = 1056;
|
||||||
|
|
||||||
const INITIAL_CONTENT = dedent`
|
const INITIAL_CONTENT = dedent`
|
||||||
@@ -115,7 +118,10 @@ const BrewRenderer = (props)=>{
|
|||||||
pageShadows : true
|
pageShadows : true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [headerState, setHeaderState] = useState(false);
|
||||||
|
|
||||||
const mainRef = useRef(null);
|
const mainRef = useRef(null);
|
||||||
|
const pagesRef = useRef(null);
|
||||||
|
|
||||||
if(props.renderer == 'legacy') {
|
if(props.renderer == 'legacy') {
|
||||||
rawPages = props.text.split('\\page');
|
rawPages = props.text.split('\\page');
|
||||||
@@ -287,7 +293,7 @@ const BrewRenderer = (props)=>{
|
|||||||
<NotificationPopup />
|
<NotificationPopup />
|
||||||
</div>
|
</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.*/}
|
{/*render in iFrame so broken code doesn't crash the site.*/}
|
||||||
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
||||||
@@ -306,12 +312,13 @@ const BrewRenderer = (props)=>{
|
|||||||
&&
|
&&
|
||||||
<>
|
<>
|
||||||
{renderedStyle}
|
{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}
|
{renderedPages}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{headerState ? <HeaderNav ref={pagesRef} /> : <></>}
|
||||||
</Frame>
|
</Frame>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
|
|
||||||
.pane { position : relative; }
|
.pane { position : relative; }
|
||||||
|
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.toolBar { display : none; }
|
.toolBar { display : none; }
|
||||||
.brewRenderer {
|
.brewRenderer {
|
||||||
@@ -82,4 +83,7 @@
|
|||||||
& > .page { box-shadow : unset; }
|
& > .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 MAX_ZOOM = 300;
|
||||||
const MIN_ZOOM = 10;
|
const MIN_ZOOM = 10;
|
||||||
|
|
||||||
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages })=>{
|
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{
|
||||||
|
|
||||||
const [pageNum, setPageNum] = useState(1);
|
const [pageNum, setPageNum] = useState(1);
|
||||||
const [toolsVisible, setToolsVisible] = useState(true);
|
const [toolsVisible, setToolsVisible] = useState(true);
|
||||||
@@ -76,7 +76,10 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
<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*/}
|
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
||||||
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
|
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -166,7 +166,7 @@
|
|||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
flex-wrap : nowrap;
|
flex-wrap : nowrap;
|
||||||
width : 32px;
|
width : 92px;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
background-color : unset;
|
background-color : unset;
|
||||||
opacity : 0.5;
|
opacity : 0.5;
|
||||||
@@ -178,10 +178,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.toggleButton {
|
.toggleButton {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
left : 0;
|
left : 0;
|
||||||
z-index : 5;
|
z-index : 5;
|
||||||
width : 32px;
|
width : 32px;
|
||||||
min-width : unset;
|
min-width : unset;
|
||||||
|
height : 100%;
|
||||||
|
display : flex;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user