diff --git a/client/components/Anchored.jsx b/client/components/Anchored.jsx new file mode 100644 index 000000000..4c7a225e4 --- /dev/null +++ b/client/components/Anchored.jsx @@ -0,0 +1,91 @@ +import React, { useState, useRef, forwardRef, useEffect, cloneElement, Children } from 'react'; +import './Anchored.less'; + +// Anchored is a wrapper component that must have as children an and a component. +// AnchoredTrigger must have a unique `id` prop, which is passed up to Anchored, saved in state on mount, and +// then passed down through props into AnchoredBox. The `id` is used for the CSS Anchor Positioning properties. +// **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. + + +const Anchored = ({ children })=>{ + const [visible, setVisible] = useState(false); + const [anchorId, setAnchorId] = useState(null); + const boxRef = useRef(null); + const triggerRef = useRef(null); + + // promote trigger id to Anchored id (to pass it back down to the box as "anchorId") + useEffect(()=>{ + if(triggerRef.current){ + setAnchorId(triggerRef.current.id); + } + }, []); + + // close box on outside click or Escape key + useEffect(()=>{ + const handleClickOutside = (evt)=>{ + if( + boxRef.current && + !boxRef.current.contains(evt.target) && + triggerRef.current && + !triggerRef.current.contains(evt.target) + ) { + setVisible(false); + } + }; + + const handleEscapeKey = (evt)=>{ + if(evt.key === 'Escape') setVisible(false); + }; + + window.addEventListener('click', handleClickOutside); + window.addEventListener('keydown', handleEscapeKey); + + return ()=>{ + window.removeEventListener('click', handleClickOutside); + window.removeEventListener('keydown', handleEscapeKey); + }; + }, []); + + const toggleVisibility = ()=>setVisible((prev)=>!prev); + + // Map children to inject necessary props + const mappedChildren = Children.map(children, (child)=>{ + if(child.type === AnchoredTrigger) { + return cloneElement(child, { ref: triggerRef, toggleVisibility, visible }); + } + if(child.type === AnchoredBox) { + return cloneElement(child, { ref: boxRef, visible, anchorId }); + } + return child; + }); + + return <>{mappedChildren}; +}; + +// forward ref for AnchoredTrigger +const AnchoredTrigger = forwardRef(({ toggleVisibility, visible, children, className, ...props }, ref)=>( + +)); + +// forward ref for AnchoredBox +const AnchoredBox = forwardRef(({ visible, children, className, anchorId, ...props }, ref)=>( +
+ {children} +
+)); + +export { Anchored, AnchoredTrigger, AnchoredBox }; diff --git a/client/components/Anchored.less b/client/components/Anchored.less new file mode 100644 index 000000000..4f0e2fa8f --- /dev/null +++ b/client/components/Anchored.less @@ -0,0 +1,13 @@ + + +.anchored-box { + position:absolute; + @supports (inset-block-start: anchor(bottom)){ + inset-block-start: anchor(bottom); + } + justify-self: anchor-center; + visibility: hidden; + &.active { + visibility: visible; + } +} \ No newline at end of file diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 8fc631eb5..1c45269cf 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -37,7 +37,7 @@ const BrewPage = (props)=>{ ...props }; const cleanText = safeHTML(props.contents); - return
+ return
; }; @@ -65,8 +65,14 @@ const BrewRenderer = (props)=>{ const [state, setState] = useState({ isMounted : false, - visibility : 'hidden', - zoom : 100 + visibility : 'hidden' + }); + + const [displayOptions, setDisplayOptions] = useState({ + zoomLevel : 100, + spread : 'single', + startOnRight : true, + pageShadows : true }); const mainRef = useRef(null); @@ -137,7 +143,13 @@ const BrewRenderer = (props)=>{ } 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 ; + + const styles = { + ...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {}) + // Add more conditions as needed + }; + + return ; } }; @@ -187,12 +199,14 @@ const BrewRenderer = (props)=>{ document.dispatchEvent(new MouseEvent('click')); }; - //Toolbar settings: - const handleZoom = (newZoom)=>{ - setState((prevState)=>({ - ...prevState, - zoom : newZoom - })); + const handleDisplayOptionsChange = (newDisplayOptions)=>{ + setDisplayOptions(newDisplayOptions); + }; + + const pagesStyle = { + zoom : `${displayOptions.zoomLevel}%`, + columnGap : `${displayOptions.columnGap}px`, + rowGap : `${displayOptions.rowGap}px` }; const styleObject = {}; @@ -221,7 +235,7 @@ const BrewRenderer = (props)=>{
- + {/*render in iFrame so broken code doesn't crash the site.*/} { && <> {renderedStyle} -
+
{renderedPages}
diff --git a/client/homebrew/brewRenderer/brewRenderer.less b/client/homebrew/brewRenderer/brewRenderer.less index 177576abc..5f08e9455 100644 --- a/client/homebrew/brewRenderer/brewRenderer.less +++ b/client/homebrew/brewRenderer/brewRenderer.less @@ -3,13 +3,45 @@ .brewRenderer { overflow-y : scroll; will-change : transform; - padding-top : 30px; + padding-top : 60px; height : 100vh; + &:has(.facing, .flow) { + padding : 60px 30px; + } &.deployment { background-color: darkred; } :where(.pages) { - margin : 30px 0px; + &.facing { + display: grid; + grid-template-columns: repeat(2, auto); + grid-template-rows: repeat(3, auto); + gap: 10px 10px; + justify-content: 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 + grid-column-start: 2; + } + & :where(.page) { + margin-left: unset !important; + margin-right: unset !important; + } + } + + &.flow { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: flex-start; + & :where(.page) { + flex: 0 0 auto; + margin-left: unset !important; + margin-right: unset !important; + } + + } + & > :where(.page) { width : 215.9mm; height : 279.4mm; diff --git a/client/homebrew/brewRenderer/toolBar/toolBar.jsx b/client/homebrew/brewRenderer/toolBar/toolBar.jsx index 73b48d778..b0300e0e4 100644 --- a/client/homebrew/brewRenderer/toolBar/toolBar.jsx +++ b/client/homebrew/brewRenderer/toolBar/toolBar.jsx @@ -3,26 +3,28 @@ const React = require('react'); const { useState, useEffect } = React; const _ = require('lodash'); +import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx'; +// import * as ZoomIcons from '../../../icons/icon-components/zoomIcons.jsx'; const MAX_ZOOM = 300; const MIN_ZOOM = 10; -const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{ +const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChange })=>{ - const [zoomLevel, setZoomLevel] = useState(100); - const [pageNum, setPageNum] = useState(currentPage); + const [pageNum, setPageNum] = useState(currentPage); const [toolsVisible, setToolsVisible] = useState(true); - useEffect(()=>{ - onZoomChange(zoomLevel); - }, [zoomLevel]); - useEffect(()=>{ setPageNum(currentPage); }, [currentPage]); const handleZoomButton = (zoom)=>{ - setZoomLevel(_.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM))); + handleOptionChange('zoomLevel', _.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM))); + }; + + const handleOptionChange = (optionKey, newValue)=>{ + //setDisplayOptions(prevOptions => ({ ...prevOptions, [optionKey]: newValue })); + onDisplayOptionsChange({ ...displayOptions, [optionKey]: newValue }); }; const handlePageInput = (pageInput)=>{ @@ -63,47 +65,51 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{ const margin = 5; // extra space so page isn't edge to edge (not truly "to fill") - const deltaZoom = (desiredZoom - zoomLevel) - margin; + const deltaZoom = (desiredZoom - displayOptions.zoomLevel) - margin; return deltaZoom; }; return ( -
+