mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-06 07:52:40 +00:00
Add View Mode Options
Adds a new AnchoredBox component that is functionally a clone of the "saving error" notifications, but drops a lot of the JS in favor of the new (chrome-only!) CSS Anchor Positioning API. In subsequent commits, either alternate styling or a polyfill will be added non-supported browsers. The box contains a few inputs that modify the CSS applied to `.pages`, most critically a "start on right" toggle for the Facing Pages mode.
This commit is contained in:
55
client/components/anchoredBox.jsx
Normal file
55
client/components/anchoredBox.jsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
require('./anchoredBox.less');
|
||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
const AnchoredBox = ({ anchorPoint = 'center', className, children, ...props })=>{
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const triggerRef = useRef(null);
|
||||||
|
const boxRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
const handleClickOutside = (evt)=>{
|
||||||
|
if(boxRef.current &&
|
||||||
|
!boxRef.current.contains(evt.target) &&
|
||||||
|
triggerRef.current &&
|
||||||
|
!triggerRef.current.contains(evt.target)){
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('click', handleClickOutside);
|
||||||
|
|
||||||
|
const iframe = document.querySelector('iframe');
|
||||||
|
if(iframe) {
|
||||||
|
iframe.addEventListener('load', ()=>{
|
||||||
|
const iframeDoc = iframe.contentWindow.document;
|
||||||
|
if(iframeDoc) {
|
||||||
|
iframeDoc.addEventListener('click', handleClickOutside);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ()=>{
|
||||||
|
window.removeEventListener('click', handleClickOutside);
|
||||||
|
if(iframe?.contentWindow?.document) {
|
||||||
|
iframe.contentWindow.document.removeEventListener('click', handleClickOutside);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []); // Empty dependency array to run effect on mount only
|
||||||
|
|
||||||
|
const handleClick = ()=>{
|
||||||
|
setVisible(!visible);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button className={`${className} anchored-trigger${visible ? ' active' : ''}`} onClick={handleClick} ref={triggerRef}>
|
||||||
|
<i className='fas fa-gear' />
|
||||||
|
</button>
|
||||||
|
<div className={`anchored-box${visible ? ' active' : ''}`} ref={boxRef}>
|
||||||
|
<h1>{props.title}</h1>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnchoredBox;
|
||||||
57
client/components/anchoredBox.less
Normal file
57
client/components/anchoredBox.less
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
.anchored-trigger {
|
||||||
|
anchor-name: --view-settings;
|
||||||
|
&.active {
|
||||||
|
background-color: #444444;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.anchored-box {
|
||||||
|
position-anchor: --view-settings;
|
||||||
|
position:absolute;
|
||||||
|
inset-block-start: anchor(bottom);
|
||||||
|
justify-self: anchor-center;
|
||||||
|
visibility: hidden;
|
||||||
|
&.active {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
padding : 15px;
|
||||||
|
color : white;
|
||||||
|
background-color : #555555;
|
||||||
|
border-radius : 5px;
|
||||||
|
font-size : .8em;
|
||||||
|
margin-top : 10px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
border-bottom: 1px solid currentColor;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: lightgray;
|
||||||
|
border-bottom: 1px solid currentColor;
|
||||||
|
margin: 1em 0 0.5em 0;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
position: absolute;
|
||||||
|
border: 10px solid transparent;
|
||||||
|
border-bottom: 10px solid #555555;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
top: -20px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,8 @@ const BrewRenderer = (props)=>{
|
|||||||
height : PAGE_HEIGHT,
|
height : PAGE_HEIGHT,
|
||||||
isMounted : false,
|
isMounted : false,
|
||||||
visibility : 'hidden',
|
visibility : 'hidden',
|
||||||
zoom : 100
|
zoom : 100,
|
||||||
|
pagesStyle : null
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainRef = useRef(null);
|
const mainRef = useRef(null);
|
||||||
@@ -187,6 +188,13 @@ const BrewRenderer = (props)=>{
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStyle = (newStyle)=>{
|
||||||
|
setState((prevState)=>({
|
||||||
|
...prevState,
|
||||||
|
pagesStyle : { ...prevState.pagesStyle, ...newStyle },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/*render dummy page while iFrame is mounting.*/}
|
{/*render dummy page while iFrame is mounting.*/}
|
||||||
@@ -204,7 +212,7 @@ const BrewRenderer = (props)=>{
|
|||||||
<NotificationPopup />
|
<NotificationPopup />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToolBar onZoomChange={handleZoom} currentPage={props.currentBrewRendererPageNum} totalPages={rawPages.length}/>
|
<ToolBar onZoomChange={handleZoom} currentPage={props.currentBrewRendererPageNum} totalPages={rawPages.length} onStyleChange={handleStyle} />
|
||||||
|
|
||||||
{/*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}
|
||||||
@@ -223,7 +231,7 @@ const BrewRenderer = (props)=>{
|
|||||||
&&
|
&&
|
||||||
<>
|
<>
|
||||||
{renderStyle()}
|
{renderStyle()}
|
||||||
<div className='pages' lang={`${props.lang || 'en'}`} style={{ zoom: `${state.zoom}%` }}>
|
<div className='pages' lang={`${props.lang || 'en'}`} style={{ zoom: `${state.zoom}%`, ...state.pagesStyle }}>
|
||||||
{renderPages()}
|
{renderPages()}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ const React = require('react');
|
|||||||
const { useState, useEffect } = React;
|
const { useState, useEffect } = React;
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
import AnchoredBox from '../../../components/anchoredBox.jsx';
|
||||||
|
// import * as ZoomIcons from '../../../icons/icon-components/zoomIcons.jsx';
|
||||||
|
|
||||||
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, onPageChange, totalPages, onStyleChange })=>{
|
||||||
|
|
||||||
const [zoomLevel, setZoomLevel] = useState(100);
|
const [zoomLevel, setZoomLevel] = useState(100);
|
||||||
const [pageNum, setPageNum] = useState(currentPage);
|
const [pageNum, setPageNum] = useState(currentPage);
|
||||||
const [arrangement, setArrangement] = useState('single');
|
const [arrangement, setArrangement] = useState('single');
|
||||||
const [startOnRight, setStartOnRight] = useState(true);
|
const [startOnRight, setStartOnRight] = useState(true);
|
||||||
|
const [pagesStyle, setPagesStyle] = useState({});
|
||||||
const [toolsVisible, setToolsVisible] = useState(true);
|
const [toolsVisible, setToolsVisible] = useState(true);
|
||||||
const modes = ['single', 'facing', 'flow'];
|
const modes = ['single', 'facing', 'flow'];
|
||||||
|
|
||||||
@@ -25,6 +28,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
|
|||||||
}, [currentPage]);;
|
}, [currentPage]);;
|
||||||
|
|
||||||
// update display arrangement when arrangement state is changed.
|
// update display arrangement when arrangement state is changed.
|
||||||
|
// todo: do this the 'react' way, without querying the dom.
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
const iframe = document.getElementById('BrewRenderer');
|
const iframe = document.getElementById('BrewRenderer');
|
||||||
const pagesContainer = iframe?.contentWindow?.document.querySelector('.pages');
|
const pagesContainer = iframe?.contentWindow?.document.querySelector('.pages');
|
||||||
@@ -35,7 +39,6 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
|
|||||||
['recto', 'verso'].forEach((leaf)=>pagesContainer.classList.remove(leaf));
|
['recto', 'verso'].forEach((leaf)=>pagesContainer.classList.remove(leaf));
|
||||||
pagesContainer.classList.add(startOnRight ? 'recto' : 'verso');
|
pagesContainer.classList.add(startOnRight ? 'recto' : 'verso');
|
||||||
}
|
}
|
||||||
}, [arrangement]);
|
|
||||||
}, [arrangement, startOnRight]);
|
}, [arrangement, startOnRight]);
|
||||||
|
|
||||||
|
|
||||||
@@ -152,6 +155,19 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
|
|||||||
>
|
>
|
||||||
{arrangement}
|
{arrangement}
|
||||||
</button>
|
</button>
|
||||||
|
<AnchoredBox id='view-mode-options' className='tool' title='Options'>
|
||||||
|
<label title='Modify the horizontal space between pages.'>Column gap<input type='range' min={0} max={200} className='range-input' onChange={(evt)=>onStyleChange({ columnGap: `${evt.target.value}px` })} /></label>
|
||||||
|
<label title='Modify the vertical space between rows of pages.'>Row gap<input type='range' min={0} max={200} className='range-input' onChange={(evt)=>onStyleChange({ rowGap: `${evt.target.value}px` })} /></label>
|
||||||
|
|
||||||
|
<h2>Facing</h2>
|
||||||
|
<label title='Start 1st page on the right side, such as if you have cover page.'>Start on right
|
||||||
|
<input type='checkbox'
|
||||||
|
onChange={()=>setStartOnRight(!startOnRight)}
|
||||||
|
checked={startOnRight}
|
||||||
|
disabled={arrangement !== 'facing' ? true : false}
|
||||||
|
title={arrangement !== 'facing' ? 'Switch to Facing to enable toggle.' : null} />
|
||||||
|
</label>
|
||||||
|
</AnchoredBox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='group'>
|
<div className='group'>
|
||||||
|
|||||||
@@ -34,6 +34,13 @@
|
|||||||
align-items : center;
|
align-items : center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.anchored-box {
|
||||||
|
color: #CCCCCC;
|
||||||
|
input[type='number']{
|
||||||
|
width: 4ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
position : relative;
|
position : relative;
|
||||||
height : 1.5em;
|
height : 1.5em;
|
||||||
|
|||||||
Reference in New Issue
Block a user