0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-19 18:32:39 +00:00

Setup Intersection Observers & more...

Bad commit here with too much stuff.  I apologize.

This sets up two Intersection Observers: the first captures every page that is at least 30% visible inside the `.pages` container, and the second captures every page that has at least one pixel on the horizontal center line of `.pages`.  Both can be arrays of integers (page index).

The "visiblePages" array is duplicated and formatted into a "formattedPages" state, which gets displayed in the toolbar.

The toolbar displays that, unless the user clicks into the page input and enters their own integer (only a single integer, no range), which can then jump the preview to that page on Enter or blur().

The Arrow 'change page' buttons jump the preview back and forth by a 'full set'.
 If one page is viewed at a time, this is moved on page a time, and if 10 pages are viewed at a time it jumps the pages by 10.

Left to do:  adapt the "jump editor to match preview" divider button to work with new "centerPage".
This commit is contained in:
Gazook89
2024-10-21 00:30:45 -05:00
parent f1af87ee7e
commit 41fdf48ad3
3 changed files with 126 additions and 33 deletions

View File

@@ -1,7 +1,7 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
require('./brewRenderer.less'); require('./brewRenderer.less');
const React = require('react'); const React = require('react');
const { useState, useRef, useCallback } = React; const { useState, useRef, useCallback, useEffect } = React;
const _ = require('lodash'); const _ = require('lodash');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
@@ -30,14 +30,47 @@ const INITIAL_CONTENT = dedent`
</head><body style='overflow: hidden'><div></div></body></html>`; </head><body style='overflow: hidden'><div></div></body></html>`;
//v=====----------------------< Brew Page Component >---------------------=====v// //v=====----------------------< Brew Page Component >---------------------=====v//
const BrewPage = (props)=>{ const BrewPage = ({contents = '', index = 0, onVisibilityChange, onCenterPageChange, ...props})=>{
props = { const pageRef = useRef(null);
contents : '', const cleanText = contents; //DOMPurify.sanitize(props.contents, purifyConfig);
index : 0,
...props useEffect(()=>{
}; if(!pageRef.current) return;
const cleanText = props.contents; //DOMPurify.sanitize(props.contents, purifyConfig); const observer = new IntersectionObserver(
return <div className={props.className} id={`p${props.index + 1}`} > (entries)=>{
entries.forEach((entry)=>{
if(entry.isIntersecting){
onVisibilityChange(index + 1, true);
} else {
onVisibilityChange(index + 1, false);
}
});
},
{ threshold: .3, rootMargin: '0px 0px 0px 0px' }
);
// Observer for tracking the page at the center of the iframe.
const centerObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
onCenterPageChange(index + 1); // Set this page as the center page
}
});
},
{ threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center
);
observer.observe(pageRef.current);
centerObserver.observe(pageRef.current);
return ()=>{
observer.disconnect();
centerObserver.disconnect();
};
}, [index, onVisibilityChange, onCenterPageChange]);
return <div className={props.className} id={`p${index + 1}`} data-index={index} ref={pageRef}>
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} /> <div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
</div>; </div>;
}; };
@@ -64,11 +97,14 @@ const BrewRenderer = (props)=>{
}; };
const [state, setState] = useState({ const [state, setState] = useState({
isMounted : false, isMounted : false,
visibility : 'hidden', visibility : 'hidden',
zoom : 100 zoom : 100,
visiblePages : [],
formattedPages : '',
centerPage : 1
}); });
const iframeRef = useRef(null);
const mainRef = useRef(null); const mainRef = useRef(null);
if(props.renderer == 'legacy') { if(props.renderer == 'legacy') {
@@ -77,13 +113,54 @@ const BrewRenderer = (props)=>{
rawPages = props.text.split(/^\\page$/gm); rawPages = props.text.split(/^\\page$/gm);
} }
const updateCurrentPage = useCallback(_.throttle((e)=>{ useEffect(() => {
const { scrollTop, clientHeight, scrollHeight } = e.target; props.onPageChange(formatVisiblePages(state.visiblePages));
const totalScrollableHeight = scrollHeight - clientHeight; }, [state.visiblePages]);
const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1);
props.onPageChange(currentPageNumber); const handlePageVisibilityChange = useCallback((pageNum, isVisible) => {
}, 200), []); setState((prevState) => {
let updatedVisiblePages = new Set(prevState.visiblePages);
if(isVisible){
updatedVisiblePages.add(pageNum)
} else {
updatedVisiblePages.delete(pageNum)
}
const pages = Array.from(updatedVisiblePages);
return { ...prevState,
visiblePages : _.sortBy(pages),
formattedPages : formatVisiblePages(pages)
};
});
}, []);
const formatVisiblePages = (pages) => {
if (pages.length === 0) return '';
const sortedPages = [...pages].sort((a, b) => a - b); // Copy and sort the array
let 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(', ');
};
const handleCenterPageChange = useCallback((pageNum) => {
setState((prevState) => ({
...prevState,
centerPage : pageNum,
}));
}, []);
const isInView = (index)=>{ const isInView = (index)=>{
if(!state.isMounted) if(!state.isMounted)
@@ -113,11 +190,11 @@ const BrewRenderer = (props)=>{
const renderPage = (pageText, index)=>{ const renderPage = (pageText, index)=>{
if(props.renderer == 'legacy') { if(props.renderer == 'legacy') {
const html = MarkdownLegacy.render(pageText); const html = MarkdownLegacy.render(pageText);
return <BrewPage className='page phb' index={index} key={index} contents={html} />; return <BrewPage className='page phb' index={index} key={index} contents={html} onVisibilityChange={handlePageVisibilityChange} onCenterPageChange={handleCenterPageChange} />;
} else { } else {
pageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) pageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //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); const html = Markdown.render(pageText, index);
return <BrewPage className='page' index={index} key={index} contents={html} />; return <BrewPage className='page' index={index} key={index} contents={html} onVisibilityChange={handlePageVisibilityChange} onCenterPageChange={handleCenterPageChange} />;
} }
}; };
@@ -183,7 +260,9 @@ const BrewRenderer = (props)=>{
<> <>
{/*render dummy page while iFrame is mounting.*/} {/*render dummy page while iFrame is mounting.*/}
{!state.isMounted {!state.isMounted
? <div className='brewRenderer' onScroll={updateCurrentPage}> ? <div className='brewRenderer'
// onScroll={updateCurrentPage}
>
<div className='pages'> <div className='pages'>
{renderDummyPage(1)} {renderDummyPage(1)}
</div> </div>
@@ -196,7 +275,7 @@ const BrewRenderer = (props)=>{
<NotificationPopup /> <NotificationPopup />
</div> </div>
<ToolBar onZoomChange={handleZoom} currentPage={props.currentBrewRendererPageNum} totalPages={rawPages.length}/> <ToolBar onZoomChange={handleZoom} centerPage={state.centerPage} visiblePages={state.visiblePages} formattedPages={state.formattedPages} totalPages={rawPages.length}/>
{/*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}
@@ -205,17 +284,19 @@ const BrewRenderer = (props)=>{
onClick={()=>{emitClick();}} onClick={()=>{emitClick();}}
> >
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`} <div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
onScroll={updateCurrentPage} // onScroll={updateCurrentPage}
onKeyDown={handleControlKeys} onKeyDown={handleControlKeys}
tabIndex={-1} tabIndex={-1}
style={ styleObject }> style={ styleObject }
>
{/* Apply CSS from Style tab and render pages from Markdown tab */} {/* Apply CSS from Style tab and render pages from Markdown tab */}
{state.isMounted {state.isMounted
&& &&
<> <>
{renderStyle()} {renderStyle()}
<div className='pages' lang={`${props.lang || 'en'}`} style={{ zoom: `${state.zoom}%` }}> <div className='pages' lang={`${props.lang || 'en'}`} style={{ zoom: `${state.zoom}%` }}
ref={iframeRef}>
{renderPages()} {renderPages()}
</div> </div>
</> </>

View File

@@ -7,12 +7,20 @@ const _ = require('lodash');
const MAX_ZOOM = 300; const MAX_ZOOM = 300;
const MIN_ZOOM = 10; const MIN_ZOOM = 10;
const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{ const ToolBar = ({ onZoomChange, currentPage, visiblePages, formattedPages, centerPage, totalPages })=>{
const [zoomLevel, setZoomLevel] = useState(100); const [zoomLevel, setZoomLevel] = useState(100);
const [pageNum, setPageNum] = useState(currentPage); const [pageNum, setPageNum] = useState(null);
const [toolsVisible, setToolsVisible] = useState(true); const [toolsVisible, setToolsVisible] = useState(true);
useEffect(()=>{
setPageNum(visiblePages[0]);
}, []);
useEffect(()=>{
setPageNum(formattedPages);
}, [visiblePages]);
useEffect(()=>{ useEffect(()=>{
onZoomChange(zoomLevel); onZoomChange(zoomLevel);
}, [zoomLevel]); }, [zoomLevel]);
@@ -26,17 +34,21 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
}; };
const handlePageInput = (pageInput)=>{ const handlePageInput = (pageInput)=>{
console.log(pageInput);
if(/[0-9]/.test(pageInput)) if(/[0-9]/.test(pageInput))
setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number. setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number.
}; };
const scrollToPage = (pageNumber)=>{ const scrollToPage = (pageNumber)=>{
console.log('visiblePages:', visiblePages);
console.log('centerPage:', centerPage);
console.log('pageNumber:', pageNumber);
if(typeof pageNumber !== 'number') return;
pageNumber = _.clamp(pageNumber, 1, totalPages); pageNumber = _.clamp(pageNumber, 1, totalPages);
const iframe = document.getElementById('BrewRenderer'); const iframe = document.getElementById('BrewRenderer');
const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer'); const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer');
const page = brewRenderer?.querySelector(`#p${pageNumber}`); const page = brewRenderer?.querySelector(`#p${pageNumber}`);
page?.scrollIntoView({ block: 'start' }); page?.scrollIntoView({ block: 'start' });
setPageNum(pageNumber);
}; };
@@ -125,7 +137,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
<button <button
id='previous-page' id='previous-page'
className='previousPage tool' className='previousPage tool'
onClick={()=>scrollToPage(pageNum - 1)} onClick={()=>scrollToPage(_.min(visiblePages) - visiblePages.length)}
disabled={pageNum <= 1} disabled={pageNum <= 1}
> >
<i className='fas fa-arrow-left'></i> <i className='fas fa-arrow-left'></i>
@@ -139,7 +151,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
name='page' name='page'
inputMode='numeric' inputMode='numeric'
pattern='[0-9]' pattern='[0-9]'
value={pageNum} value={`${pageNum}`}
onClick={(e)=>e.target.select()} onClick={(e)=>e.target.select()}
onChange={(e)=>handlePageInput(e.target.value)} onChange={(e)=>handlePageInput(e.target.value)}
onBlur={()=>scrollToPage(pageNum)} onBlur={()=>scrollToPage(pageNum)}
@@ -151,7 +163,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
<button <button
id='next-page' id='next-page'
className='tool' className='tool'
onClick={()=>scrollToPage(pageNum + 1)} onClick={()=>scrollToPage(_.max(visiblePages) + 1)}
disabled={pageNum >= totalPages} disabled={pageNum >= totalPages}
> >
<i className='fas fa-arrow-right'></i> <i className='fas fa-arrow-right'></i>

View File

@@ -76,7 +76,7 @@
// `.text-input` if generic to all range inputs, or `#page-input` if only for current page input // `.text-input` if generic to all range inputs, or `#page-input` if only for current page input
&#page-input { &#page-input {
width : 4ch; width : 8ch;
margin-right : 1ch; margin-right : 1ch;
text-align : center; text-align : center;
} }