diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index b19bcdce3..27a1b25e2 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -1,7 +1,7 @@ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ require('./brewRenderer.less'); const React = require('react'); -const { useState, useRef, useCallback, useMemo } = React; +const { useState, useRef, useCallback, useMemo, useEffect } = React; const _ = require('lodash'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); @@ -32,12 +32,54 @@ const INITIAL_CONTENT = dedent` //v=====----------------------< Brew Page Component >---------------------=====v// const BrewPage = (props)=>{ props = { - contents : '', - index : 0, + contents : '', + index : 0, + onVisibilityChange : ()=>{}, + onCenterPageChange : ()=>{}, ...props }; + const pageRef = useRef(null); const cleanText = safeHTML(props.contents); - return
+ + useEffect(()=>{ + if(!pageRef.current) return; + + // Observer for tracking pages within the `.pages` div + const visibleObserver = new IntersectionObserver( + (entries)=>{ + entries.forEach((entry)=>{ + if(entry.isIntersecting){ + props.onVisibilityChange(props.index + 1, true); // add page to array of visible pages. + } else { + props.onVisibilityChange(props.index + 1, false); + } + }); + }, + { 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.onCenterPageChange(props.index + 1); // Set this page as the center page + } + }); + }, + { threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center + ); + + // attach observers to each `.page` + visibleObserver.observe(pageRef.current); + centerObserver.observe(pageRef.current); + return ()=>{ + visibleObserver.disconnect(); + centerObserver.disconnect(); + }; + }, [props.index, props.onVisibilityChange, props.onCenterPageChange]); + + return
; }; @@ -65,7 +107,9 @@ const BrewRenderer = (props)=>{ const [state, setState] = useState({ isMounted : false, - visibility : 'hidden' + visibility : 'hidden', + visiblePages : [], + centerPage : 1 }); const [displayOptions, setDisplayOptions] = useState({ @@ -75,6 +119,7 @@ const BrewRenderer = (props)=>{ pageShadows : true }); + const iframeRef = useRef(null); const mainRef = useRef(null); if(props.renderer == 'legacy') { @@ -83,33 +128,33 @@ const BrewRenderer = (props)=>{ rawPages = props.text.split(/^\\page$/gm); } - const scrollToHash = (hash)=>{ - if(!hash) return; + // update centerPage (aka "current page") and pass it up to parent components + useEffect(()=>{ + props.onPageChange(state.centerPage); + }, [state.centerPage]); - const iframeDoc = document.getElementById('BrewRenderer').contentDocument; - let anchor = iframeDoc.querySelector(hash); + const handlePageVisibilityChange = useCallback((pageNum, isVisible)=>{ + setState((prevState)=>{ + const updatedVisiblePages = new Set(prevState.visiblePages); + if(isVisible){ + updatedVisiblePages.add(pageNum); + } else { + updatedVisiblePages.delete(pageNum); + } + const pages = Array.from(updatedVisiblePages); - if(anchor) { - anchor.scrollIntoView({ behavior: 'smooth' }); - } else { - // Use MutationObserver to wait for the element if it's not immediately available - new MutationObserver((mutations, obs)=>{ - anchor = iframeDoc.querySelector(hash); - if(anchor) { - anchor.scrollIntoView({ behavior: 'smooth' }); - obs.disconnect(); - } - }).observe(iframeDoc, { childList: true, subtree: true }); - } - }; + return { ...prevState, + visiblePages : _.sortBy(pages) + }; + }); + }, []); - const updateCurrentPage = useCallback(_.throttle((e)=>{ - const { scrollTop, clientHeight, scrollHeight } = e.target; - const totalScrollableHeight = scrollHeight - clientHeight; - const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1); - - props.onPageChange(currentPageNumber); - }, 200), []); + const handleCenterPageChange = useCallback((pageNum)=>{ + setState((prevState)=>({ + ...prevState, + centerPage : pageNum, + })); + }, []); const isInView = (index)=>{ if(!state.isMounted) @@ -139,7 +184,7 @@ const BrewRenderer = (props)=>{ const renderPage = (pageText, index)=>{ if(props.renderer == 'legacy') { const html = MarkdownLegacy.render(pageText); - return ; + return ; } else { pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) const html = Markdown.render(pageText, index); @@ -149,7 +194,7 @@ const BrewRenderer = (props)=>{ // Add more conditions as needed }; - return ; + return ; } }; @@ -182,7 +227,6 @@ const BrewRenderer = (props)=>{ }; const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount" - scrollToHash(window.location.hash); 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 @@ -222,7 +266,7 @@ const BrewRenderer = (props)=>{ <> {/*render dummy page while iFrame is mounting.*/} {!state.isMounted - ?
+ ?
{renderDummyPage(1)}
@@ -235,7 +279,7 @@ const BrewRenderer = (props)=>{
- + {/*render in iFrame so broken code doesn't crash the site.*/} { onClick={()=>{emitClick();}} >
+ style={ styleObject } + > {/* Apply CSS from Style tab and render pages from Markdown tab */} {state.isMounted && <> - {renderStyle()} -
- {renderPages()} + {renderedStyle} +
+ {renderedPages}
} diff --git a/client/homebrew/brewRenderer/toolBar/toolBar.jsx b/client/homebrew/brewRenderer/toolBar/toolBar.jsx index b0300e0e4..483e57df1 100644 --- a/client/homebrew/brewRenderer/toolBar/toolBar.jsx +++ b/client/homebrew/brewRenderer/toolBar/toolBar.jsx @@ -4,19 +4,20 @@ const { useState, useEffect } = React; const _ = require('lodash'); import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx'; -// import * as ZoomIcons from '../../../icons/icon-components/zoomIcons.jsx'; const MAX_ZOOM = 300; const MIN_ZOOM = 10; -const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChange })=>{ +const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages })=>{ - const [pageNum, setPageNum] = useState(currentPage); + const [pageNum, setPageNum] = useState(1); const [toolsVisible, setToolsVisible] = useState(true); useEffect(()=>{ - setPageNum(currentPage); - }, [currentPage]); + if(visiblePages.length !== 0){ // If zoomed in enough, it's possible that no page fits the intersection criteria, so don't update. + setPageNum(formatVisiblePages(visiblePages)); + } + }, [visiblePages]); const handleZoomButton = (zoom)=>{ handleOptionChange('zoomLevel', _.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM))); @@ -28,20 +29,21 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan }; const handlePageInput = (pageInput)=>{ + console.log(pageInput); if(/[0-9]/.test(pageInput)) setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number. }; + // scroll to a page, used in the Prev/Next Page buttons. const scrollToPage = (pageNumber)=>{ + if(typeof pageNumber !== 'number') return; pageNumber = _.clamp(pageNumber, 1, totalPages); const iframe = document.getElementById('BrewRenderer'); const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer'); const page = brewRenderer?.querySelector(`#p${pageNumber}`); page?.scrollIntoView({ block: 'start' }); - setPageNum(pageNumber); }; - const calculateChange = (mode)=>{ const iframe = document.getElementById('BrewRenderer'); const iframeWidth = iframe.getBoundingClientRect().width; @@ -69,6 +71,28 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan return deltaZoom; }; + // format the visible pages to work with ranges, including separate ranges ("2-7, 10-15") + const formatVisiblePages = (pages)=>{ + if(pages.length === 0) return ''; + + const sortedPages = [...pages].sort((a, b)=>a - b); // Copy and sort the array + const ranges = []; + let start = sortedPages[0]; + + for (let i = 1; i <= sortedPages.length; i++) { + // If the current page is not consecutive or it's the end of the list + if(i === sortedPages.length || sortedPages[i] !== sortedPages[i - 1] + 1) { + // Push the range to the list + ranges.push( + start === sortedPages[i - 1] ? `${start}` : `${start} - ${sortedPages[i - 1]}` + ); + start = sortedPages[i]; // Start a new range + } + } + + return ranges.join(', '); + }; + return (