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:
@@ -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 \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
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);
|
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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user