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:
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user