mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-18 18:42:41 +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:
@@ -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 } = React;
|
||||
const { useState, useRef, useCallback, useEffect } = React;
|
||||
const _ = require('lodash');
|
||||
|
||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||
@@ -30,14 +30,47 @@ const INITIAL_CONTENT = dedent`
|
||||
</head><body style='overflow: hidden'><div></div></body></html>`;
|
||||
|
||||
//v=====----------------------< Brew Page Component >---------------------=====v//
|
||||
const BrewPage = (props)=>{
|
||||
props = {
|
||||
contents : '',
|
||||
index : 0,
|
||||
...props
|
||||
};
|
||||
const cleanText = props.contents; //DOMPurify.sanitize(props.contents, purifyConfig);
|
||||
return <div className={props.className} id={`p${props.index + 1}`} >
|
||||
const BrewPage = ({contents = '', index = 0, onVisibilityChange, onCenterPageChange, ...props})=>{
|
||||
const pageRef = useRef(null);
|
||||
const cleanText = contents; //DOMPurify.sanitize(props.contents, purifyConfig);
|
||||
|
||||
useEffect(()=>{
|
||||
if(!pageRef.current) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(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>;
|
||||
};
|
||||
@@ -64,11 +97,14 @@ const BrewRenderer = (props)=>{
|
||||
};
|
||||
|
||||
const [state, setState] = useState({
|
||||
isMounted : false,
|
||||
visibility : 'hidden',
|
||||
zoom : 100
|
||||
isMounted : false,
|
||||
visibility : 'hidden',
|
||||
zoom : 100,
|
||||
visiblePages : [],
|
||||
formattedPages : '',
|
||||
centerPage : 1
|
||||
});
|
||||
|
||||
const iframeRef = useRef(null);
|
||||
const mainRef = useRef(null);
|
||||
|
||||
if(props.renderer == 'legacy') {
|
||||
@@ -77,13 +113,54 @@ const BrewRenderer = (props)=>{
|
||||
rawPages = props.text.split(/^\\page$/gm);
|
||||
}
|
||||
|
||||
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);
|
||||
useEffect(() => {
|
||||
props.onPageChange(formatVisiblePages(state.visiblePages));
|
||||
}, [state.visiblePages]);
|
||||
|
||||
props.onPageChange(currentPageNumber);
|
||||
}, 200), []);
|
||||
const handlePageVisibilityChange = useCallback((pageNum, isVisible) => {
|
||||
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)=>{
|
||||
if(!state.isMounted)
|
||||
@@ -113,11 +190,11 @@ const BrewRenderer = (props)=>{
|
||||
const renderPage = (pageText, index)=>{
|
||||
if(props.renderer == 'legacy') {
|
||||
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 {
|
||||
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);
|
||||
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.*/}
|
||||
{!state.isMounted
|
||||
? <div className='brewRenderer' onScroll={updateCurrentPage}>
|
||||
? <div className='brewRenderer'
|
||||
// onScroll={updateCurrentPage}
|
||||
>
|
||||
<div className='pages'>
|
||||
{renderDummyPage(1)}
|
||||
</div>
|
||||
@@ -196,7 +275,7 @@ const BrewRenderer = (props)=>{
|
||||
<NotificationPopup />
|
||||
</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.*/}
|
||||
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
||||
@@ -205,17 +284,19 @@ const BrewRenderer = (props)=>{
|
||||
onClick={()=>{emitClick();}}
|
||||
>
|
||||
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
|
||||
onScroll={updateCurrentPage}
|
||||
// onScroll={updateCurrentPage}
|
||||
onKeyDown={handleControlKeys}
|
||||
tabIndex={-1}
|
||||
style={ styleObject }>
|
||||
style={ styleObject }
|
||||
>
|
||||
|
||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||
{state.isMounted
|
||||
&&
|
||||
<>
|
||||
{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()}
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user