mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #18435 from overleaf/dp-ae-pdf-viewer-controls
Update PDF viewer controls GitOrigin-RevId: 4e15b7cbd34e878d0175be635369b8d620188203
This commit is contained in:
parent
e0f6622519
commit
55e54ce875
12 changed files with 721 additions and 48 deletions
|
@ -328,6 +328,7 @@ const _ProjectController = {
|
|||
'pdf-caching-mode',
|
||||
'pdf-caching-prefetch-large',
|
||||
'pdf-caching-prefetching',
|
||||
'pdf-controls',
|
||||
'pdfjs-40',
|
||||
'personal-access-token',
|
||||
'revert-file',
|
||||
|
|
|
@ -808,6 +808,7 @@
|
|||
"newsletter": "",
|
||||
"newsletter_onboarding_accept": "",
|
||||
"next": "",
|
||||
"next_page": "",
|
||||
"next_payment_of_x_collectected_on_y": "",
|
||||
"no_actions": "",
|
||||
"no_borders": "",
|
||||
|
@ -934,6 +935,7 @@
|
|||
"postal_code": "",
|
||||
"premium_feature": "",
|
||||
"premium_plan_label": "",
|
||||
"previous_page": "",
|
||||
"price": "",
|
||||
"primarily_work_study_question": "",
|
||||
"primarily_work_study_question_company": "",
|
||||
|
@ -1529,6 +1531,7 @@
|
|||
"view_metrics_commons_subtext": "",
|
||||
"view_metrics_group_subtext": "",
|
||||
"view_more": "",
|
||||
"view_options": "",
|
||||
"view_pdf": "",
|
||||
"view_your_invoices": "",
|
||||
"viewing_x": "",
|
||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { debounce, throttle } from 'lodash'
|
||||
import PdfViewerControls from './pdf-viewer-controls'
|
||||
import PdfViewerControlsToolbar from './pdf-viewer-controls-toolbar'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
import { buildHighlightElement } from '../util/highlights'
|
||||
|
@ -14,6 +15,7 @@ import * as eventTracking from '../../../infrastructure/event-tracking'
|
|||
import { getPdfCachingMetrics } from '../util/metrics'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
function PdfJsViewer({ url, pdfFile }) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
@ -23,16 +25,34 @@ function PdfJsViewer({ url, pdfFile }) {
|
|||
|
||||
const { setLoadingError } = usePdfPreviewContext()
|
||||
|
||||
const hasNewPdfToolbar = useFeatureFlag('pdf-controls')
|
||||
|
||||
// state values persisted in localStorage to restore on load
|
||||
const [scale, setScale] = usePersistedState(
|
||||
`pdf-viewer-scale:${projectId}`,
|
||||
'page-width'
|
||||
)
|
||||
|
||||
// rawScale is different from scale as it is always a number.
|
||||
// This is relevant when scale is e.g. 'page-width'.
|
||||
const [rawScale, setRawScale] = useState(null)
|
||||
const [page, setPage] = useState(null)
|
||||
const [totalPages, setTotalPages] = useState(null)
|
||||
|
||||
// local state values
|
||||
const [pdfJsWrapper, setPdfJsWrapper] = useState()
|
||||
const [initialised, setInitialised] = useState(false)
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
newPage => {
|
||||
setPage(newPage)
|
||||
if (pdfJsWrapper?.viewer) {
|
||||
pdfJsWrapper.viewer.currentPageNumber = newPage
|
||||
}
|
||||
},
|
||||
[pdfJsWrapper, setPage]
|
||||
)
|
||||
|
||||
// create the viewer when the container is mounted
|
||||
const handleContainer = useCallback(
|
||||
parent => {
|
||||
|
@ -109,13 +129,27 @@ function PdfJsViewer({ url, pdfFile }) {
|
|||
pdfJsWrapper.eventBus.off('pagerendered', handleRendered)
|
||||
}
|
||||
|
||||
const handleRenderedInitialPageNumber = () => {
|
||||
setPage(pdfJsWrapper.viewer.currentPageNumber)
|
||||
}
|
||||
|
||||
const handleScaleChanged = () => {
|
||||
setRawScale(pdfJsWrapper.viewer.currentScale)
|
||||
}
|
||||
|
||||
// `pagesinit` fires when the data for rendering the first page is ready.
|
||||
pdfJsWrapper.eventBus.on('pagesinit', handlePagesinit)
|
||||
// `pagerendered` fires when a page was actually rendered.
|
||||
pdfJsWrapper.eventBus.on('pagerendered', handleRendered)
|
||||
// Once a page has been rendered we know the scale that it has been rendered to.
|
||||
pdfJsWrapper.eventBus.on('pagerendered', handleScaleChanged)
|
||||
// Once a page has been rendered we can set the initial current page number.
|
||||
pdfJsWrapper.eventBus.on('pagerendered', handleRenderedInitialPageNumber)
|
||||
return () => {
|
||||
pdfJsWrapper.eventBus.off('pagesinit', handlePagesinit)
|
||||
pdfJsWrapper.eventBus.off('pagerendered', handleRendered)
|
||||
pdfJsWrapper.eventBus.off('pagerendered', handleScaleChanged)
|
||||
pdfJsWrapper.eventBus.off('pagerendered', handleRenderedInitialPageNumber)
|
||||
}
|
||||
}, [pdfJsWrapper, firstRenderDone, startFetch])
|
||||
|
||||
|
@ -138,6 +172,9 @@ function PdfJsViewer({ url, pdfFile }) {
|
|||
}
|
||||
pdfJsWrapper
|
||||
.loadDocument({ url, pdfFile, abortController, handleFetchError })
|
||||
.then(doc => {
|
||||
setTotalPages(doc.numPages)
|
||||
})
|
||||
.catch(error => {
|
||||
if (abortController.signal.aborted) return
|
||||
debugConsole.error(error)
|
||||
|
@ -175,6 +212,7 @@ function PdfJsViewer({ url, pdfFile }) {
|
|||
|
||||
const scrollListener = () => {
|
||||
storePosition(pdfJsWrapper)
|
||||
setPage(pdfJsWrapper.viewer.currentPageNumber)
|
||||
}
|
||||
|
||||
pdfJsWrapper.container.addEventListener('scroll', scrollListener)
|
||||
|
@ -233,7 +271,6 @@ function PdfJsViewer({ url, pdfFile }) {
|
|||
}
|
||||
}, [pdfJsWrapper])
|
||||
|
||||
// restore the saved scale and scroll position
|
||||
const positionRef = useRef(position)
|
||||
useEffect(() => {
|
||||
positionRef.current = position
|
||||
|
@ -244,6 +281,7 @@ function PdfJsViewer({ url, pdfFile }) {
|
|||
scaleRef.current = scale
|
||||
}, [scale])
|
||||
|
||||
// restore the saved scale and scroll position
|
||||
useEffect(() => {
|
||||
if (initialised && pdfJsWrapper) {
|
||||
if (!pdfJsWrapper.isVisible()) {
|
||||
|
@ -332,6 +370,8 @@ function PdfJsViewer({ url, pdfFile }) {
|
|||
const setZoom = useCallback(
|
||||
zoom => {
|
||||
switch (zoom) {
|
||||
// TODO: We can remove fit-width and fit-height once the
|
||||
// pdf toolbar is fully rolled out
|
||||
case 'fit-width':
|
||||
setScale('page-width')
|
||||
break
|
||||
|
@ -339,7 +379,6 @@ function PdfJsViewer({ url, pdfFile }) {
|
|||
case 'fit-height':
|
||||
setScale('page-height')
|
||||
break
|
||||
|
||||
case 'zoom-in':
|
||||
if (pdfJsWrapper) {
|
||||
setScale(pdfJsWrapper.viewer.currentScale * 1.25)
|
||||
|
@ -351,6 +390,9 @@ function PdfJsViewer({ url, pdfFile }) {
|
|||
setScale(pdfJsWrapper.viewer.currentScale * 0.75)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
setScale(zoom)
|
||||
}
|
||||
},
|
||||
[pdfJsWrapper, setScale]
|
||||
|
@ -394,7 +436,7 @@ function PdfJsViewer({ url, pdfFile }) {
|
|||
|
||||
case '0':
|
||||
event.preventDefault()
|
||||
setZoom('fit-width')
|
||||
setZoom('page-width')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -432,20 +474,37 @@ function PdfJsViewer({ url, pdfFile }) {
|
|||
}
|
||||
}, [pdfJsWrapper])
|
||||
|
||||
// Don't render the toolbar until we have the necessary information
|
||||
const toolbarInfoLoaded =
|
||||
rawScale !== null && page !== null && totalPages !== null
|
||||
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||
return (
|
||||
<div className="pdfjs-viewer pdfjs-viewer-outer" ref={handleContainer}>
|
||||
<div
|
||||
className="pdfjs-viewer-inner"
|
||||
role="tabpanel"
|
||||
tabIndex="0"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div
|
||||
className="pdfjs-viewer pdfjs-viewer-outer"
|
||||
ref={handleContainer}
|
||||
onKeyDown={handleKeyDown}
|
||||
role="tabpanel"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="pdfjs-viewer-inner">
|
||||
<div className="pdfViewer" />
|
||||
</div>
|
||||
<div className="pdfjs-controls" tabIndex="0">
|
||||
<PdfViewerControls setZoom={setZoom} />
|
||||
{hasNewPdfToolbar ? (
|
||||
toolbarInfoLoaded && (
|
||||
<PdfViewerControlsToolbar
|
||||
setZoom={setZoom}
|
||||
rawScale={rawScale}
|
||||
setPage={handlePageChange}
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<PdfViewerControls setZoom={setZoom} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import { ButtonGroup } from 'react-bootstrap'
|
||||
import PDFToolbarButton from './pdf-toolbar-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
type PdfPageNumberControlProps = {
|
||||
setPage: (page: number) => void
|
||||
page: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
function PdfPageNumberControl({
|
||||
setPage,
|
||||
page,
|
||||
totalPages,
|
||||
}: PdfPageNumberControlProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [pageInputValue, setPageInputValue] = useState(page.toString())
|
||||
|
||||
useEffect(() => {
|
||||
setPageInputValue(page.toString())
|
||||
}, [page])
|
||||
|
||||
const handleSubmit = (event: React.SyntheticEvent) => {
|
||||
event.preventDefault()
|
||||
const parsedValue = Number(pageInputValue)
|
||||
if (parsedValue < 1) {
|
||||
setPage(1)
|
||||
setPageInputValue('1')
|
||||
} else if (parsedValue > totalPages) {
|
||||
setPage(totalPages)
|
||||
setPageInputValue(`${totalPages}`)
|
||||
} else {
|
||||
setPage(parsedValue)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup className="pdfjs-toolbar-buttons ">
|
||||
<PDFToolbarButton
|
||||
tooltipId="pdf-controls-previous-page-tooltip"
|
||||
icon="keyboard_arrow_up"
|
||||
label={t('previous_page')}
|
||||
disabled={page === 1}
|
||||
onClick={() => setPage(page - 1)}
|
||||
/>
|
||||
<PDFToolbarButton
|
||||
tooltipId="pdf-controls-next-page-tooltip"
|
||||
icon="keyboard_arrow_down"
|
||||
label={t('next_page')}
|
||||
disabled={page === totalPages}
|
||||
onClick={() => setPage(page + 1)}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<div className="pdfjs-page-number-input">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
inputMode="numeric"
|
||||
value={pageInputValue}
|
||||
onFocus={event => event.target.select()}
|
||||
onBlur={handleSubmit}
|
||||
onChange={event => {
|
||||
const rawValue = event.target.value
|
||||
setPageInputValue(rawValue.replace(/\D/g, ''))
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
<span>/ {totalPages}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PdfPageNumberControl
|
|
@ -9,6 +9,7 @@ import PdfHybridDownloadButton from './pdf-hybrid-download-button'
|
|||
import PdfHybridCodeCheckButton from './pdf-hybrid-code-check-button'
|
||||
import PdfOrphanRefreshButton from './pdf-orphan-refresh-button'
|
||||
import { DetachedSynctexControl } from './detach-synctex-control'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
||||
const ORPHAN_UI_TIMEOUT_MS = 5000
|
||||
|
@ -54,6 +55,8 @@ function PdfPreviewHybridToolbar() {
|
|||
}
|
||||
|
||||
function PdfPreviewHybridToolbarInner() {
|
||||
const hasNewPdfToolbar = useFeatureFlag('pdf-controls')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="toolbar-pdf-left">
|
||||
|
@ -62,6 +65,9 @@ function PdfPreviewHybridToolbarInner() {
|
|||
<PdfHybridDownloadButton />
|
||||
</div>
|
||||
<div className="toolbar-pdf-right">
|
||||
{hasNewPdfToolbar && (
|
||||
<div className="toolbar-pdf-controls" id="toolbar-pdf-controls" />
|
||||
)}
|
||||
<PdfHybridCodeCheckButton />
|
||||
<SwitchToEditorButton />
|
||||
<DetachedSynctexControl />
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import Button from 'react-bootstrap/lib/Button'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import Tooltip from '@/shared/components/tooltip'
|
||||
|
||||
type PDFToolbarButtonProps = {
|
||||
tooltipId: string
|
||||
icon: string
|
||||
label: string
|
||||
onClick: () => void
|
||||
shortcut?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function PDFToolbarButton({
|
||||
tooltipId,
|
||||
disabled,
|
||||
label,
|
||||
icon,
|
||||
onClick,
|
||||
shortcut,
|
||||
}: PDFToolbarButtonProps) {
|
||||
return (
|
||||
<Tooltip
|
||||
id={tooltipId}
|
||||
description={
|
||||
<>
|
||||
<div>{label}</div>
|
||||
{shortcut && <div>{shortcut}</div>}
|
||||
</>
|
||||
}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
<Button
|
||||
aria-label={label}
|
||||
bsSize="large"
|
||||
bsStyle={null}
|
||||
className="pdfjs-toolbar-button"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
<MaterialIcon type={icon} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import { useRef } from 'react'
|
||||
|
||||
import PdfPageNumberControl from './pdf-page-number-control'
|
||||
import PdfZoomButtons from './pdf-zoom-buttons'
|
||||
import { Button, Overlay, Popover } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import Tooltip from '@/shared/components/tooltip'
|
||||
import useDropdown from '@/shared/hooks/use-dropdown'
|
||||
|
||||
type PdfViewerControlsMenuButtonProps = {
|
||||
setZoom: (zoom: string) => void
|
||||
setPage: (page: number) => void
|
||||
page: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export default function PdfViewerControlsMenuButton({
|
||||
setZoom,
|
||||
setPage,
|
||||
page,
|
||||
totalPages,
|
||||
}: PdfViewerControlsMenuButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
open: popoverOpen,
|
||||
onToggle: togglePopover,
|
||||
ref: popoverRef,
|
||||
} = useDropdown()
|
||||
|
||||
const targetRef = useRef<any>(null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
id="pdf-controls-menu-tooltip"
|
||||
description={t('view_options')}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
<Button
|
||||
className="pdfjs-toolbar-popover-button"
|
||||
onClick={togglePopover}
|
||||
ref={targetRef}
|
||||
>
|
||||
<MaterialIcon type="more_horiz" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Overlay
|
||||
show={popoverOpen}
|
||||
target={targetRef.current}
|
||||
placement="bottom"
|
||||
containerPadding={0}
|
||||
animation
|
||||
rootClose
|
||||
onHide={() => togglePopover(false)}
|
||||
>
|
||||
<Popover
|
||||
className="pdfjs-toolbar-popover"
|
||||
id="pdf-toolbar-popover-menu"
|
||||
ref={popoverRef}
|
||||
>
|
||||
<PdfPageNumberControl
|
||||
setPage={setPage}
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
<div className="pdfjs-zoom-controls">
|
||||
<PdfZoomButtons setZoom={setZoom} />
|
||||
</div>
|
||||
</Popover>
|
||||
</Overlay>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
import { memo, useCallback, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import PdfPageNumberControl from './pdf-page-number-control'
|
||||
import PdfZoomButtons from './pdf-zoom-buttons'
|
||||
import PdfZoomDropdown from './pdf-zoom-dropdown'
|
||||
|
||||
import { useResizeObserver } from '@/shared/hooks/use-resize-observer'
|
||||
import PdfViewerControlsMenuButton from './pdf-viewer-controls-menu-button'
|
||||
|
||||
type PdfViewerControlsToolbarProps = {
|
||||
setZoom: (zoom: string) => void
|
||||
rawScale: number
|
||||
setPage: (page: number) => void
|
||||
page: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
function PdfViewerControlsToolbar({
|
||||
setZoom,
|
||||
rawScale,
|
||||
setPage,
|
||||
page,
|
||||
totalPages,
|
||||
}: PdfViewerControlsToolbarProps) {
|
||||
const toolbarControlsElement = document.querySelector('#toolbar-pdf-controls')
|
||||
|
||||
const [availableWidth, setAvailableWidth] = useState<number>(1000)
|
||||
|
||||
const handleResize = useCallback(
|
||||
element => {
|
||||
setAvailableWidth(element.offsetWidth)
|
||||
},
|
||||
[setAvailableWidth]
|
||||
)
|
||||
|
||||
const { elementRef: pdfControlsRef } = useResizeObserver(handleResize)
|
||||
|
||||
if (!toolbarControlsElement) {
|
||||
return null
|
||||
}
|
||||
|
||||
const InnerControlsComponent =
|
||||
availableWidth >= 300
|
||||
? PdfViewerControlsToolbarFull
|
||||
: PdfViewerControlsToolbarSmall
|
||||
|
||||
return createPortal(
|
||||
<div className="pdfjs-viewer-controls" ref={pdfControlsRef}>
|
||||
<InnerControlsComponent
|
||||
setZoom={setZoom}
|
||||
rawScale={rawScale}
|
||||
setPage={setPage}
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>,
|
||||
|
||||
toolbarControlsElement
|
||||
)
|
||||
}
|
||||
|
||||
type InnerControlsProps = {
|
||||
setZoom: (zoom: string) => void
|
||||
rawScale: number
|
||||
setPage: (page: number) => void
|
||||
page: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
function PdfViewerControlsToolbarFull({
|
||||
setZoom,
|
||||
rawScale,
|
||||
setPage,
|
||||
page,
|
||||
totalPages,
|
||||
}: InnerControlsProps) {
|
||||
return (
|
||||
<>
|
||||
<PdfPageNumberControl
|
||||
setPage={setPage}
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
<div className="pdfjs-zoom-controls">
|
||||
<PdfZoomButtons setZoom={setZoom} />
|
||||
<PdfZoomDropdown rawScale={rawScale} setZoom={setZoom} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PdfViewerControlsToolbarSmall({
|
||||
setZoom,
|
||||
rawScale,
|
||||
setPage,
|
||||
page,
|
||||
totalPages,
|
||||
}: InnerControlsProps) {
|
||||
return (
|
||||
<div className="pdfjs-viewer-controls-small">
|
||||
<PdfZoomDropdown rawScale={rawScale} setZoom={setZoom} />
|
||||
<PdfViewerControlsMenuButton
|
||||
setZoom={setZoom}
|
||||
setPage={setPage}
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PdfViewerControlsToolbar)
|
|
@ -0,0 +1,37 @@
|
|||
import { ButtonGroup } from 'react-bootstrap'
|
||||
import PDFToolbarButton from './pdf-toolbar-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const isMac = /Mac/.test(window.navigator?.platform)
|
||||
|
||||
type PdfZoomButtonsProps = {
|
||||
setZoom: (zoom: string) => void
|
||||
}
|
||||
|
||||
function PdfZoomButtons({ setZoom }: PdfZoomButtonsProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const zoomInShortcut = isMac ? '⌘+' : 'Ctrl++'
|
||||
const zoomOutShortcut = isMac ? '⌘-' : 'Ctrl+-'
|
||||
|
||||
return (
|
||||
<ButtonGroup className="pdfjs-toolbar-buttons">
|
||||
<PDFToolbarButton
|
||||
tooltipId="pdf-controls-zoom-out-tooltip"
|
||||
label={t('zoom_out')}
|
||||
icon="remove"
|
||||
onClick={() => setZoom('zoom-out')}
|
||||
shortcut={zoomOutShortcut}
|
||||
/>
|
||||
<PDFToolbarButton
|
||||
tooltipId="pdf-controls-zoom-in-tooltip"
|
||||
label={t('zoom_in')}
|
||||
icon="add"
|
||||
onClick={() => setZoom('zoom-in')}
|
||||
shortcut={zoomInShortcut}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default PdfZoomButtons
|
|
@ -0,0 +1,133 @@
|
|||
import { Dropdown, MenuItem } from 'react-bootstrap'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ControlledDropdown from '@/shared/components/controlled-dropdown'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const isMac = /Mac/.test(window.navigator?.platform)
|
||||
|
||||
const shortcuts = isMac
|
||||
? {
|
||||
'zoom-in': ['⌘', '+'],
|
||||
'zoom-out': ['⌘', '-'],
|
||||
'fit-to-width': ['⌘', '0'],
|
||||
}
|
||||
: {
|
||||
'zoom-in': ['Ctrl', '+'],
|
||||
'zoom-out': ['Ctrl', '-'],
|
||||
'fit-to-width': ['Ctrl', '0'],
|
||||
}
|
||||
|
||||
type PdfZoomDropdownProps = {
|
||||
setZoom: (zoom: string) => void
|
||||
rawScale: number
|
||||
}
|
||||
|
||||
const zoomValues = ['0.5', '0.75', '1', '1.5', '2', '4']
|
||||
|
||||
const rawScaleToPercentage = (rawScale: number) => {
|
||||
return `${Math.round(rawScale * 100)}%`
|
||||
}
|
||||
|
||||
function PdfZoomDropdown({ setZoom, rawScale }: PdfZoomDropdownProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [customZoomValue, setCustomZoomValue] = useState<string>(
|
||||
rawScaleToPercentage(rawScale)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setCustomZoomValue(rawScaleToPercentage(rawScale))
|
||||
}, [rawScale])
|
||||
|
||||
return (
|
||||
<ControlledDropdown
|
||||
id="pdf-zoom-dropdown"
|
||||
onSelect={eventKey => {
|
||||
if (eventKey !== 'custom-zoom') setZoom(eventKey)
|
||||
}}
|
||||
pullRight
|
||||
>
|
||||
<Dropdown.Toggle
|
||||
className="btn pdfjs-zoom-dropdown-button small"
|
||||
value={rawScale}
|
||||
title={rawScaleToPercentage(rawScale)}
|
||||
/>
|
||||
<Dropdown.Menu className="pdfjs-zoom-dropdown-menu">
|
||||
<MenuItem
|
||||
draggable={false}
|
||||
disabled
|
||||
className="pdfjs-custom-zoom-menu-item"
|
||||
key="custom-zoom"
|
||||
eventKey="custom-zoom"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
onFocus={event => event.target.select()}
|
||||
value={customZoomValue}
|
||||
onKeyDown={event => {
|
||||
if (event.key === 'Enter') {
|
||||
const zoom = Number(customZoomValue.replace('%', '')) / 100
|
||||
|
||||
// Only allow zoom values between 10% and 999%
|
||||
if (zoom < 0.1) {
|
||||
setZoom('0.1')
|
||||
} else if (zoom > 9.99) {
|
||||
setZoom('9.99')
|
||||
} else {
|
||||
setZoom(`${zoom}`)
|
||||
}
|
||||
}
|
||||
}}
|
||||
onChange={event => {
|
||||
const rawValue = event.target.value
|
||||
const parsedValue = rawValue.replace(/[^0-9%]/g, '')
|
||||
setCustomZoomValue(parsedValue)
|
||||
}}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem draggable={false} key="zoom-in" eventKey="zoom-in">
|
||||
<span>{t('zoom_in')}</span>
|
||||
<Shortcut keys={shortcuts['zoom-in']} />
|
||||
</MenuItem>
|
||||
<MenuItem draggable={false} key="zoom-out" eventKey="zoom-out">
|
||||
<span>{t('zoom_out')}</span>
|
||||
<Shortcut keys={shortcuts['zoom-out']} />
|
||||
</MenuItem>
|
||||
<MenuItem draggable={false} key="page-width" eventKey="page-width">
|
||||
{t('fit_to_width')}
|
||||
<Shortcut keys={shortcuts['fit-to-width']} />
|
||||
</MenuItem>
|
||||
<MenuItem draggable={false} key="page-height" eventKey="page-height">
|
||||
{t('fit_to_height')}
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
{zoomValues.map(value => (
|
||||
<MenuItem draggable={false} key={value} eventKey={value}>
|
||||
{rawScaleToPercentage(Number(value))}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</ControlledDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
function Shortcut({ keys }: { keys: string[] }) {
|
||||
return (
|
||||
<span className="pull-right">
|
||||
{keys.map((key, idx) => (
|
||||
<span
|
||||
className={classNames({
|
||||
'pdfjs-zoom-dropdown-mac-shortcut-char': key.length === 1,
|
||||
})}
|
||||
key={`${key}${idx}`}
|
||||
>
|
||||
{key}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default PdfZoomDropdown
|
|
@ -17,15 +17,26 @@
|
|||
|
||||
.toolbar-pdf-orphan,
|
||||
.toolbar-pdf-left,
|
||||
.toolbar-pdf-right {
|
||||
.toolbar-pdf-right,
|
||||
.toolbar-pdf-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.toolbar-pdf-orphan,
|
||||
.toolbar-pdf-controls {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.toolbar-pdf-controls {
|
||||
margin-right: 4px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.toolbar-pdf-right {
|
||||
flex: 1 0 auto;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.toolbar-pdf-orphan {
|
||||
|
@ -203,52 +214,163 @@
|
|||
background-color: @link-color;
|
||||
}
|
||||
}
|
||||
.pdfjs-controls {
|
||||
position: absolute;
|
||||
padding: @line-height-computed / 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
z-index: 10; // above the PDF viewer
|
||||
}
|
||||
|
||||
// TODO: remove this block once the new pdfjs toolbar is fully rolled out
|
||||
.pdfjs-controls {
|
||||
position: absolute;
|
||||
padding: @line-height-computed / 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
z-index: 10; // above the PDF viewer
|
||||
|
||||
.btn-group {
|
||||
transition:
|
||||
opacity 0.5s ease,
|
||||
visibility 0 linear 0.5s;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:focus-within,
|
||||
&:hover {
|
||||
// make .pdfjs-controls and its children visible when it or any of its descendants are focused
|
||||
.btn-group {
|
||||
transition:
|
||||
opacity 0.5s ease,
|
||||
visibility 0 linear 0.5s;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: none;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-within,
|
||||
&:hover {
|
||||
// make .pdfjs-controls and its children visible when it or any of its descendants are focused
|
||||
.btn-group {
|
||||
transition: none;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
&:hover,
|
||||
&.flash {
|
||||
.btn-group {
|
||||
transition: none;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.flash {
|
||||
.btn-group {
|
||||
transition: none;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
i.fa-arrows-h {
|
||||
border-right: 2px solid @content-primary;
|
||||
border-left: 2px solid @content-primary;
|
||||
}
|
||||
i.fa-arrows-v {
|
||||
border-top: 2px solid @content-primary;
|
||||
border-bottom: 2px solid @content-primary;
|
||||
}
|
||||
}
|
||||
|
||||
i.fa-arrows-h {
|
||||
border-right: 2px solid @content-primary;
|
||||
border-left: 2px solid @content-primary;
|
||||
}
|
||||
i.fa-arrows-v {
|
||||
border-top: 2px solid @content-primary;
|
||||
border-bottom: 2px solid @content-primary;
|
||||
.pdfjs-viewer-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdfjs-zoom-controls {
|
||||
border-left: 1px solid rgba(125, 125, 125, 0.3);
|
||||
}
|
||||
|
||||
.pdfjs-toolbar-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pdfjs-toolbar-button {
|
||||
padding: 2px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: @toolbar-btn-color;
|
||||
}
|
||||
}
|
||||
|
||||
.pdfjs-zoom-dropdown-button {
|
||||
width: 60px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
|
||||
.caret {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.pdfjs-zoom-dropdown-mac-shortcut-char {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pdfjs-custom-zoom-menu-item {
|
||||
a:hover {
|
||||
background-color: initial !important;
|
||||
color: initial !important;
|
||||
cursor: initial !important;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
a {
|
||||
color: initial !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pdfjs-page-number-input {
|
||||
color: @toolbar-btn-color;
|
||||
font-size: 14px;
|
||||
padding: 8px 8px 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
input {
|
||||
color: initial;
|
||||
border: 1px solid @neutral-60;
|
||||
width: 32px;
|
||||
height: 24px;
|
||||
border-radius: @border-radius-base;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.pdfjs-viewer-controls-small {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pdfjs-toolbar-popover-button {
|
||||
padding: 2px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pdfjs-toolbar-popover {
|
||||
background-color: @editor-toolbar-bg;
|
||||
border-radius: 4px;
|
||||
|
||||
.arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
color: @toolbar-btn-color;
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// The new viewer UI has overflow on the inner element,
|
||||
// so disable the overflow on the outer element
|
||||
.pdf-viewer .pdfjs-viewer.pdfjs-viewer-outer {
|
||||
|
|
|
@ -1181,6 +1181,7 @@
|
|||
"newsletter_info_unsubscribed": "You are currently <0>unsubscribed</0> to the __appName__ newsletter.",
|
||||
"newsletter_onboarding_accept": "I’d like emails about product offers and company news and events.",
|
||||
"next": "Next",
|
||||
"next_page": "Next page",
|
||||
"next_payment_of_x_collectected_on_y": "The next payment of <0>__paymentAmmount__</0> will be collected on <1>__collectionDate__</1>.",
|
||||
"nl": "Dutch",
|
||||
"no": "Norwegian",
|
||||
|
@ -1380,6 +1381,7 @@
|
|||
"premium_plan_label": "You’re using <b>Overleaf Premium</b>",
|
||||
"presentation": "Presentation",
|
||||
"press_and_awards": "Press & awards",
|
||||
"previous_page": "Previous page",
|
||||
"price": "Price",
|
||||
"primarily_work_study_question": "Where do you primarily work or study?",
|
||||
"primarily_work_study_question_company": "Company",
|
||||
|
@ -2136,6 +2138,7 @@
|
|||
"view_metrics_commons_subtext": "Monitor and download usage metrics for your Commons subscription",
|
||||
"view_metrics_group_subtext": "Monitor and download usage metrics for your group subscription",
|
||||
"view_more": "View more",
|
||||
"view_options": "View options",
|
||||
"view_pdf": "View PDF",
|
||||
"view_source": "View Source",
|
||||
"view_your_invoices": "View Your Invoices",
|
||||
|
|
Loading…
Reference in a new issue