Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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(() => viewerRef.current && 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(() => viewerRef.current && 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