Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intersection Observers for getting "Current Page(s)" #3845

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
41fdf48
Setup Intersection Observers & more...
Gazook89 Oct 21, 2024
119755e
Merge branch 'master' into Intersection-Observer
Gazook89 Oct 21, 2024
0afc2ab
modify effect to enable Jump Editor button
Gazook89 Oct 22, 2024
183dd63
style change on page text input
Gazook89 Oct 22, 2024
822d0c7
Fix `NaN`/`undefined` showing on first load
Gazook89 Oct 22, 2024
b6bd7cc
Merge branch 'master' into Intersection-Observer
Gazook89 Oct 22, 2024
de7b13b
Add some comments and cleanup
Gazook89 Oct 22, 2024
5c0d6e6
move formatting of visible pages to toolbar
Gazook89 Oct 22, 2024
26050e2
add comment
Gazook89 Oct 22, 2024
4126188
linting
Gazook89 Oct 22, 2024
5ab867f
adjust prev/next page buttons to meet expectations
Gazook89 Oct 23, 2024
93b9f1d
Merge branch 'Intersection-Observer' into Observer-Master-merge
Gazook89 Nov 5, 2024
2222550
Merge branch 'master' into Observer-Master-merge
Gazook89 Nov 7, 2024
3818424
Adjust "next page" button
Gazook89 Nov 7, 2024
274e734
add displayOptions to dependency array for memo
Gazook89 Nov 7, 2024
5b14e0e
tweak alignment of spreads
Gazook89 Nov 7, 2024
a0e88bb
Add comment about future Popover API use
Gazook89 Nov 7, 2024
63add04
'fit page' zoom button fits two pages in "facing" spread
Gazook89 Nov 7, 2024
a6bc87b
apply displayOptions to legacy brews as well.
Gazook89 Nov 7, 2024
9d86384
refactor styles
Gazook89 Nov 7, 2024
88b34a7
Fix 'current page' input when zoomed in close
Gazook89 Nov 7, 2024
9ef11bc
lint and refactor
Gazook89 Nov 7, 2024
650ec04
fix 'disabled' attribute on min/max of page range
Gazook89 Nov 8, 2024
28855d0
dynamic text input width to match characters
Gazook89 Nov 8, 2024
28a7f24
add scrollToHash method back in
Gazook89 Nov 8, 2024
2e8368d
give dismisskeys a default
Gazook89 Nov 9, 2024
b7b1981
lint
Gazook89 Nov 9, 2024
234d484
Merge remote-tracking branch 'upstream/master'
Gazook89 Nov 10, 2024
4bad047
Merge branch 'master' into Intersection-Observer
Gazook89 Nov 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions client/components/Anchored.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import './Anchored.less';
// **The Anchor Positioning API is not available in Firefox yet**
// So in Firefox the positioning isn't perfect but is likely sufficient, and FF team seems to be working on the API quickly.

// When Anchor Positioning is added to Firefox, this can also be rewritten using the Popover API-- add the `popover` attribute
// to the container div, which will render the container in the *top level* and give it better interactions like
// click outside to dismiss. **Do not** add without Anchor, though, because positioning is very limited with the `popover`
// attribute.


const Anchored = ({ children })=>{
const [visible, setVisible] = useState(false);
Expand Down
147 changes: 103 additions & 44 deletions client/homebrew/brewRenderer/brewRenderer.jsx
Original file line number Diff line number Diff line change
@@ -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, useMemo } = React;
const { useState, useRef, useCallback, useMemo, useEffect } = React;
const _ = require('lodash');

const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
Expand Down Expand Up @@ -32,12 +32,54 @@ const INITIAL_CONTENT = dedent`
//v=====----------------------< Brew Page Component >---------------------=====v//
const BrewPage = (props)=>{
props = {
contents : '',
index : 0,
contents : '',
index : 0,
onVisibilityChange : ()=>{},
onCenterPageChange : ()=>{},
...props
};
const pageRef = useRef(null);
const cleanText = safeHTML(props.contents);
return <div className={props.className} id={`p${props.index + 1}`} style={props.style}>

useEffect(()=>{
if(!pageRef.current) return;

// Observer for tracking pages within the `.pages` div
const visibleObserver = new IntersectionObserver(
(entries)=>{
entries.forEach((entry)=>{
if(entry.isIntersecting){
props.onVisibilityChange(props.index + 1, true); // add page to array of visible pages.
} else {
props.onVisibilityChange(props.index + 1, false);
}
});
},
{ threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds.
);

// Observer for tracking the page at the center of the iframe.
const centerObserver = new IntersectionObserver(
(entries)=>{
entries.forEach((entry)=>{
if(entry.isIntersecting) {
props.onCenterPageChange(props.index + 1); // Set this page as the center page
}
});
},
{ threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center
);

// attach observers to each `.page`
visibleObserver.observe(pageRef.current);
centerObserver.observe(pageRef.current);
return ()=>{
visibleObserver.disconnect();
centerObserver.disconnect();
};
}, [props.index, props.onVisibilityChange, props.onCenterPageChange]);

return <div className={props.className} id={`p${props.index + 1}`} data-index={props.index} ref={pageRef} style={props.style}>
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
</div>;
};
Expand All @@ -64,8 +106,10 @@ const BrewRenderer = (props)=>{
};

const [state, setState] = useState({
isMounted : false,
visibility : 'hidden'
isMounted : false,
visibility : 'hidden',
visiblePages : [],
centerPage : 1
});

const [displayOptions, setDisplayOptions] = useState({
Expand All @@ -75,6 +119,7 @@ const BrewRenderer = (props)=>{
pageShadows : true
});

const iframeRef = useRef(null);
const mainRef = useRef(null);

if(props.renderer == 'legacy') {
Expand All @@ -83,33 +128,26 @@ const BrewRenderer = (props)=>{
rawPages = props.text.split(/^\\page$/gm);
}

const scrollToHash = (hash)=>{
if(!hash) return;
const handlePageVisibilityChange = useCallback((pageNum, isVisible)=>{
setState((prevState)=>{
const updatedVisiblePages = new Set(prevState.visiblePages);
isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum);

const iframeDoc = document.getElementById('BrewRenderer').contentDocument;
let anchor = iframeDoc.querySelector(hash);

if(anchor) {
anchor.scrollIntoView({ behavior: 'smooth' });
} else {
// Use MutationObserver to wait for the element if it's not immediately available
new MutationObserver((mutations, obs)=>{
anchor = iframeDoc.querySelector(hash);
if(anchor) {
anchor.scrollIntoView({ behavior: 'smooth' });
obs.disconnect();
}
}).observe(iframeDoc, { childList: true, subtree: true });
}
};
return {
...prevState,
visiblePages : [...updatedVisiblePages].sort((a, b)=>a - b)
};
});
}, []);

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);
const handleCenterPageChange = useCallback((pageNum)=>{
setState((prevState)=>({
...prevState,
centerPage : pageNum,
}));

props.onPageChange(currentPageNumber);
}, 200), []);
props.onPageChange(pageNum);
}, [props.onPageChange]);

const isInView = (index)=>{
if(!state.isMounted)
Expand Down Expand Up @@ -137,19 +175,21 @@ const BrewRenderer = (props)=>{
};

const renderPage = (pageText, index)=>{

const styles = {
...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {})
// Add more conditions as needed
};

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} style={styles} 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);

const styles = {
...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {})
// Add more conditions as needed
};

return <BrewPage className='page' index={index} key={index} contents={html} style={styles} />;
return <BrewPage className='page' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} onCenterPageChange={handleCenterPageChange} />;
}
};

Expand Down Expand Up @@ -181,6 +221,26 @@ const BrewRenderer = (props)=>{
}
};

const scrollToHash = (hash)=>{
if(!hash) return;

const iframeDoc = document.getElementById('BrewRenderer').contentDocument;
let anchor = iframeDoc.querySelector(hash);

if(anchor) {
anchor.scrollIntoView({ behavior: 'smooth' });
} else {
// Use MutationObserver to wait for the element if it's not immediately available
new MutationObserver((mutations, obs)=>{
anchor = iframeDoc.querySelector(hash);
if(anchor) {
anchor.scrollIntoView({ behavior: 'smooth' });
obs.disconnect();
}
}).observe(iframeDoc, { childList: true, subtree: true });
}
};

const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
scrollToHash(window.location.hash);

Expand Down Expand Up @@ -216,13 +276,13 @@ const BrewRenderer = (props)=>{
}

const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
renderedPages = useMemo(()=>renderPages(), [props.text]);
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);

return (
<>
{/*render dummy page while iFrame is mounting.*/}
{!state.isMounted
? <div className='brewRenderer' onScroll={updateCurrentPage}>
? <div className='brewRenderer'>
<div className='pages'>
{renderDummyPage(1)}
</div>
Expand All @@ -235,7 +295,7 @@ const BrewRenderer = (props)=>{
<NotificationPopup />
</div>

<ToolBar displayOptions={displayOptions} currentPage={props.currentBrewRendererPageNum} totalPages={rawPages.length} onDisplayOptionsChange={handleDisplayOptionsChange} />
<ToolBar displayOptions={displayOptions} onDisplayOptionsChange={handleDisplayOptionsChange} centerPage={state.centerPage} visiblePages={state.visiblePages.length > 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length}/>

{/*render in iFrame so broken code doesn't crash the site.*/}
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
Expand All @@ -244,18 +304,17 @@ const BrewRenderer = (props)=>{
onClick={()=>{emitClick();}}
>
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
onScroll={updateCurrentPage}
onKeyDown={handleControlKeys}
tabIndex={-1}
style={ styleObject }>
style={ styleObject }
>

{/* Apply CSS from Style tab and render pages from Markdown tab */}
{state.isMounted
&&
<>
{renderedStyle}
<div lang={`${props.lang || 'en'}`} style={pagesStyle} className={
`pages ${displayOptions.startOnRight ? 'recto' : 'verso'} ${displayOptions.spread}` } >
<div className={`pages ${displayOptions.startOnRight ? 'recto' : 'verso'} ${displayOptions.spread}`} lang={`${props.lang || 'en'}`} style={pagesStyle} ref={iframeRef}>
{renderedPages}
</div>
</>
Expand Down
4 changes: 2 additions & 2 deletions client/homebrew/brewRenderer/brewRenderer.less
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
grid-template-columns: repeat(2, auto);
grid-template-rows: repeat(3, auto);
gap: 10px 10px;
justify-content: center;
justify-content: safe center;
&.recto .page:first-child {
// sets first page on 'right' ('recto') of the preview, as if for a Cover page.
// todo: add a checkbox to toggle this setting
Expand All @@ -33,7 +33,7 @@
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-start;
justify-content: safe center;
& :where(.page) {
flex: 0 0 auto;
margin-left: unset !important;
Expand Down
Loading