mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-01 20:10:35 +01:00
pushing final tweaks around scolling and addition/deletion dropdown filtering
This commit is contained in:
parent
227a974a09
commit
9801724232
@ -1992,6 +1992,7 @@
|
||||
"2": "Document 2"
|
||||
},
|
||||
"submit": "Compare",
|
||||
"newLine": "new-line",
|
||||
"complex": {
|
||||
"message": "One or both of the provided documents are large files, accuracy of comparison may be reduced"
|
||||
},
|
||||
|
||||
@ -1321,6 +1321,7 @@
|
||||
"removed": "Removed from base",
|
||||
"added": "Added in comparison"
|
||||
},
|
||||
"newLine": "new-line",
|
||||
"summary": {
|
||||
"baseHeading": "Base document",
|
||||
"comparisonHeading": "Comparison document",
|
||||
|
||||
@ -229,7 +229,7 @@ const CompareDocumentPane = ({
|
||||
style={{ width: `${Math.round(page.width * fit)}px` }}
|
||||
>
|
||||
<div
|
||||
className="compare-diff-page__inner"
|
||||
className={`compare-diff-page__inner compare-diff-page__inner--${pane}`}
|
||||
style={{ transform: `translate(${-panX}px, ${-panY}px) scale(${zoom})`, transformOrigin: 'top left' }}
|
||||
>
|
||||
<img
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Combobox, ScrollArea, useCombobox } from '@mantine/core';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface NavigationDropdownProps {
|
||||
changes: Array<{ value: string; label: string; pageNumber?: number }>;
|
||||
@ -14,6 +15,8 @@ const CompareNavigationDropdown = ({
|
||||
className,
|
||||
onNavigate,
|
||||
}: NavigationDropdownProps) => {
|
||||
const { t } = useTranslation();
|
||||
const newLineLabel = t('compare.newLine', 'new-line');
|
||||
const combobox = useCombobox({
|
||||
onDropdownClose: () => combobox.resetSelectedOption(),
|
||||
});
|
||||
@ -50,13 +53,21 @@ const CompareNavigationDropdown = ({
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const normalizedChanges = useMemo(() => {
|
||||
// Helper to strip localized new-line marker occurrences from labels
|
||||
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const stripNewLine = (s: string) =>
|
||||
s
|
||||
.replace(new RegExp(`\\b${esc(newLineLabel)}\\b`, 'gi'), ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
const cleaned = changes
|
||||
.map((c) => ({ value: c.value, label: sanitize(c.label), pageNumber: c.pageNumber }))
|
||||
.filter((c) => isMeaningful(c.label));
|
||||
.map((c) => ({ value: c.value, label: stripNewLine(sanitize(c.label)), pageNumber: c.pageNumber }))
|
||||
.filter((c) => isMeaningful(c.label) && c.label.length > 0 && c.label.toLowerCase() !== newLineLabel.toLowerCase());
|
||||
const q = sanitize(query).toLowerCase();
|
||||
if (!q) return cleaned;
|
||||
return cleaned.filter((c) => c.label.toLowerCase().includes(q));
|
||||
}, [changes, query]);
|
||||
}, [changes, query, newLineLabel]);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
|
||||
@ -72,6 +72,39 @@ export const useCompareChangeNavigation = (
|
||||
|
||||
container.scrollTo({ top: desiredTop, left: desiredLeft, behavior: 'smooth' });
|
||||
|
||||
// Also scroll the peer container to the corresponding location in the
|
||||
// other PDF (same page and approximate vertical position within page),
|
||||
// not just the same list/scroll position.
|
||||
const peerRef = pane === 'base' ? comparisonScrollRef : baseScrollRef;
|
||||
const peer = peerRef.current;
|
||||
if (peer) {
|
||||
// Use the first node as the anchor
|
||||
const anchor = nodes[0];
|
||||
const pageEl = anchor.closest('.compare-diff-page') as HTMLElement | null;
|
||||
const pageNumAttr = pageEl?.getAttribute('data-page-number');
|
||||
const topPercent = parseFloat((anchor as HTMLElement).style.top || '0');
|
||||
if (pageNumAttr) {
|
||||
const peerPageEl = peer.querySelector(
|
||||
`.compare-diff-page[data-page-number="${pageNumAttr}"]`
|
||||
) as HTMLElement | null;
|
||||
const peerInner = peerPageEl?.querySelector('.compare-diff-page__inner') as HTMLElement | null;
|
||||
if (peerPageEl && peerInner) {
|
||||
const innerRect = peerInner.getBoundingClientRect();
|
||||
const innerHeight = Math.max(1, innerRect.height);
|
||||
const absoluteTopInPage = (topPercent / 100) * innerHeight;
|
||||
const peerDesiredTop = Math.max(
|
||||
0,
|
||||
peerPageEl.offsetTop + absoluteTopInPage - peer.clientHeight / 2
|
||||
);
|
||||
peer.scrollTo({ top: peerDesiredTop, behavior: 'smooth' });
|
||||
} else if (peerPageEl) {
|
||||
// Fallback: Scroll to page top
|
||||
const top = Math.max(0, peerPageEl.offsetTop - Math.round(peer.clientHeight * 0.2));
|
||||
peer.scrollTo({ top, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const groupsByInner = new Map<HTMLElement, HTMLElement[]>();
|
||||
nodes.forEach((element) => {
|
||||
const inner = element.closest('.compare-diff-page__inner') as HTMLElement | null;
|
||||
|
||||
@ -7,6 +7,7 @@ import type {
|
||||
import type { CompareChangeOption } from '../../../../types/compareWorkbench';
|
||||
import type { PagePreview } from '../../../../hooks/useProgressivePagePreviews';
|
||||
import type { WordHighlightEntry } from '../types';
|
||||
import { PARAGRAPH_SENTINEL } from '../../../../types/compare';
|
||||
|
||||
interface TokenGroupMap {
|
||||
base: Map<number, string>;
|
||||
@ -42,22 +43,32 @@ const buildWordChanges = (
|
||||
for (let i = 0; i < tokens.length; i += 1) {
|
||||
const token = tokens[i];
|
||||
if (token.type === targetType) {
|
||||
const startIndex = metadataIndex;
|
||||
const parts: string[] = [];
|
||||
const runIndices: number[] = [];
|
||||
const pageNumber = metadata[metadataIndex]?.page ?? 1;
|
||||
while (i < tokens.length && tokens[i].type === targetType) {
|
||||
parts.push(tokens[i].text);
|
||||
runIndices.push(metadataIndex);
|
||||
const t = tokens[i].text;
|
||||
const isPara = t === PARAGRAPH_SENTINEL || t.startsWith('\uE000') || t.includes('PARA');
|
||||
// Skip paragraph sentinel tokens entirely from labels and grouping
|
||||
if (!isPara) {
|
||||
parts.push(t);
|
||||
// Only add to grouping if there is a corresponding metadata index
|
||||
if (metadata[metadataIndex]) {
|
||||
runIndices.push(metadataIndex);
|
||||
}
|
||||
}
|
||||
metadataIndex += 1;
|
||||
i += 1;
|
||||
}
|
||||
i -= 1;
|
||||
const endIndex = metadataIndex - 1;
|
||||
const groupId = `${groupPrefix}-${startIndex}-${endIndex}`;
|
||||
runIndices.forEach((idx) => tokenIndexToGroupId.set(idx, groupId));
|
||||
const label = parts.join(' ').trim();
|
||||
items.push({ value: groupId, label: label || '(…)', pageNumber });
|
||||
if (label.length > 0 && runIndices.length > 0) {
|
||||
const startIndexForId = runIndices[0];
|
||||
const endIndexForId = runIndices[runIndices.length - 1];
|
||||
const groupId = `${groupPrefix}-${startIndexForId}-${endIndexForId}`;
|
||||
runIndices.forEach((idx) => tokenIndexToGroupId.set(idx, groupId));
|
||||
items.push({ value: groupId, label, pageNumber });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (token.type !== (targetType === 'added' ? 'removed' : 'added')) {
|
||||
|
||||
@ -29,6 +29,13 @@ interface ScrollLinkDelta {
|
||||
horizontal: number;
|
||||
}
|
||||
|
||||
// Pixel-based anchors captured when linking scroll, to preserve the
|
||||
// visual offset between panes and avoid an initial snap.
|
||||
interface ScrollLinkAnchors {
|
||||
deltaPixelsBaseToComp: number;
|
||||
deltaPixelsCompToBase: number;
|
||||
}
|
||||
|
||||
interface PanDragState {
|
||||
active: boolean;
|
||||
source: Pane | null;
|
||||
@ -93,6 +100,10 @@ export const useComparePanZoom = ({
|
||||
const comparisonScrollRef = useRef<HTMLDivElement>(null);
|
||||
const isSyncingRef = useRef(false);
|
||||
const scrollLinkDeltaRef = useRef<ScrollLinkDelta>({ vertical: 0, horizontal: 0 });
|
||||
const scrollLinkAnchorsRef = useRef<ScrollLinkAnchors>({
|
||||
deltaPixelsBaseToComp: 0,
|
||||
deltaPixelsCompToBase: 0,
|
||||
});
|
||||
const [isScrollLinked, setIsScrollLinked] = useState(true);
|
||||
const [isPanMode, setIsPanMode] = useState(false);
|
||||
const panDragRef = useRef<PanDragState>({
|
||||
@ -328,12 +339,16 @@ export const useComparePanZoom = ({
|
||||
lastActivePaneRef.current = source === baseScrollRef.current ? 'base' : 'comparison';
|
||||
|
||||
const sourceIsBase = source === baseScrollRef.current;
|
||||
const deltaV = scrollLinkDeltaRef.current.vertical;
|
||||
|
||||
const targetVerticalRange = Math.max(1, target.scrollHeight - target.clientHeight);
|
||||
const mappedTop = mapScrollTopBetweenPanes(source.scrollTop, sourceIsBase);
|
||||
const offset = deltaV * targetVerticalRange;
|
||||
const desiredTop = Math.max(0, Math.min(targetVerticalRange, mappedTop + (sourceIsBase ? offset : -offset)));
|
||||
|
||||
// Use pixel anchors captured at link time to preserve offset
|
||||
const deltaPx = sourceIsBase
|
||||
? scrollLinkAnchorsRef.current.deltaPixelsBaseToComp
|
||||
: scrollLinkAnchorsRef.current.deltaPixelsCompToBase;
|
||||
|
||||
const desiredTop = Math.max(0, Math.min(targetVerticalRange, mappedTop + deltaPx));
|
||||
|
||||
isSyncingRef.current = true;
|
||||
target.scrollTop = desiredTop;
|
||||
@ -623,6 +638,7 @@ export const useComparePanZoom = ({
|
||||
const compEl = comparisonScrollRef.current;
|
||||
if (!baseEl || !compEl) {
|
||||
scrollLinkDeltaRef.current = { vertical: 0, horizontal: 0 };
|
||||
scrollLinkAnchorsRef.current = { deltaPixelsBaseToComp: 0, deltaPixelsCompToBase: 0 };
|
||||
return;
|
||||
}
|
||||
const baseVMax = Math.max(1, baseEl.scrollHeight - baseEl.clientHeight);
|
||||
@ -639,10 +655,19 @@ export const useComparePanZoom = ({
|
||||
vertical: compV - baseV,
|
||||
horizontal: compH - baseH,
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Capture pixel anchors in mapped space
|
||||
const mappedBaseToComp = mapScrollTopBetweenPanes(baseEl.scrollTop, true);
|
||||
const mappedCompToBase = mapScrollTopBetweenPanes(compEl.scrollTop, false);
|
||||
scrollLinkAnchorsRef.current = {
|
||||
deltaPixelsBaseToComp: compEl.scrollTop - mappedBaseToComp,
|
||||
deltaPixelsCompToBase: baseEl.scrollTop - mappedCompToBase,
|
||||
};
|
||||
}, [mapScrollTopBetweenPanes]);
|
||||
|
||||
const clearScrollLinkDelta = useCallback(() => {
|
||||
scrollLinkDeltaRef.current = { vertical: 0, horizontal: 0 };
|
||||
scrollLinkAnchorsRef.current = { deltaPixelsBaseToComp: 0, deltaPixelsCompToBase: 0 };
|
||||
}, []);
|
||||
|
||||
const zoomLimits = useMemo(() => ({ min: ZOOM_MIN, max: ZOOM_MAX, step: ZOOM_STEP }), []);
|
||||
|
||||
@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CompareRoundedIcon from '@mui/icons-material/CompareRounded';
|
||||
import { Box, Group, Stack, Text, Button } from '@mantine/core';
|
||||
import { Tooltip } from '../components/shared/Tooltip';
|
||||
import { createToolFlow } from '../components/tools/shared/createToolFlow';
|
||||
import { useBaseTool } from '../hooks/tools/shared/useBaseTool';
|
||||
import { BaseToolProps, ToolComponent } from '../types/tool';
|
||||
@ -22,6 +23,7 @@ import type { StirlingFile } from '../types/fileContext';
|
||||
import DocumentThumbnail from '../components/shared/filePreview/DocumentThumbnail';
|
||||
import './compareTool.css';
|
||||
import type { CompareWorkbenchData } from '../types/compareWorkbench';
|
||||
import FitText from '../components/shared/FitText';
|
||||
|
||||
const CUSTOM_VIEW_ID = 'compareWorkbenchView';
|
||||
const CUSTOM_WORKBENCH_ID = 'custom:compareWorkbenchView' as const;
|
||||
@ -234,9 +236,14 @@ const Compare = (props: BaseToolProps) => {
|
||||
<DocumentThumbnail file={stub ?? null} thumbnail={stub?.thumbnailUrl || null} />
|
||||
</Box>
|
||||
<Stack gap={4} className="compare-tool__details">
|
||||
<Text fw={600} truncate>
|
||||
{stub?.name}
|
||||
</Text>
|
||||
<Tooltip content={stub?.name || ''} position="top" arrow>
|
||||
<FitText
|
||||
text={stub?.name || ''}
|
||||
minimumFontScale={0.5}
|
||||
lines={2}
|
||||
style={{ fontWeight: 600 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
{meta && (
|
||||
<Text size="sm" c="dimmed">
|
||||
{meta}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user