mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-29 19:52:43 +00:00
If the 'facing' spread is active, the 'fit to view' zoom button fits two pages side by side in the view, rather than setting only one page in view.
273 lines
9.8 KiB
JavaScript
273 lines
9.8 KiB
JavaScript
/* eslint-disable max-lines */
|
|
require('./toolBar.less');
|
|
const React = require('react');
|
|
const { useState, useEffect } = React;
|
|
const _ = require('lodash');
|
|
|
|
import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx';
|
|
|
|
const MAX_ZOOM = 300;
|
|
const MIN_ZOOM = 10;
|
|
|
|
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages })=>{
|
|
|
|
const [pageNum, setPageNum] = useState(1);
|
|
const [toolsVisible, setToolsVisible] = useState(true);
|
|
|
|
useEffect(()=>{
|
|
if(visiblePages.length !== 0){ // If zoomed in enough, it's possible that no page fits the intersection criteria, so don't update.
|
|
setPageNum(formatVisiblePages(visiblePages));
|
|
}
|
|
}, [visiblePages]);
|
|
|
|
const handleZoomButton = (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)=>{
|
|
console.log(pageInput);
|
|
if(/[0-9]/.test(pageInput))
|
|
setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number.
|
|
};
|
|
|
|
// scroll to a page, used in the Prev/Next Page buttons.
|
|
const scrollToPage = (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' });
|
|
};
|
|
|
|
const calculateChange = (mode)=>{
|
|
const iframe = document.getElementById('BrewRenderer');
|
|
const iframeWidth = iframe.getBoundingClientRect().width;
|
|
const iframeHeight = iframe.getBoundingClientRect().height;
|
|
const pages = iframe.contentWindow.document.getElementsByClassName('page');
|
|
|
|
let desiredZoom = 0;
|
|
|
|
if(mode == 'fill'){
|
|
// find widest page, in case pages are different widths, so that the zoom is adapted to not cut the widest page off screen.
|
|
const widestPage = _.maxBy([...pages], 'offsetWidth').offsetWidth;
|
|
|
|
desiredZoom = (iframeWidth / widestPage) * 100;
|
|
|
|
} else if(mode == 'fit'){
|
|
let minDimRatio;
|
|
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
|
if(displayOptions.spread === 'facing'){
|
|
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth / 2), Infinity); // if 'facing' spread, fit two pages in view
|
|
} else {
|
|
minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
|
}
|
|
console.log(minDimRatio)
|
|
|
|
desiredZoom = minDimRatio * 100;
|
|
}
|
|
|
|
const margin = 5; // extra space so page isn't edge to edge (not truly "to fill")
|
|
|
|
const deltaZoom = (desiredZoom - displayOptions.zoomLevel) - margin;
|
|
return deltaZoom;
|
|
};
|
|
|
|
// format the visible pages to work with ranges, including separate ranges ("2-7, 10-15")
|
|
const formatVisiblePages = (pages)=>{
|
|
if(pages.length === 0) return '';
|
|
|
|
const sortedPages = [...pages].sort((a, b)=>a - b); // Copy and sort the array
|
|
const 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(', ');
|
|
};
|
|
|
|
return (
|
|
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
|
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
|
|
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
|
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
|
|
<button
|
|
id='fill-width'
|
|
className='tool'
|
|
title='Set zoom to fill preview with one page'
|
|
onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fill'))}
|
|
>
|
|
<i className='fac fit-width' />
|
|
</button>
|
|
<button
|
|
id='zoom-to-fit'
|
|
className='tool'
|
|
title='Set zoom to fit entire page in preview'
|
|
onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fit'))}
|
|
>
|
|
<i className='fac zoom-to-fit' />
|
|
</button>
|
|
<button
|
|
id='zoom-out'
|
|
className='tool'
|
|
onClick={()=>handleZoomButton(displayOptions.zoomLevel - 20)}
|
|
disabled={displayOptions.zoomLevel <= MIN_ZOOM}
|
|
title='Zoom Out'
|
|
>
|
|
<i className='fas fa-magnifying-glass-minus' />
|
|
</button>
|
|
<input
|
|
id='zoom-slider'
|
|
className='range-input tool hover-tooltip'
|
|
type='range'
|
|
name='zoom'
|
|
title='Set Zoom'
|
|
list='zoomLevels'
|
|
min={MIN_ZOOM}
|
|
max={MAX_ZOOM}
|
|
step='1'
|
|
value={displayOptions.zoomLevel}
|
|
onChange={(e)=>handleZoomButton(parseInt(e.target.value))}
|
|
/>
|
|
<datalist id='zoomLevels'>
|
|
<option value='100' />
|
|
</datalist>
|
|
|
|
<button
|
|
id='zoom-in'
|
|
className='tool'
|
|
onClick={()=>handleZoomButton(displayOptions.zoomLevel + 20)}
|
|
disabled={displayOptions.zoomLevel >= MAX_ZOOM}
|
|
title='Zoom In'
|
|
>
|
|
<i className='fas fa-magnifying-glass-plus' />
|
|
</button>
|
|
</div>
|
|
|
|
{/*v=====----------------------< Spread Controls >---------------------=====v*/}
|
|
<div className='group' role='group' aria-label='Spread' aria-hidden={!toolsVisible}>
|
|
<div className='radio-group' role='radiogroup'>
|
|
<button role='radio'
|
|
id='single-spread'
|
|
className='tool'
|
|
title='Single Page'
|
|
onClick={()=>{handleOptionChange('spread', 'active');}}
|
|
aria-checked={displayOptions.spread === 'single'}
|
|
><i className='fac single-spread' /></button>
|
|
<button role='radio'
|
|
id='facing-spread'
|
|
className='tool'
|
|
title='Facing Pages'
|
|
onClick={()=>{handleOptionChange('spread', 'facing');}}
|
|
aria-checked={displayOptions.spread === 'facing'}
|
|
><i className='fac facing-spread' /></button>
|
|
<button role='radio'
|
|
id='flow-spread'
|
|
className='tool'
|
|
title='Flow Pages'
|
|
onClick={()=>{handleOptionChange('spread', 'flow');}}
|
|
aria-checked={displayOptions.spread === 'flow'}
|
|
><i className='fac flow-spread' /></button>
|
|
|
|
</div>
|
|
<Anchored>
|
|
<AnchoredTrigger id='spread-settings' className='tool' title='Spread options'><i className='fas fa-gear' /></AnchoredTrigger>
|
|
<AnchoredBox title='Options'>
|
|
<h1>Options</h1>
|
|
<label title='Modify the horizontal space between pages.'>
|
|
Column gap
|
|
<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} />
|
|
</label>
|
|
<label title='Modify the vertical space between rows of pages.'>
|
|
Row gap
|
|
<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} />
|
|
</label>
|
|
<label title='Start 1st page on the right side, such as if you have cover page.'>
|
|
Start on right
|
|
<input type='checkbox' checked={displayOptions.startOnRight} onChange={()=>{handleOptionChange('startOnRight', !displayOptions.startOnRight);}}
|
|
title={displayOptions.spread !== 'facing' ? 'Switch to Facing to enable toggle.' : null} />
|
|
</label>
|
|
<label title='Toggle the page shadow on every page.'>
|
|
Page shadows
|
|
<input type='checkbox' checked={displayOptions.pageShadows} onChange={()=>{handleOptionChange('pageShadows', !displayOptions.pageShadows);}} />
|
|
</label>
|
|
</AnchoredBox>
|
|
</Anchored>
|
|
</div>
|
|
|
|
{/*v=====----------------------< Page Controls >---------------------=====v*/}
|
|
<div className='group' role='group' aria-label='Pages' aria-hidden={!toolsVisible}>
|
|
<button
|
|
id='previous-page'
|
|
className='previousPage tool'
|
|
type='button'
|
|
title='Previous Page(s)'
|
|
onClick={()=>{
|
|
const rangeOffset = visiblePages.length > 1 ? 1 : 0;
|
|
scrollToPage(_.min(visiblePages) - visiblePages.length + rangeOffset);
|
|
}}
|
|
disabled={pageNum <= 1}
|
|
>
|
|
<i className='fas fa-arrow-left'></i>
|
|
</button>
|
|
|
|
<div className='tool'>
|
|
<input
|
|
id='page-input'
|
|
className='text-input'
|
|
type='text'
|
|
name='page'
|
|
title='Current page(s) in view'
|
|
inputMode='numeric'
|
|
pattern='[0-9]'
|
|
value={`${pageNum}`}
|
|
onClick={(e)=>e.target.select()}
|
|
onChange={(e)=>handlePageInput(e.target.value)}
|
|
onBlur={()=>scrollToPage(pageNum)}
|
|
onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)}
|
|
/>
|
|
<span id='page-count' title='Total Page Count'>/ {totalPages}</span>
|
|
</div>
|
|
|
|
<button
|
|
id='next-page'
|
|
className='tool'
|
|
type='button'
|
|
title='Next Page(s)'
|
|
onClick={()=>{
|
|
// if there are multiple pages in a 'row' and they are in 'view',
|
|
// then the 'max'/last page in view will always be the same, and
|
|
// the other pages will always be the same (since the viewport doesn't change).
|
|
// So this needs to scroll to the 'max', then see what is newly in view,
|
|
// and if the same pages are visible, do it again but +1.
|
|
const start = _.max(visiblePages);
|
|
scrollToPage(start);
|
|
if(start === _.max(visiblePages)){
|
|
scrollToPage(start + 1)
|
|
};
|
|
}}
|
|
disabled={pageNum >= totalPages}
|
|
>
|
|
<i className='fas fa-arrow-right'></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
module.exports = ToolBar;
|