Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "virtual-react-json-diff",
"type": "module",
"version": "1.0.13",
"version": "1.0.14",
"description": "Fast, virtualized React component for visually comparing large JSON objects. Includes search, theming, and minimap.",
"author": {
"name": "Utku Akyüz"
Expand Down
13 changes: 7 additions & 6 deletions src/components/DiffViewer/components/VirtualDiffGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// VirtualDiffGrid.tsx
import type { InlineDiffOptions } from "json-diff-kit";
import type { Dispatch } from "react";
import type { ListOnScrollProps } from "react-window";
Expand All @@ -22,27 +21,29 @@ type ListDataType = {
type VirtualDiffGridProps = {
leftDiff: DiffRowOrCollapsed[];
rightDiff: DiffRowOrCollapsed[];
outerRef: React.RefObject<Node | null>;
listRef: React.RefObject<List<ListDataType>>;
height: number;
inlineDiffOptions?: InlineDiffOptions;
className?: string;
setScrollTop: Dispatch<React.SetStateAction<number>>;
onExpand: (segmentIndex: number) => void;
overScanCount?: number;
viewerRef?: React.RefObject<HTMLDivElement>;
listContainerRef?: React.RefObject<HTMLDivElement>;
};

const VirtualDiffGrid: React.FC<VirtualDiffGridProps> = ({
leftDiff,
rightDiff,
outerRef,
listRef,
height,
inlineDiffOptions,
className,
setScrollTop,
onExpand,
overScanCount = 10,
viewerRef,
listContainerRef,
}) => {
// Virtual List Data
const listData = useMemo(
Expand All @@ -65,7 +66,7 @@ const VirtualDiffGrid: React.FC<VirtualDiffGridProps> = ({

// ROW HEIGHT CALCULATION
const ROW_HEIGHT = useMemo(() => getRowHeightFromCSS(), []);
const rowHeights = useRowHeights(leftDiff);
const rowHeights = useRowHeights(leftDiff, viewerRef);
const dynamicRowHeights = useCallback(
(index: number) => {
const leftLine = leftDiff[index];
Expand All @@ -81,12 +82,12 @@ const VirtualDiffGrid: React.FC<VirtualDiffGridProps> = ({
}, [rowHeights]);

return (
<div className={classes}>
<div className={classes} ref={viewerRef}>
<List
height={height}
width="100%"
style={{ alignItems: "start" }}
outerRef={outerRef}
outerRef={listContainerRef}
ref={listRef}
className="virtual-json-diff-list-container"
itemCount={Math.max(leftDiff.length, rightDiff.length)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ export const VirtualizedDiffViewer: React.FC<VirtualizedDiffViewerProps> = ({
inlineDiffOptions,
overScanCount,
}) => {
const outerRef = useRef<Node>(null);
const listRef = useRef<List>(null);
const getDiffDataRef = useRef<typeof getDiffData>();
const lastSent = useRef<number>();
const viewerRef = useRef<HTMLDivElement>(null);
const listContainerRef = useRef<HTMLDivElement>(null);

const differ = customDiffer ?? useMemo(
() =>
Expand Down Expand Up @@ -75,6 +76,8 @@ export const VirtualizedDiffViewer: React.FC<VirtualizedDiffViewerProps> = ({
listRef.current?.scrollToItem(idx, "center");
onSearchMatch?.(idx);
},
viewerRef,
listContainerRef,
);

const handleExpand = useCallback(
Expand Down Expand Up @@ -140,7 +143,6 @@ export const VirtualizedDiffViewer: React.FC<VirtualizedDiffViewerProps> = ({
{/* List & Minimap */}
<div style={{ display: "flex", gap: "8px", position: "relative" }}>
<VirtualDiffGrid
outerRef={outerRef}
listRef={listRef}
leftDiff={leftView}
rightDiff={rightView}
Expand All @@ -150,6 +152,8 @@ export const VirtualizedDiffViewer: React.FC<VirtualizedDiffViewerProps> = ({
onExpand={handleExpand}
className="virtual-json-diff-list-container"
inlineDiffOptions={inlineDiffOptions}
viewerRef={viewerRef}
listContainerRef={listContainerRef}
/>

<div className="minimap-overlay">
Expand Down
11 changes: 7 additions & 4 deletions src/components/DiffViewer/hooks/useRowHeights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,26 @@ function getWrapCount(el: Element) {
return Math.round(el.scrollHeight / lh);
}

export function useRowHeights(leftView: DiffRowOrCollapsed[]) {
export function useRowHeights(leftView: DiffRowOrCollapsed[], viewerRef?: React.RefObject<HTMLDivElement | null>) {
const [rowHeights, setRowHeights] = useState<number[]>([]);

const measureRows = useCallback(() => {
const preElements = document.querySelectorAll(".json-diff-viewer pre");
if (!viewerRef?.current)
return;

const preElements = viewerRef.current.querySelectorAll("pre");
const newHeights: number[] = [];
for (let i = 0; i < preElements.length; i += 2) {
const leftWraps = getWrapCount(preElements[i]);
const rightWraps = getWrapCount(preElements[i + 1]);
newHeights.push(Math.max(leftWraps, rightWraps));
}
setRowHeights(newHeights);
}, []);
}, [viewerRef]);

useLayoutEffect(() => {
measureRows();
}, [leftView]);
}, [leftView, measureRows]);

useLayoutEffect(() => {
window.addEventListener("resize", measureRows);
Expand Down
27 changes: 17 additions & 10 deletions src/components/DiffViewer/hooks/useSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import { useCallback, useEffect, useRef, useState } from "react";

import type { DiffRowOrCollapsed, SearchState } from "../types";

import { DIFF_VIEWER_CLASS, SEARCH_DEBOUNCE_MS } from "../utils/constants";
import { SEARCH_DEBOUNCE_MS } from "../utils/constants";
import { highlightMatches, performSearch } from "../utils/diffSearchUtils";

export function useSearch(leftView: DiffRowOrCollapsed[], initialTerm?: string, onSearchMatch?: (index: number) => void) {
export function useSearch(
leftView: DiffRowOrCollapsed[],
initialTerm?: string,
onSearchMatch?: (index: number) => void,
viewerRef?: React.RefObject<HTMLDivElement | null>,
listContainerRef?: React.RefObject<HTMLDivElement | null>,
) {
const [searchState, setSearchState] = useState<SearchState>({
term: initialTerm ?? "",
results: [],
Expand Down Expand Up @@ -43,17 +49,18 @@ export function useSearch(leftView: DiffRowOrCollapsed[], initialTerm?: string,
}, [searchState, onSearchMatch]);

useEffect(() => {
highlightMatches(searchState.term, DIFF_VIEWER_CLASS);
if (!viewerRef?.current)
return;

highlightMatches(searchState.term, viewerRef.current);

const observer = new MutationObserver(() => highlightMatches(searchState.term, DIFF_VIEWER_CLASS));
const observer = new MutationObserver(() => highlightMatches(searchState.term, viewerRef.current!));
const config = { childList: true, subtree: true };
const viewer = document.querySelector(`.${DIFF_VIEWER_CLASS}`);
if (viewer)
observer.observe(viewer, config);
observer.observe(viewerRef.current, config);

const listContainer = document.querySelector(".virtual-json-diff-list-container");
const listContainer = listContainerRef?.current;
if (listContainer) {
const handleScroll = () => setTimeout(() => highlightMatches(searchState.term, DIFF_VIEWER_CLASS), 100);
const handleScroll = () => setTimeout(() => highlightMatches(searchState.term, viewerRef.current!), 100);
listContainer.addEventListener("scroll", handleScroll);
return () => {
observer.disconnect();
Expand All @@ -62,7 +69,7 @@ export function useSearch(leftView: DiffRowOrCollapsed[], initialTerm?: string,
}

return () => observer.disconnect();
}, [searchState.term]);
}, [searchState.term, viewerRef, listContainerRef]);

useEffect(() => {
if (initialTerm !== undefined) {
Expand Down
6 changes: 3 additions & 3 deletions src/components/DiffViewer/utils/diffSearchUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ export function performSearch(term: string, leftView: DiffRowOrCollapsed[]): num
return results;
}

export function highlightMatches(term: string, className: string = "json-diff-viewer-theme-custom"): void {
export function highlightMatches(term: string, container: HTMLDivElement): void {
if (!term) {
const elements = document.querySelectorAll(`.${className} span.token.search-match`);
const elements = container.querySelectorAll("span.token.search-match");
elements.forEach(element => element.classList.remove("search-match"));
return;
}

const termToUse = term.replaceAll("(", "").replaceAll(")", "");
const regex = new RegExp(termToUse, "gi");
const elements = document.querySelectorAll(`.${className} span.token`);
const elements = container.querySelectorAll("span.token");

elements.forEach((element) => {
const text = element.textContent || "";
Expand Down