diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 9b68455e3..17f261c2d 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -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)=>{ - 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length}/> + 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/> {/*render in iFrame so broken code doesn't crash the site.*/} { && <> {renderedStyle} -
+
{renderedPages}
}
+ {headerState ? : <>} ); diff --git a/client/homebrew/brewRenderer/brewRenderer.less b/client/homebrew/brewRenderer/brewRenderer.less index b40e9062e..68c688fb6 100644 --- a/client/homebrew/brewRenderer/brewRenderer.less +++ b/client/homebrew/brewRenderer/brewRenderer.less @@ -70,6 +70,7 @@ .pane { position : relative; } + @media print { .toolBar { display : none; } .brewRenderer { @@ -82,4 +83,7 @@ & > .page { box-shadow : unset; } } } + .headerNav { + visibility: hidden; + } } \ No newline at end of file diff --git a/client/homebrew/brewRenderer/headerNav/headerNav.jsx b/client/homebrew/brewRenderer/headerNav/headerNav.jsx new file mode 100644 index 000000000..68963129f --- /dev/null +++ b/client/homebrew/brewRenderer/headerNav/headerNav.jsx @@ -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 ; + }); + + }; + + return ; +} +); + +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
  • + + {trimString(text, depth)} + +
  • ; +}; + +export default HeaderNav; \ No newline at end of file diff --git a/client/homebrew/brewRenderer/headerNav/headerNav.less b/client/homebrew/brewRenderer/headerNav/headerNav.less new file mode 100644 index 000000000..8b35041d9 --- /dev/null +++ b/client/homebrew/brewRenderer/headerNav/headerNav.less @@ -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); + } + }); + } + } +} \ No newline at end of file diff --git a/client/homebrew/brewRenderer/toolBar/toolBar.jsx b/client/homebrew/brewRenderer/toolBar/toolBar.jsx index 72ab02c9a..f11d1f127 100644 --- a/client/homebrew/brewRenderer/toolBar/toolBar.jsx +++ b/client/homebrew/brewRenderer/toolBar/toolBar.jsx @@ -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 (