-
Notifications
You must be signed in to change notification settings - Fork 397
✨(frontend) add pdf block to the editor #1293
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
base: main
Are you sure you want to change the base?
Changes from all commits
2d10452
558fbd8
3883912
77fadc1
9bd2175
e66aa84
7db7e6f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems to have some drag and drop issue with the embed tag, not sure why. -.Docs.1.webm |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,158 @@ | ||||||||
/* eslint-disable react-hooks/rules-of-hooks */ | ||||||||
import { insertOrUpdateBlock } from '@blocknote/core'; | ||||||||
import { | ||||||||
AddFileButton, | ||||||||
FileBlockWrapper, | ||||||||
createReactBlockSpec, | ||||||||
} from '@blocknote/react'; | ||||||||
import { TFunction } from 'i18next'; | ||||||||
import { useCallback } from 'react'; | ||||||||
import { useTranslation } from 'react-i18next'; | ||||||||
|
||||||||
import { Box, Icon } from '@/components'; | ||||||||
import { useResponsiveStore } from '@/stores'; | ||||||||
|
||||||||
import { usePdfResizer } from '../../hook/usePdfResizer'; | ||||||||
import { DocsBlockNoteEditor } from '../../types'; | ||||||||
|
||||||||
type FileBlockEditor = Parameters<typeof AddFileButton>[0]['editor']; | ||||||||
|
||||||||
export const PdfBlock = createReactBlockSpec( | ||||||||
{ | ||||||||
type: 'pdf', | ||||||||
content: 'none', | ||||||||
propSchema: { | ||||||||
name: { default: '' as const }, | ||||||||
url: { default: '' as const }, | ||||||||
caption: { default: '' as const }, | ||||||||
showPreview: { default: true }, | ||||||||
previewWidth: { default: undefined, type: 'number' }, | ||||||||
}, | ||||||||
isFileBlock: true, | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To target only pdf files:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||||||||
fileBlockAccept: ['application/pdf'], | ||||||||
}, | ||||||||
{ | ||||||||
render: ({ editor, block, contentRef }) => { | ||||||||
const pdfUrl = block.props.url; | ||||||||
|
||||||||
const { t } = useTranslation(); | ||||||||
|
||||||||
const { isMobile } = useResponsiveStore(); | ||||||||
|
||||||||
const handleResizeEnd = useCallback( | ||||||||
(width: number) => { | ||||||||
editor.updateBlock(block, { | ||||||||
props: { previewWidth: width }, | ||||||||
}); | ||||||||
}, | ||||||||
[editor, block], | ||||||||
); | ||||||||
|
||||||||
const { wrapperRef, pdfWidth, handlePointerDown } = usePdfResizer( | ||||||||
block.props.previewWidth ?? 100, | ||||||||
handleResizeEnd, | ||||||||
); | ||||||||
|
||||||||
return ( | ||||||||
<div ref={contentRef} className="bn-file-block-content-wrapper"> | ||||||||
{pdfUrl === '' ? ( | ||||||||
<AddFileButton | ||||||||
block={block} | ||||||||
editor={editor as unknown as FileBlockEditor} | ||||||||
buttonText={t('Add PDF')} | ||||||||
buttonIcon={<Icon iconName="upload" />} | ||||||||
/> | ||||||||
) : ( | ||||||||
<FileBlockWrapper | ||||||||
block={block} | ||||||||
editor={editor as unknown as FileBlockEditor} | ||||||||
> | ||||||||
{isMobile ? ( | ||||||||
<Box | ||||||||
$display="flex" | ||||||||
$align="center" | ||||||||
$justify="center" | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||
$direction="row" | ||||||||
$gap="1rem" | ||||||||
> | ||||||||
<Icon iconName="picture_as_pdf" $size="18px" /> | ||||||||
<Box | ||||||||
as="p" | ||||||||
$display="inline-block" | ||||||||
style={{ | ||||||||
textOverflow: 'ellipsis', | ||||||||
whiteSpace: 'nowrap', | ||||||||
overflow: 'hidden', | ||||||||
display: 'inline', | ||||||||
}} | ||||||||
> | ||||||||
{block.props.name} | ||||||||
</Box> | ||||||||
</Box> | ||||||||
Comment on lines
+70
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose you added this part because sometime There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I added this because on iOS Safari the But please let me know if you’d like me to take another look at this. |
||||||||
) : ( | ||||||||
<Box ref={wrapperRef} $width="100%" $position="relative"> | ||||||||
<Box | ||||||||
$width={`${pdfWidth}%`} | ||||||||
$height="500px" | ||||||||
$position="relative" | ||||||||
$css={` | ||||||||
border: 1px solid #ccc; | ||||||||
margin: auto; | ||||||||
`} | ||||||||
> | ||||||||
<embed | ||||||||
src={pdfUrl} | ||||||||
type="application/pdf" | ||||||||
width="100%" | ||||||||
height="100%" | ||||||||
contentEditable={false} | ||||||||
draggable={false} | ||||||||
onClick={() => editor.setTextCursorPosition(block)} | ||||||||
/> | ||||||||
{/* Right-edge drag handle */} | ||||||||
<div | ||||||||
role="separator" | ||||||||
aria-orientation="vertical" | ||||||||
style={{ | ||||||||
position: 'absolute', | ||||||||
top: '50%', | ||||||||
right: -4, | ||||||||
width: 4, | ||||||||
height: '7.5rem', | ||||||||
borderRadius: '2px', | ||||||||
background: 'gray', | ||||||||
cursor: 'ew-resize', | ||||||||
zIndex: 1, | ||||||||
touchAction: 'none', | ||||||||
transform: 'translateY(-50%)', | ||||||||
}} | ||||||||
onPointerDown={handlePointerDown} | ||||||||
onDragStart={(e) => e.preventDefault()} | ||||||||
/> | ||||||||
</Box> | ||||||||
</Box> | ||||||||
)} | ||||||||
</FileBlockWrapper> | ||||||||
)} | ||||||||
</div> | ||||||||
); | ||||||||
}, | ||||||||
}, | ||||||||
); | ||||||||
|
||||||||
export const getPdfReactSlashMenuItems = ( | ||||||||
editor: DocsBlockNoteEditor, | ||||||||
t: TFunction<'translation', undefined>, | ||||||||
group: string, | ||||||||
) => [ | ||||||||
{ | ||||||||
title: t('PDF'), | ||||||||
onItemClick: () => { | ||||||||
insertOrUpdateBlock(editor, { type: 'pdf' }); | ||||||||
}, | ||||||||
aliases: ['pdf', 'document', 'embed', 'file'], | ||||||||
group, | ||||||||
icon: <Icon iconName="picture_as_pdf" $size="18px" />, | ||||||||
subtext: t('Embed a PDF file'), | ||||||||
}, | ||||||||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './AccessibleImageBlock'; | ||
export * from './CalloutBlock'; | ||
export * from './DividerBlock'; | ||
export * from './PdfBlock'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { useCallback, useEffect, useRef, useState } from 'react'; | ||
|
||
const MAX_WIDTH_PERCENTAGE = 100; | ||
const MIN_WIDTH_PERCENTAGE = 40; | ||
|
||
export const usePdfResizer = ( | ||
initialWidth: number = 100, | ||
onResizeEnd: (width: number) => void, | ||
) => { | ||
const dragOffsetRef = useRef(0); | ||
const wrapperRef = useRef<HTMLDivElement>(null); | ||
|
||
const [pdfWidth, setPdfWidth] = useState(initialWidth); | ||
const [dragging, setDragging] = useState(false); | ||
|
||
const clamp = (v: number) => | ||
Math.max(MIN_WIDTH_PERCENTAGE, Math.min(MAX_WIDTH_PERCENTAGE, v)); | ||
|
||
const onPointerMove = useCallback( | ||
(e: PointerEvent) => { | ||
if (!dragging || !wrapperRef.current) { | ||
return; | ||
} | ||
|
||
const rect = wrapperRef.current.getBoundingClientRect(); | ||
|
||
const pct = | ||
((e.clientX - rect.left - dragOffsetRef.current) / rect.width) * 100; | ||
|
||
setPdfWidth(clamp(pct)); | ||
}, | ||
[dragging], | ||
); | ||
|
||
const stopDragging = useCallback(() => { | ||
onResizeEnd(pdfWidth); | ||
setDragging(false); | ||
}, [onResizeEnd, pdfWidth]); | ||
|
||
useEffect(() => { | ||
if (!dragging) { | ||
return; | ||
} | ||
|
||
const prevUserSelect = document.body.style.userSelect; | ||
document.body.style.userSelect = 'none'; | ||
|
||
window.addEventListener('pointermove', onPointerMove); | ||
window.addEventListener('pointerup', stopDragging); | ||
window.addEventListener('pointercancel', stopDragging); | ||
|
||
return () => { | ||
document.body.style.userSelect = prevUserSelect; | ||
window.removeEventListener('pointermove', onPointerMove); | ||
window.removeEventListener('pointerup', stopDragging); | ||
window.removeEventListener('pointercancel', stopDragging); | ||
}; | ||
}, [dragging, onPointerMove, stopDragging]); | ||
|
||
const handlePointerDown = useCallback( | ||
(e: React.PointerEvent) => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
|
||
if (wrapperRef.current) { | ||
const rect = wrapperRef.current.getBoundingClientRect(); | ||
const currentRight = rect.left + (pdfWidth / 100) * rect.width; | ||
|
||
// capture grab offset | ||
dragOffsetRef.current = e.clientX - currentRight; | ||
} | ||
|
||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); | ||
setDragging(true); | ||
}, | ||
[pdfWidth], | ||
); | ||
|
||
return { | ||
pdfWidth, | ||
wrapperRef, | ||
dragging, | ||
handlePointerDown, | ||
} as const; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { Paragraph, TextRun } from 'docx'; | ||
|
||
import { DocsExporterDocx } from '../types'; | ||
|
||
export const blockMappingPdfDocx: DocsExporterDocx['mappings']['blockMapping']['pdf'] = | ||
(block) => { | ||
const pdfName = block.props.name || 'PDF Document'; | ||
const pdfUrl = block.props.url; | ||
|
||
const children: TextRun[] = [ | ||
new TextRun({ | ||
text: '📄 ', | ||
size: 20, | ||
}), | ||
new TextRun({ | ||
text: pdfName, | ||
bold: true, | ||
size: 24, | ||
}), | ||
]; | ||
|
||
if (pdfUrl) { | ||
children.push( | ||
new TextRun({ | ||
text: '\nSource: ', | ||
size: 20, | ||
color: '666666', | ||
}), | ||
new TextRun({ | ||
text: pdfUrl, | ||
size: 20, | ||
color: '666666', | ||
}), | ||
); | ||
} | ||
|
||
children.push( | ||
new TextRun({ | ||
text: '\n[PDF content cannot be embedded in exported DOCX]', | ||
size: 18, | ||
color: '999999', | ||
italics: true, | ||
}), | ||
); | ||
|
||
return new Paragraph({ | ||
children, | ||
spacing: { | ||
before: 200, | ||
after: 200, | ||
}, | ||
border: { | ||
top: { size: 1, color: 'CCCCCC', style: 'single' }, | ||
bottom: { size: 1, color: 'CCCCCC', style: 'single' }, | ||
left: { size: 1, color: 'CCCCCC', style: 'single' }, | ||
right: { size: 1, color: 'CCCCCC', style: 'single' }, | ||
}, | ||
shading: { | ||
fill: 'F9F9F9', | ||
}, | ||
}); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure to understand why it is necessary, seems to work fine with the
embed
tag without changing to "inline" ?Is there a browser where you get a problem maybe ?