Skip to content

Commit

Permalink
fix: Text Selection
Browse files Browse the repository at this point in the history
  • Loading branch information
gpalsingh committed Nov 26, 2024
1 parent 8cca4f8 commit a37d316
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 9 deletions.
4 changes: 2 additions & 2 deletions packages/react-pdf/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@commutatus/react-pdf",
"version": "8.0.67",
"version": "8.0.68",
"description": "Display PDFs in your React app as easily as if they were images.",
"type": "module",
"sideEffects": [
Expand Down Expand Up @@ -67,7 +67,7 @@
},
"license": "MIT",
"dependencies": {
"@commutatus/pdfjs-dist": "5.0.52",
"@commutatus/pdfjs-dist": "5.0.53",
"clsx": "^2.0.0",
"dequal": "^2.0.3",
"lodash.debounce": "^4.0.8",
Expand Down
108 changes: 108 additions & 0 deletions packages/react-pdf/src/Document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,8 @@ const Document = forwardRef(function Document(
// TODO: Fix this workaround and use a single scale value
const [scaleState, setScaleState] = useState({ scale: 1, pdfjsInternalScale: 1 });
const firstPage = useRef<PDFPageProxy | null>(null);
const textLayersForSelection = useRef(new Map());
const selectionChangeAbortController = useRef<AbortController | null>(null);

useEffect(
function initializeEventsRef() {
Expand Down Expand Up @@ -1051,6 +1053,110 @@ const Document = forwardRef(function Document(
[pdf, isSaveInProgress, eventsRefProp],
);

function unregisterDivForGlobalSelectionListener(textLayerDiv: HTMLDivElement) {
textLayersForSelection.current.delete(textLayerDiv);

if (textLayersForSelection.current.size === 0) {
selectionChangeAbortController.current?.abort();
selectionChangeAbortController.current = null;
}
}

function registerDivForGlobalSelectionListener(div: HTMLDivElement, end: HTMLDivElement) {
textLayersForSelection.current.set(div, end);

if (selectionChangeAbortController.current) {
// document-level event listeners already installed
return;
}
selectionChangeAbortController.current = new AbortController();

const reset = (end: HTMLDivElement, textLayer: HTMLDivElement) => {
end.classList.remove('active');
};

document.addEventListener(
'pointerup',
() => {
textLayersForSelection.current.forEach(reset);
},
{ signal: selectionChangeAbortController.current.signal },
);

let isFirefox, prevRange: Range;

document.addEventListener(
'selectionchange',
() => {
const selection = document.getSelection();
if (!selection || selection.rangeCount === 0) {
textLayersForSelection.current.forEach(reset);
return;
}

// Even though the spec says that .rangeCount should be 0 or 1, Firefox
// creates multiple ranges when selecting across multiple pages.
// Make sure to collect all the .textLayer elements where the selection
// is happening.
const activeTextLayers = new Set();
for (let i = 0; i < selection.rangeCount; i++) {
const range = selection.getRangeAt(i);
for (const textLayerDiv of textLayersForSelection.current.keys()) {
if (!activeTextLayers.has(textLayerDiv) && range.intersectsNode(textLayerDiv)) {
activeTextLayers.add(textLayerDiv);
}
}
}

for (const [textLayerDiv, endDiv] of textLayersForSelection.current) {
if (activeTextLayers.has(textLayerDiv)) {
endDiv.classList.add('active');
} else {
reset(endDiv, textLayerDiv);
}
}

isFirefox ??=
getComputedStyle(textLayersForSelection.current.values().next().value).getPropertyValue(
'-moz-user-select',
) === 'none';

if (!isFirefox) {
// In non-Firefox browsers, when hovering over an empty space (thus,
// on .endOfContent), the selection will expand to cover all the
// text between the current selection and .endOfContent. By moving
// .endOfContent to right after (or before, depending on which side
// of the selection the user is moving), we limit the selection jump
// to at most cover the entirety of the <span> where the selection
// is being modified.
const range = selection.getRangeAt(0);
const modifyStart =
prevRange &&
(range.compareBoundaryPoints(Range.END_TO_END, prevRange) === 0 ||
range.compareBoundaryPoints(Range.START_TO_END, prevRange) === 0);
let anchor = modifyStart ? range.startContainer : range.endContainer;
if (anchor.parentNode && anchor.nodeType === Node.TEXT_NODE) {
anchor = anchor.parentNode;
}

if (anchor.parentElement) {
const parentTextLayer: HTMLDivElement | null =
anchor.parentElement.closest('.textLayer');
const endDiv = textLayersForSelection.current.get(parentTextLayer);
if (endDiv && parentTextLayer) {
endDiv.style.width = parentTextLayer.style.width;
endDiv.style.height = parentTextLayer.style.height;
anchor.parentElement.insertBefore(endDiv, modifyStart ? anchor : anchor.nextSibling);
}
}

prevRange = range.cloneRange();
}
},
{ signal: selectionChangeAbortController.current.signal },
);
}

/**
* Called when a document is read successfully
*/
Expand Down Expand Up @@ -1194,6 +1300,8 @@ const Document = forwardRef(function Document(
registerPage,
renderMode,
rotate,
registerDivForGlobalSelectionListener,
unregisterDivForGlobalSelectionListener,
...scaleState,
}),
[
Expand Down
11 changes: 8 additions & 3 deletions packages/react-pdf/src/Page/TextLayer.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,17 @@
position: absolute;
text-align: initial;
inset: 0;
overflow: hidden;
overflow: clip;
line-height: 1;
text-size-adjust: none;
forced-color-adjust: none;
transform-origin: 0 0;
z-index: 2;
z-index: 0;

> :not(.markedContent),
.markedContent span:not(.markedContent) {
z-index: 1;
}
}

.textLayer :is(span, br) {
Expand Down Expand Up @@ -104,7 +109,7 @@
display: block;
position: absolute;
inset: 100% 0 0;
z-index: -1;
z-index: 0;
cursor: default;
user-select: none;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react-pdf/src/Page/TextLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export default function TextLayer() {
rotate,
scale,
textLayerRef: layerElement,
registerDivForGlobalSelectionListener,
unregisterDivForGlobalSelectionListener,
} = mergedProps;

invariant(page, 'Attempted to load page text content, but no page was specified.');
Expand Down Expand Up @@ -172,6 +174,9 @@ export default function TextLayer() {
if (onRenderTextLayerError) {
onRenderTextLayerError(error);
}
if (layerElement) {
unregisterDivForGlobalSelectionListener?.((layerElement as any).current);
}
},
[onRenderTextLayerError],
);
Expand Down Expand Up @@ -235,6 +240,7 @@ export default function TextLayer() {
const end = document.createElement('div');
end.className = 'endOfContent';
layer.append(end);
registerDivForGlobalSelectionListener?.(layer, end);
endElement.current = end;

const layerChildren = layer.querySelectorAll('[role="presentation"]');
Expand Down
2 changes: 2 additions & 0 deletions packages/react-pdf/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export type DocumentContextType = {
registerAnnotationEditorLayer: RegisterAnnotationEditorLayer;
renderMode?: RenderMode;
rotate?: number | null;
registerDivForGlobalSelectionListener: (div: HTMLDivElement, end: HTMLDivElement) => void;
unregisterDivForGlobalSelectionListener: (div: HTMLDivElement) => void;
} | null;

export type PageContextType = {
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,10 @@
"@babel/helper-validator-identifier" "^7.24.6"
to-fast-properties "^2.0.0"

"@commutatus/pdfjs-dist@5.0.52":
version "5.0.52"
resolved "https://registry.yarnpkg.com/@commutatus/pdfjs-dist/-/pdfjs-dist-5.0.52.tgz#21ea0dcc671cba9fd5086e58e9f8e47c5f6cbfa6"
integrity sha512-AHjUIEr+9BIk8faDuIlLOAo359OXTznDktwewlXkoKUCUlYLU/ppV9Kqh1PsGj8CgeGTBRmeeenxyalFzozhGg==
"@commutatus/pdfjs-dist@5.0.53":
version "5.0.53"
resolved "https://registry.yarnpkg.com/@commutatus/pdfjs-dist/-/pdfjs-dist-5.0.53.tgz#7c9561e98fca480b4768885c725aaf5eb6877138"
integrity sha512-MmONfny2G60MxXxwQbvLXoRWcRhXHFW8HyYxWLtSPz80znqdJuDph+gRsduVgwQGrJn/Jj8SSysde7YC59mcRA==
optionalDependencies:
canvas "^2.11.2"
path2d-polyfill "^2.0.1"
Expand Down

0 comments on commit a37d316

Please sign in to comment.