0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-03 14:52:38 +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}]*/
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&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);
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>
</>

View File

@@ -7,12 +7,20 @@ const _ = require('lodash');
const MAX_ZOOM = 300;
const MIN_ZOOM = 10;
const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
const ToolBar = ({ onZoomChange, currentPage, visiblePages, formattedPages, centerPage, totalPages })=>{
const [zoomLevel, setZoomLevel] = useState(100);
const [pageNum, setPageNum] = useState(currentPage);
const [pageNum, setPageNum] = useState(null);
const [toolsVisible, setToolsVisible] = useState(true);
useEffect(()=>{
setPageNum(visiblePages[0]);
}, []);
useEffect(()=>{
setPageNum(formattedPages);
}, [visiblePages]);
useEffect(()=>{
onZoomChange(zoomLevel);
}, [zoomLevel]);
@@ -26,17 +34,21 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
};
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.
};
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);
const iframe = document.getElementById('BrewRenderer');
const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer');
const page = brewRenderer?.querySelector(`#p${pageNumber}`);
page?.scrollIntoView({ block: 'start' });
setPageNum(pageNumber);
};
@@ -125,7 +137,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
<button
id='previous-page'
className='previousPage tool'
onClick={()=>scrollToPage(pageNum - 1)}
onClick={()=>scrollToPage(_.min(visiblePages) - visiblePages.length)}
disabled={pageNum <= 1}
>
<i className='fas fa-arrow-left'></i>
@@ -139,7 +151,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
name='page'
inputMode='numeric'
pattern='[0-9]'
value={pageNum}
value={`${pageNum}`}
onClick={(e)=>e.target.select()}
onChange={(e)=>handlePageInput(e.target.value)}
onBlur={()=>scrollToPage(pageNum)}
@@ -151,7 +163,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
<button
id='next-page'
className='tool'
onClick={()=>scrollToPage(pageNum + 1)}
onClick={()=>scrollToPage(_.max(visiblePages) + 1)}
disabled={pageNum >= totalPages}
>
<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
&#page-input {
width : 4ch;
width : 8ch;
margin-right : 1ch;
text-align : center;
}