react-diff-viewer-continued 4.1.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +6 -1
- package/CHANGELOG.md +6 -0
- package/lib/cjs/src/compute-lines.js +15 -5
- package/lib/cjs/src/index.d.ts +44 -0
- package/lib/cjs/src/index.js +257 -17
- package/lib/cjs/src/styles.d.ts +1 -0
- package/lib/cjs/src/styles.js +15 -0
- package/lib/esm/src/compute-lines.js +15 -5
- package/lib/esm/src/index.js +252 -16
- package/lib/esm/src/styles.js +15 -0
- package/package.json +2 -2
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
"Bash(pnpm test:*)",
|
|
9
9
|
"Bash(CI=1 pnpm test:*)",
|
|
10
10
|
"Bash(CI=1 npm test:*)",
|
|
11
|
-
"Bash(pnpm remove:*)"
|
|
11
|
+
"Bash(pnpm remove:*)",
|
|
12
|
+
"Bash(npm test:*)",
|
|
13
|
+
"Bash(npx tsc:*)",
|
|
14
|
+
"Bash(ls:*)",
|
|
15
|
+
"Bash(pnpm run build:*)",
|
|
16
|
+
"Bash(pnpm exec vitest:*)"
|
|
12
17
|
],
|
|
13
18
|
"deny": [],
|
|
14
19
|
"ask": []
|
package/CHANGELOG.md
CHANGED
|
@@ -490,19 +490,29 @@ const computeLineInformation = (oldString, newString, disableWordDiff = false, l
|
|
|
490
490
|
* @returns Promise<ComputedLineInformation> - Resolves with line-by-line diff data from the worker.
|
|
491
491
|
*/
|
|
492
492
|
const computeLineInformationWorker = (oldString, newString, disableWordDiff = false, lineCompareMethod = DiffMethod.CHARS, linesOffset = 0, showLines = [], deferWordDiff = false) => {
|
|
493
|
+
const fallback = () => computeLineInformation(oldString, newString, disableWordDiff, lineCompareMethod, linesOffset, showLines, deferWordDiff);
|
|
493
494
|
// Fall back to synchronous computation if Worker is not available (e.g., in Node.js/test environments)
|
|
494
495
|
if (typeof Worker === 'undefined') {
|
|
495
|
-
return Promise.resolve(
|
|
496
|
+
return Promise.resolve(fallback());
|
|
496
497
|
}
|
|
497
|
-
return new Promise((resolve
|
|
498
|
-
|
|
498
|
+
return new Promise((resolve) => {
|
|
499
|
+
let worker;
|
|
500
|
+
try {
|
|
501
|
+
worker = new Worker(new URL('./computeWorker.js', import.meta.url), { type: 'module' });
|
|
502
|
+
}
|
|
503
|
+
catch (_a) {
|
|
504
|
+
// Worker instantiation failed - fall back to synchronous computation
|
|
505
|
+
resolve(fallback());
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
499
508
|
worker.onmessage = (e) => {
|
|
500
509
|
resolve(e.data);
|
|
501
510
|
worker.terminate();
|
|
502
511
|
};
|
|
503
|
-
worker.onerror = (
|
|
504
|
-
|
|
512
|
+
worker.onerror = () => {
|
|
513
|
+
// Worker error - fall back to synchronous computation
|
|
505
514
|
worker.terminate();
|
|
515
|
+
resolve(fallback());
|
|
506
516
|
};
|
|
507
517
|
worker.postMessage({ oldString, newString, disableWordDiff, lineCompareMethod, linesOffset, showLines, deferWordDiff });
|
|
508
518
|
});
|
package/lib/cjs/src/index.d.ts
CHANGED
|
@@ -65,6 +65,10 @@ export interface ReactDiffViewerProps {
|
|
|
65
65
|
* Hide the summary bar (expand/collapse button, change count, filename)
|
|
66
66
|
*/
|
|
67
67
|
hideSummary?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Show debug overlay with virtualization info (for development)
|
|
70
|
+
*/
|
|
71
|
+
showDebugInfo?: boolean;
|
|
68
72
|
}
|
|
69
73
|
export interface ReactDiffViewerState {
|
|
70
74
|
expandedBlocks?: number[];
|
|
@@ -73,10 +77,17 @@ export interface ReactDiffViewerState {
|
|
|
73
77
|
computedDiffResult: Record<string, ComputedDiffResult>;
|
|
74
78
|
isLoading: boolean;
|
|
75
79
|
visibleStartRow: number;
|
|
80
|
+
contentColumnWidth: number | null;
|
|
81
|
+
charWidth: number | null;
|
|
82
|
+
cumulativeOffsets: number[] | null;
|
|
76
83
|
}
|
|
77
84
|
declare class DiffViewer extends React.Component<ReactDiffViewerProps, ReactDiffViewerState> {
|
|
78
85
|
private styles;
|
|
79
86
|
private wordDiffCache;
|
|
87
|
+
private contentColumnRef;
|
|
88
|
+
private charMeasureRef;
|
|
89
|
+
private stickyHeaderRef;
|
|
90
|
+
private resizeObserver;
|
|
80
91
|
static defaultProps: ReactDiffViewerProps;
|
|
81
92
|
constructor(props: ReactDiffViewerProps);
|
|
82
93
|
/**
|
|
@@ -94,6 +105,38 @@ declare class DiffViewer extends React.Component<ReactDiffViewerProps, ReactDiff
|
|
|
94
105
|
* this value is used to expand/fold unmodified code.
|
|
95
106
|
*/
|
|
96
107
|
private onBlockExpand;
|
|
108
|
+
/**
|
|
109
|
+
* Gets the height of the sticky header, if present.
|
|
110
|
+
*/
|
|
111
|
+
private getStickyHeaderHeight;
|
|
112
|
+
/**
|
|
113
|
+
* Measures the width of a single character in the monospace font.
|
|
114
|
+
* Falls back to 7.2px if measurement fails.
|
|
115
|
+
*/
|
|
116
|
+
private measureCharWidth;
|
|
117
|
+
/**
|
|
118
|
+
* Measures the available width for content in a content column.
|
|
119
|
+
* Falls back to estimating from container width if direct measurement fails.
|
|
120
|
+
*/
|
|
121
|
+
private measureContentColumnWidth;
|
|
122
|
+
/**
|
|
123
|
+
* Gets the text length from a value that may be a string or DiffInformation array.
|
|
124
|
+
*/
|
|
125
|
+
private getTextLength;
|
|
126
|
+
/**
|
|
127
|
+
* Builds cumulative vertical offsets for each line based on character count and column width.
|
|
128
|
+
* This allows accurate scroll position calculations with variable row heights.
|
|
129
|
+
*/
|
|
130
|
+
private buildCumulativeOffsets;
|
|
131
|
+
/**
|
|
132
|
+
* Binary search to find the line index at a given scroll offset.
|
|
133
|
+
*/
|
|
134
|
+
private findLineAtOffset;
|
|
135
|
+
/**
|
|
136
|
+
* Recalculates cumulative offsets based on current measurements.
|
|
137
|
+
* Called on resize and when blocks are expanded/collapsed.
|
|
138
|
+
*/
|
|
139
|
+
private recalculateOffsets;
|
|
97
140
|
/**
|
|
98
141
|
* Computes final styles for the diff viewer. It combines the default styles with the user
|
|
99
142
|
* supplied overrides. The computed styles are cached with performance in mind.
|
|
@@ -200,6 +243,7 @@ declare class DiffViewer extends React.Component<ReactDiffViewerProps, ReactDiff
|
|
|
200
243
|
private renderDiff;
|
|
201
244
|
componentDidUpdate(prevProps: ReactDiffViewerProps): void;
|
|
202
245
|
componentDidMount(): void;
|
|
246
|
+
componentWillUnmount(): void;
|
|
203
247
|
render: () => ReactElement;
|
|
204
248
|
}
|
|
205
249
|
export default DiffViewer;
|
package/lib/cjs/src/index.js
CHANGED
|
@@ -161,6 +161,11 @@ class DiffViewer extends React.Component {
|
|
|
161
161
|
super(props);
|
|
162
162
|
// Cache for on-demand word diff computation
|
|
163
163
|
this.wordDiffCache = new Map();
|
|
164
|
+
// Refs for measuring content column width and character width
|
|
165
|
+
this.contentColumnRef = React.createRef();
|
|
166
|
+
this.charMeasureRef = React.createRef();
|
|
167
|
+
this.stickyHeaderRef = React.createRef();
|
|
168
|
+
this.resizeObserver = null;
|
|
164
169
|
/**
|
|
165
170
|
* Computes word diff on-demand for a line, with caching.
|
|
166
171
|
* This is used when word diff was deferred during initial computation.
|
|
@@ -211,8 +216,28 @@ class DiffViewer extends React.Component {
|
|
|
211
216
|
this.onBlockExpand = (id) => {
|
|
212
217
|
const prevState = this.state.expandedBlocks.slice();
|
|
213
218
|
prevState.push(id);
|
|
214
|
-
this.setState({
|
|
215
|
-
|
|
219
|
+
this.setState({ expandedBlocks: prevState }, () => this.recalculateOffsets());
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Recalculates cumulative offsets based on current measurements.
|
|
223
|
+
* Called on resize and when blocks are expanded/collapsed.
|
|
224
|
+
*/
|
|
225
|
+
this.recalculateOffsets = () => {
|
|
226
|
+
var _a;
|
|
227
|
+
if (!this.props.infiniteLoading)
|
|
228
|
+
return;
|
|
229
|
+
const columnWidth = this.measureContentColumnWidth();
|
|
230
|
+
const charWidth = this.measureCharWidth();
|
|
231
|
+
if (!columnWidth)
|
|
232
|
+
return;
|
|
233
|
+
const cacheKey = this.getMemoisedKey();
|
|
234
|
+
const { lineInformation, lineBlocks, blocks } = (_a = this.state.computedDiffResult[cacheKey]) !== null && _a !== void 0 ? _a : {};
|
|
235
|
+
if (!lineInformation)
|
|
236
|
+
return;
|
|
237
|
+
const offsets = this.buildCumulativeOffsets(lineInformation, lineBlocks, blocks, this.state.expandedBlocks, this.props.showDiffOnly, charWidth, columnWidth, this.props.splitView);
|
|
238
|
+
this.setState({ cumulativeOffsets: offsets, contentColumnWidth: columnWidth, charWidth }, () => {
|
|
239
|
+
// Force a scroll position update to recalculate visible rows with new offsets
|
|
240
|
+
this.onScroll();
|
|
216
241
|
});
|
|
217
242
|
};
|
|
218
243
|
/**
|
|
@@ -379,7 +404,7 @@ class DiffViewer extends React.Component {
|
|
|
379
404
|
[this.styles.diffRemoved]: removed,
|
|
380
405
|
[this.styles.diffChanged]: changed,
|
|
381
406
|
[this.styles.highlightedLine]: highlightLine,
|
|
382
|
-
}), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { className: cn(this.styles.content, {
|
|
407
|
+
}), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { ref: prefix === LineNumberPrefix.LEFT && !this.state.cumulativeOffsets ? this.contentColumnRef : undefined, className: cn(this.styles.content, {
|
|
383
408
|
[this.styles.emptyLine]: !content,
|
|
384
409
|
[this.styles.diffAdded]: added,
|
|
385
410
|
[this.styles.diffRemoved]: removed,
|
|
@@ -517,7 +542,13 @@ class DiffViewer extends React.Component {
|
|
|
517
542
|
: Math.round(this.props.extraLinesSurroundingDiff);
|
|
518
543
|
const { lineBlocks, blocks } = computeHiddenBlocks(lineInformation, diffLines, extraLines);
|
|
519
544
|
this.state.computedDiffResult[cacheKey] = { lineInformation, lineBlocks, blocks };
|
|
520
|
-
this.setState((prev) => (Object.assign(Object.assign({}, prev), { computedDiffResult: this.state.computedDiffResult, isLoading: false })))
|
|
545
|
+
this.setState((prev) => (Object.assign(Object.assign({}, prev), { computedDiffResult: this.state.computedDiffResult, isLoading: false })), () => {
|
|
546
|
+
// Trigger offset recalculation after diff is computed and rendered
|
|
547
|
+
// Use requestAnimationFrame to ensure DOM is ready for measurement
|
|
548
|
+
if (this.props.infiniteLoading) {
|
|
549
|
+
requestAnimationFrame(() => this.recalculateOffsets());
|
|
550
|
+
}
|
|
551
|
+
});
|
|
521
552
|
});
|
|
522
553
|
/**
|
|
523
554
|
* Handles scroll events on the scrollable container.
|
|
@@ -528,7 +559,13 @@ class DiffViewer extends React.Component {
|
|
|
528
559
|
const container = this.state.scrollableContainerRef.current;
|
|
529
560
|
if (!container || !this.props.infiniteLoading)
|
|
530
561
|
return;
|
|
531
|
-
|
|
562
|
+
// Account for sticky header height in scroll calculations
|
|
563
|
+
const headerHeight = this.getStickyHeaderHeight();
|
|
564
|
+
const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
|
|
565
|
+
const { cumulativeOffsets } = this.state;
|
|
566
|
+
const newStartRow = cumulativeOffsets
|
|
567
|
+
? this.findLineAtOffset(contentScrollTop, cumulativeOffsets)
|
|
568
|
+
: Math.floor(contentScrollTop / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
532
569
|
// Only update state if the start row changed (avoid unnecessary re-renders)
|
|
533
570
|
if (newStartRow !== this.state.visibleStartRow) {
|
|
534
571
|
this.setState({ visibleStartRow: newStartRow });
|
|
@@ -538,9 +575,9 @@ class DiffViewer extends React.Component {
|
|
|
538
575
|
* Generates the entire diff view with virtualization support.
|
|
539
576
|
*/
|
|
540
577
|
this.renderDiff = () => {
|
|
541
|
-
var _a;
|
|
578
|
+
var _a, _b, _c, _d, _e, _f;
|
|
542
579
|
const { splitView, infiniteLoading, showDiffOnly } = this.props;
|
|
543
|
-
const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef } = this.state;
|
|
580
|
+
const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef, cumulativeOffsets } = this.state;
|
|
544
581
|
const cacheKey = this.getMemoisedKey();
|
|
545
582
|
const { lineInformation = [], lineBlocks = [], blocks = [] } = (_a = computedDiffResult[cacheKey]) !== null && _a !== void 0 ? _a : {};
|
|
546
583
|
// Calculate visible range for virtualization
|
|
@@ -549,9 +586,32 @@ class DiffViewer extends React.Component {
|
|
|
549
586
|
const buffer = 5; // render extra rows above/below viewport
|
|
550
587
|
if (infiniteLoading && scrollableContainerRef.current) {
|
|
551
588
|
const container = scrollableContainerRef.current;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
589
|
+
// Account for sticky header height in scroll calculations
|
|
590
|
+
const headerHeight = this.getStickyHeaderHeight();
|
|
591
|
+
const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
|
|
592
|
+
if (cumulativeOffsets) {
|
|
593
|
+
// Variable height mode: use binary search to find visible range
|
|
594
|
+
const totalHeight = cumulativeOffsets[cumulativeOffsets.length - 1] || 0;
|
|
595
|
+
const lastRowIndex = cumulativeOffsets.length - 2;
|
|
596
|
+
visibleRowStart = Math.max(0, this.findLineAtOffset(contentScrollTop, cumulativeOffsets) - buffer);
|
|
597
|
+
visibleRowEnd = this.findLineAtOffset(contentScrollTop + container.clientHeight, cumulativeOffsets) + buffer;
|
|
598
|
+
// IMPORTANT: The calculated offsets may overestimate row heights (based on char count),
|
|
599
|
+
// but actual CSS rendering might produce shorter rows. To prevent empty space,
|
|
600
|
+
// ensure we render at least enough rows to fill the viewport using ESTIMATED_ROW_HEIGHT
|
|
601
|
+
// as a conservative minimum.
|
|
602
|
+
const minRowsToFillViewport = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
603
|
+
visibleRowEnd = Math.max(visibleRowEnd, visibleRowStart + minRowsToFillViewport + buffer);
|
|
604
|
+
// Also ensure we render all rows when near the bottom
|
|
605
|
+
if (contentScrollTop + container.clientHeight >= totalHeight - buffer * DiffViewer.ESTIMATED_ROW_HEIGHT) {
|
|
606
|
+
visibleRowEnd = lastRowIndex + buffer;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
// Fixed height fallback
|
|
611
|
+
const viewportRows = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
612
|
+
visibleRowStart = Math.max(0, visibleStartRow - buffer);
|
|
613
|
+
visibleRowEnd = visibleStartRow + viewportRows + buffer;
|
|
614
|
+
}
|
|
555
615
|
}
|
|
556
616
|
// First pass: build a map of lineIndex -> renderedRowIndex
|
|
557
617
|
// This accounts for code folding where some lines don't render or render as fold indicators
|
|
@@ -589,6 +649,7 @@ class DiffViewer extends React.Component {
|
|
|
589
649
|
const diffNodes = [];
|
|
590
650
|
let topPadding = 0;
|
|
591
651
|
let firstVisibleFound = false;
|
|
652
|
+
let lastRenderedRowIndex = -1;
|
|
592
653
|
seenBlocks.clear();
|
|
593
654
|
for (let lineIndex = 0; lineIndex < lineInformation.length; lineIndex++) {
|
|
594
655
|
const line = lineInformation[lineIndex];
|
|
@@ -606,9 +667,13 @@ class DiffViewer extends React.Component {
|
|
|
606
667
|
}
|
|
607
668
|
// Calculate top padding from the first visible row
|
|
608
669
|
if (!firstVisibleFound) {
|
|
609
|
-
topPadding =
|
|
670
|
+
topPadding = cumulativeOffsets
|
|
671
|
+
? cumulativeOffsets[rowIndex] || 0
|
|
672
|
+
: rowIndex * DiffViewer.ESTIMATED_ROW_HEIGHT;
|
|
610
673
|
firstVisibleFound = true;
|
|
611
674
|
}
|
|
675
|
+
// Track the last rendered row for bottom padding calculation
|
|
676
|
+
lastRenderedRowIndex = rowIndex;
|
|
612
677
|
// Render the line
|
|
613
678
|
if (showDiffOnly) {
|
|
614
679
|
const blockIndex = lineBlocks[lineIndex];
|
|
@@ -628,15 +693,41 @@ class DiffViewer extends React.Component {
|
|
|
628
693
|
? this.renderSplitView(line, lineIndex)
|
|
629
694
|
: this.renderInlineView(line, lineIndex));
|
|
630
695
|
}
|
|
696
|
+
// Calculate total content height
|
|
697
|
+
const totalContentHeight = cumulativeOffsets
|
|
698
|
+
? cumulativeOffsets[cumulativeOffsets.length - 1] || 0
|
|
699
|
+
: totalRenderedRows * DiffViewer.ESTIMATED_ROW_HEIGHT;
|
|
700
|
+
// Calculate bottom padding: space after the last rendered row
|
|
701
|
+
const bottomPadding = cumulativeOffsets && lastRenderedRowIndex >= 0
|
|
702
|
+
? totalContentHeight - (cumulativeOffsets[lastRenderedRowIndex + 1] || totalContentHeight)
|
|
703
|
+
: 0;
|
|
631
704
|
return {
|
|
632
705
|
diffNodes,
|
|
633
706
|
blocks,
|
|
634
707
|
lineInformation,
|
|
635
708
|
totalRenderedRows,
|
|
636
709
|
topPadding,
|
|
710
|
+
bottomPadding,
|
|
711
|
+
totalContentHeight,
|
|
712
|
+
renderedCount: diffNodes.length,
|
|
713
|
+
// Debug info
|
|
714
|
+
debug: {
|
|
715
|
+
visibleRowStart,
|
|
716
|
+
visibleRowEnd,
|
|
717
|
+
totalRows: totalRenderedRows,
|
|
718
|
+
offsetsLength: (_b = cumulativeOffsets === null || cumulativeOffsets === void 0 ? void 0 : cumulativeOffsets.length) !== null && _b !== void 0 ? _b : 0,
|
|
719
|
+
renderedCount: diffNodes.length,
|
|
720
|
+
scrollTop: (_d = (_c = scrollableContainerRef.current) === null || _c === void 0 ? void 0 : _c.scrollTop) !== null && _d !== void 0 ? _d : 0,
|
|
721
|
+
headerHeight: this.getStickyHeaderHeight(),
|
|
722
|
+
contentScrollTop: scrollableContainerRef.current
|
|
723
|
+
? Math.max(0, scrollableContainerRef.current.scrollTop - this.getStickyHeaderHeight())
|
|
724
|
+
: 0,
|
|
725
|
+
clientHeight: (_f = (_e = scrollableContainerRef.current) === null || _e === void 0 ? void 0 : _e.clientHeight) !== null && _f !== void 0 ? _f : 0,
|
|
726
|
+
}
|
|
637
727
|
};
|
|
638
728
|
};
|
|
639
729
|
this.render = () => {
|
|
730
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
640
731
|
const { oldValue, newValue, useDarkTheme, leftTitle, rightTitle, splitView, compareMethod, hideLineNumbers, nonce, } = this.props;
|
|
641
732
|
if (typeof compareMethod === "string" &&
|
|
642
733
|
compareMethod !== DiffMethod.JSON) {
|
|
@@ -690,9 +781,12 @@ class DiffViewer extends React.Component {
|
|
|
690
781
|
overflowX: 'hidden',
|
|
691
782
|
height: this.props.infiniteLoading.containerHeight
|
|
692
783
|
} : {};
|
|
693
|
-
|
|
784
|
+
// Only apply noWrap when infiniteLoading is enabled but we don't have cumulative offsets yet
|
|
785
|
+
// Once offsets are calculated, we enable pre-wrap for proper text wrapping
|
|
786
|
+
const shouldNoWrap = !!this.props.infiniteLoading && !this.state.cumulativeOffsets;
|
|
694
787
|
const tableElement = (_jsxs("table", { className: cn(this.styles.diffContainer, {
|
|
695
788
|
[this.styles.splitView]: splitView,
|
|
789
|
+
[this.styles.noWrap]: shouldNoWrap,
|
|
696
790
|
}), onMouseUp: () => {
|
|
697
791
|
const elements = document.getElementsByClassName("right");
|
|
698
792
|
for (let i = 0; i < elements.length; i++) {
|
|
@@ -705,13 +799,37 @@ class DiffViewer extends React.Component {
|
|
|
705
799
|
element.classList.remove(this.styles.noSelect);
|
|
706
800
|
}
|
|
707
801
|
}, children: [_jsxs("colgroup", { children: [!this.props.hideLineNumbers && _jsx("col", { width: "50px" }), !splitView && !this.props.hideLineNumbers && _jsx("col", { width: "50px" }), this.props.renderGutter && _jsx("col", { width: "50px" }), _jsx("col", { width: "28px" }), _jsx("col", { width: "auto" }), splitView && (_jsxs(_Fragment, { children: [!this.props.hideLineNumbers && _jsx("col", { width: "50px" }), this.props.renderGutter && _jsx("col", { width: "50px" }), _jsx("col", { width: "28px" }), _jsx("col", { width: "auto" })] }))] }), _jsx("tbody", { children: nodes.diffNodes })] }));
|
|
708
|
-
return (_jsxs("div", { style: Object.assign(Object.assign({}, scrollDivStyle), { position: 'relative' }), onScroll: this.onScroll, ref: this.state.scrollableContainerRef, children: [(!this.props.hideSummary || leftTitle || rightTitle) && (_jsxs("div", { className: this.styles.stickyHeader, children: [!this.props.hideSummary && (_jsxs("div", { className: this.styles.summary, role: "banner", children: [_jsx("button", { type: "button", className: this.styles.allExpandButton, onClick: () => {
|
|
802
|
+
return (_jsxs("div", { style: Object.assign(Object.assign({}, scrollDivStyle), { position: 'relative' }), onScroll: this.onScroll, ref: this.state.scrollableContainerRef, children: [(!this.props.hideSummary || leftTitle || rightTitle) && (_jsxs("div", { ref: this.stickyHeaderRef, className: this.styles.stickyHeader, children: [!this.props.hideSummary && (_jsxs("div", { className: this.styles.summary, role: "banner", children: [_jsx("button", { type: "button", className: this.styles.allExpandButton, onClick: () => {
|
|
709
803
|
this.setState({
|
|
710
804
|
expandedBlocks: allExpanded
|
|
711
805
|
? []
|
|
712
806
|
: nodes.blocks.map((b) => b.index),
|
|
713
|
-
});
|
|
714
|
-
}, children: allExpanded ? _jsx(Fold, {}) : _jsx(Expand, {}) }), " ", totalChanges, _jsx("div", { style: { display: "flex", gap: "1px" }, children: blocks }), this.props.summary ? _jsx("span", { children: this.props.summary }) : null] })), (leftTitle || rightTitle) && (_jsxs("div", { className: this.styles.columnHeaders, children: [_jsx("div", { className: this.styles.titleBlock, children: leftTitle ? (_jsx("pre", { className: this.styles.contentText, children: leftTitle })) : null }), splitView && (_jsx("div", { className: this.styles.titleBlock, children: rightTitle ? (_jsx("pre", { className: this.styles.contentText, children: rightTitle })) : null }))] }))] })), this.state.isLoading && LoadingElement && _jsx(LoadingElement, {}), this.props.infiniteLoading ? (_jsx("div", { style: {
|
|
807
|
+
}, () => this.recalculateOffsets());
|
|
808
|
+
}, children: allExpanded ? _jsx(Fold, {}) : _jsx(Expand, {}) }), " ", totalChanges, _jsx("div", { style: { display: "flex", gap: "1px" }, children: blocks }), this.props.summary ? _jsx("span", { children: this.props.summary }) : null] })), (leftTitle || rightTitle) && (_jsxs("div", { className: this.styles.columnHeaders, children: [_jsx("div", { className: this.styles.titleBlock, children: leftTitle ? (_jsx("pre", { className: this.styles.contentText, children: leftTitle })) : null }), splitView && (_jsx("div", { className: this.styles.titleBlock, children: rightTitle ? (_jsx("pre", { className: this.styles.contentText, children: rightTitle })) : null }))] }))] })), this.state.isLoading && LoadingElement && _jsx(LoadingElement, {}), this.props.infiniteLoading ? (_jsx("div", { style: {
|
|
809
|
+
paddingTop: nodes.topPadding,
|
|
810
|
+
paddingBottom: nodes.bottomPadding,
|
|
811
|
+
}, children: tableElement })) : (tableElement), _jsx("span", { ref: this.charMeasureRef, style: {
|
|
812
|
+
position: 'absolute',
|
|
813
|
+
top: 0,
|
|
814
|
+
left: '-9999px',
|
|
815
|
+
visibility: 'hidden',
|
|
816
|
+
whiteSpace: 'pre',
|
|
817
|
+
fontFamily: 'monospace',
|
|
818
|
+
fontSize: 12,
|
|
819
|
+
}, "aria-hidden": "true", children: "M" }), this.props.infiniteLoading && this.props.showDebugInfo && (_jsxs("div", { style: {
|
|
820
|
+
position: 'fixed',
|
|
821
|
+
top: 10,
|
|
822
|
+
right: 10,
|
|
823
|
+
background: 'rgba(0,0,0,0.85)',
|
|
824
|
+
color: '#0f0',
|
|
825
|
+
padding: '10px',
|
|
826
|
+
fontFamily: 'monospace',
|
|
827
|
+
fontSize: '11px',
|
|
828
|
+
zIndex: 9999,
|
|
829
|
+
borderRadius: '4px',
|
|
830
|
+
maxWidth: '300px',
|
|
831
|
+
lineHeight: 1.4,
|
|
832
|
+
}, children: [_jsx("div", { style: { fontWeight: 'bold', marginBottom: '5px', color: '#fff' }, children: "Debug Info" }), _jsxs("div", { children: ["scrollTop: ", nodes.debug.scrollTop] }), _jsxs("div", { children: ["headerHeight: ", nodes.debug.headerHeight] }), _jsxs("div", { children: ["contentScrollTop: ", nodes.debug.contentScrollTop] }), _jsxs("div", { children: ["clientHeight: ", nodes.debug.clientHeight] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["visibleRowStart: ", nodes.debug.visibleRowStart] }), _jsxs("div", { children: ["visibleRowEnd: ", nodes.debug.visibleRowEnd] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["totalRows: ", nodes.debug.totalRows] }), _jsxs("div", { children: ["offsetsLength: ", nodes.debug.offsetsLength] }), _jsxs("div", { children: ["renderedCount: ", nodes.debug.renderedCount] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["topPadding: ", nodes.topPadding.toFixed(0)] }), _jsxs("div", { children: ["bottomPadding: ", nodes.bottomPadding.toFixed(0)] }), _jsxs("div", { children: ["totalContentHeight: ", nodes.totalContentHeight.toFixed(0)] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px', color: '#ff0' }, children: [_jsxs("div", { children: ["cumulativeOffsets: ", this.state.cumulativeOffsets ? 'SET' : 'NULL'] }), _jsxs("div", { children: ["columnWidth: ", (_b = (_a = this.state.contentColumnWidth) === null || _a === void 0 ? void 0 : _a.toFixed(0)) !== null && _b !== void 0 ? _b : 'N/A', "px"] }), _jsxs("div", { children: ["charWidth: ", (_d = (_c = this.state.charWidth) === null || _c === void 0 ? void 0 : _c.toFixed(2)) !== null && _d !== void 0 ? _d : 'N/A', "px"] }), _jsxs("div", { children: ["charsPerRow: ", this.state.contentColumnWidth && this.state.charWidth ? Math.floor(this.state.contentColumnWidth / this.state.charWidth) : 'N/A'] })] }), this.state.cumulativeOffsets && (_jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px', color: '#0ff', fontSize: '10px' }, children: [_jsxs("div", { children: ["offsets[", nodes.debug.visibleRowEnd, "]: ", (_f = (_e = this.state.cumulativeOffsets[nodes.debug.visibleRowEnd]) === null || _e === void 0 ? void 0 : _e.toFixed(0)) !== null && _f !== void 0 ? _f : 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows - 1, "]: ", (_h = (_g = this.state.cumulativeOffsets[nodes.debug.totalRows - 1]) === null || _g === void 0 ? void 0 : _g.toFixed(0)) !== null && _h !== void 0 ? _h : 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows, "]: ", (_k = (_j = this.state.cumulativeOffsets[nodes.debug.totalRows]) === null || _j === void 0 ? void 0 : _j.toFixed(0)) !== null && _k !== void 0 ? _k : 'N/A'] }), _jsxs("div", { style: { marginTop: '3px' }, children: ["viewportEnd: ", (nodes.debug.contentScrollTop + nodes.debug.clientHeight).toFixed(0)] }), _jsxs("div", { style: { marginTop: '3px', color: '#f0f' }, children: ["scrollHeight: ", (_m = (_l = this.state.scrollableContainerRef.current) === null || _l === void 0 ? void 0 : _l.scrollHeight) !== null && _m !== void 0 ? _m : 'N/A'] }), _jsxs("div", { children: ["maxScrollTop: ", ((_p = (_o = this.state.scrollableContainerRef.current) === null || _o === void 0 ? void 0 : _o.scrollHeight) !== null && _p !== void 0 ? _p : 0) - nodes.debug.clientHeight] })] }))] }))] }));
|
|
715
833
|
};
|
|
716
834
|
this.state = {
|
|
717
835
|
expandedBlocks: [],
|
|
@@ -719,9 +837,117 @@ class DiffViewer extends React.Component {
|
|
|
719
837
|
scrollableContainerRef: React.createRef(),
|
|
720
838
|
computedDiffResult: {},
|
|
721
839
|
isLoading: false,
|
|
722
|
-
visibleStartRow: 0
|
|
840
|
+
visibleStartRow: 0,
|
|
841
|
+
contentColumnWidth: null,
|
|
842
|
+
charWidth: null,
|
|
843
|
+
cumulativeOffsets: null,
|
|
723
844
|
};
|
|
724
845
|
}
|
|
846
|
+
/**
|
|
847
|
+
* Gets the height of the sticky header, if present.
|
|
848
|
+
*/
|
|
849
|
+
getStickyHeaderHeight() {
|
|
850
|
+
var _a;
|
|
851
|
+
return ((_a = this.stickyHeaderRef.current) === null || _a === void 0 ? void 0 : _a.offsetHeight) || 0;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Measures the width of a single character in the monospace font.
|
|
855
|
+
* Falls back to 7.2px if measurement fails.
|
|
856
|
+
*/
|
|
857
|
+
measureCharWidth() {
|
|
858
|
+
const span = this.charMeasureRef.current;
|
|
859
|
+
if (!span)
|
|
860
|
+
return 7.2; // fallback
|
|
861
|
+
return span.getBoundingClientRect().width || 7.2;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Measures the available width for content in a content column.
|
|
865
|
+
* Falls back to estimating from container width if direct measurement fails.
|
|
866
|
+
*/
|
|
867
|
+
measureContentColumnWidth() {
|
|
868
|
+
// Try direct measurement first
|
|
869
|
+
const cell = this.contentColumnRef.current;
|
|
870
|
+
if (cell && cell.clientWidth > 0) {
|
|
871
|
+
const style = window.getComputedStyle(cell);
|
|
872
|
+
const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
873
|
+
const width = cell.clientWidth - padding;
|
|
874
|
+
if (width > 0)
|
|
875
|
+
return width;
|
|
876
|
+
}
|
|
877
|
+
// Fallback: estimate from container width
|
|
878
|
+
// In split view: container has 2 content columns + gutters (50px each) + markers (28px each)
|
|
879
|
+
// In unified view: 1 content column + 2 gutters + 1 marker
|
|
880
|
+
const container = this.state.scrollableContainerRef.current;
|
|
881
|
+
if (!container || container.clientWidth <= 0)
|
|
882
|
+
return null;
|
|
883
|
+
const containerWidth = container.clientWidth;
|
|
884
|
+
const gutterWidth = this.props.hideLineNumbers ? 0 : 50;
|
|
885
|
+
const markerWidth = 28;
|
|
886
|
+
const gutterCount = this.props.splitView ? 2 : 2; // left gutter(s)
|
|
887
|
+
const markerCount = this.props.splitView ? 2 : 1;
|
|
888
|
+
const contentColumns = this.props.splitView ? 2 : 1;
|
|
889
|
+
const fixedWidth = gutterCount * gutterWidth + markerCount * markerWidth;
|
|
890
|
+
const availableWidth = containerWidth - fixedWidth;
|
|
891
|
+
return Math.max(100, availableWidth / contentColumns); // minimum 100px
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Gets the text length from a value that may be a string or DiffInformation array.
|
|
895
|
+
*/
|
|
896
|
+
getTextLength(value) {
|
|
897
|
+
if (!value)
|
|
898
|
+
return 0;
|
|
899
|
+
if (typeof value === 'string')
|
|
900
|
+
return value.length;
|
|
901
|
+
return value.reduce((sum, d) => sum + (typeof d.value === 'string' ? d.value.length : 0), 0);
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Builds cumulative vertical offsets for each line based on character count and column width.
|
|
905
|
+
* This allows accurate scroll position calculations with variable row heights.
|
|
906
|
+
*/
|
|
907
|
+
buildCumulativeOffsets(lineInformation, lineBlocks, blocks, expandedBlocks, showDiffOnly, charWidth, columnWidth, splitView) {
|
|
908
|
+
var _a, _b;
|
|
909
|
+
const offsets = [0];
|
|
910
|
+
const seenBlocks = new Set();
|
|
911
|
+
for (let i = 0; i < lineInformation.length; i++) {
|
|
912
|
+
const line = lineInformation[i];
|
|
913
|
+
if (showDiffOnly) {
|
|
914
|
+
const blockIndex = lineBlocks[i];
|
|
915
|
+
if (blockIndex !== undefined && !expandedBlocks.includes(blockIndex)) {
|
|
916
|
+
const isLastLine = blocks[blockIndex].endLine === i;
|
|
917
|
+
if (!seenBlocks.has(blockIndex) && isLastLine) {
|
|
918
|
+
seenBlocks.add(blockIndex);
|
|
919
|
+
offsets.push(offsets[offsets.length - 1] + DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
920
|
+
}
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
// Calculate visual rows for this line
|
|
925
|
+
const leftLen = ((_a = line.left) === null || _a === void 0 ? void 0 : _a.value) ? this.getTextLength(line.left.value) : 0;
|
|
926
|
+
const rightLen = ((_b = line.right) === null || _b === void 0 ? void 0 : _b.value) ? this.getTextLength(line.right.value) : 0;
|
|
927
|
+
const maxLen = splitView ? Math.max(leftLen, rightLen) : (leftLen || rightLen);
|
|
928
|
+
const charsPerRow = Math.floor(columnWidth / charWidth);
|
|
929
|
+
const visualRows = charsPerRow > 0 ? Math.max(1, Math.ceil(maxLen / charsPerRow)) : 1;
|
|
930
|
+
offsets.push(offsets[offsets.length - 1] + visualRows * DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
931
|
+
}
|
|
932
|
+
return offsets;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Binary search to find the line index at a given scroll offset.
|
|
936
|
+
*/
|
|
937
|
+
findLineAtOffset(scrollTop, offsets) {
|
|
938
|
+
let low = 0;
|
|
939
|
+
let high = offsets.length - 2;
|
|
940
|
+
while (low < high) {
|
|
941
|
+
const mid = Math.floor((low + high + 1) / 2);
|
|
942
|
+
if (offsets[mid] <= scrollTop) {
|
|
943
|
+
low = mid;
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
high = mid - 1;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return low;
|
|
950
|
+
}
|
|
725
951
|
componentDidUpdate(prevProps) {
|
|
726
952
|
if (prevProps.oldValue !== this.props.oldValue ||
|
|
727
953
|
prevProps.newValue !== this.props.newValue ||
|
|
@@ -730,13 +956,27 @@ class DiffViewer extends React.Component {
|
|
|
730
956
|
prevProps.linesOffset !== this.props.linesOffset) {
|
|
731
957
|
// Clear word diff cache when diff changes
|
|
732
958
|
this.wordDiffCache.clear();
|
|
733
|
-
this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true, visibleStartRow: 0 })));
|
|
959
|
+
this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true, visibleStartRow: 0, cumulativeOffsets: null })));
|
|
734
960
|
this.memoisedCompute();
|
|
735
961
|
}
|
|
736
962
|
}
|
|
737
963
|
componentDidMount() {
|
|
738
964
|
this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true })));
|
|
739
965
|
this.memoisedCompute();
|
|
966
|
+
// Set up ResizeObserver for recalculating offsets on container resize
|
|
967
|
+
if (typeof ResizeObserver !== 'undefined' && this.props.infiniteLoading) {
|
|
968
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
969
|
+
requestAnimationFrame(() => this.recalculateOffsets());
|
|
970
|
+
});
|
|
971
|
+
const container = this.state.scrollableContainerRef.current;
|
|
972
|
+
if (container) {
|
|
973
|
+
this.resizeObserver.observe(container);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
componentWillUnmount() {
|
|
978
|
+
var _a;
|
|
979
|
+
(_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
740
980
|
}
|
|
741
981
|
}
|
|
742
982
|
DiffViewer.defaultProps = {
|
package/lib/cjs/src/styles.d.ts
CHANGED
package/lib/cjs/src/styles.js
CHANGED
|
@@ -72,6 +72,7 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
72
72
|
const { css, cx } = createEmotion({ key: "react-diff", nonce });
|
|
73
73
|
const content = css({
|
|
74
74
|
width: "auto",
|
|
75
|
+
overflow: "hidden",
|
|
75
76
|
label: "content",
|
|
76
77
|
});
|
|
77
78
|
const splitView = css({
|
|
@@ -113,6 +114,9 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
113
114
|
},
|
|
114
115
|
label: "diff-container",
|
|
115
116
|
borderCollapse: "collapse",
|
|
117
|
+
"@media (max-width: 768px)": {
|
|
118
|
+
minWidth: "unset",
|
|
119
|
+
},
|
|
116
120
|
});
|
|
117
121
|
const lineContent = css({
|
|
118
122
|
overflow: "hidden",
|
|
@@ -130,6 +134,16 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
130
134
|
userSelect: "none",
|
|
131
135
|
label: "unselectable",
|
|
132
136
|
});
|
|
137
|
+
const noWrap = css({
|
|
138
|
+
label: "no-wrap",
|
|
139
|
+
pre: {
|
|
140
|
+
whiteSpace: "pre",
|
|
141
|
+
},
|
|
142
|
+
[`.${contentText}`]: {
|
|
143
|
+
whiteSpace: "pre",
|
|
144
|
+
lineBreak: "auto",
|
|
145
|
+
},
|
|
146
|
+
});
|
|
133
147
|
const allExpandButton = css({
|
|
134
148
|
background: "transparent",
|
|
135
149
|
border: "none",
|
|
@@ -376,6 +390,7 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
376
390
|
blockDeletion,
|
|
377
391
|
wordRemoved,
|
|
378
392
|
noSelect: unselectable,
|
|
393
|
+
noWrap,
|
|
379
394
|
codeFoldGutter,
|
|
380
395
|
codeFoldExpandButton,
|
|
381
396
|
codeFoldContentContainer,
|
|
@@ -490,19 +490,29 @@ const computeLineInformation = (oldString, newString, disableWordDiff = false, l
|
|
|
490
490
|
* @returns Promise<ComputedLineInformation> - Resolves with line-by-line diff data from the worker.
|
|
491
491
|
*/
|
|
492
492
|
const computeLineInformationWorker = (oldString, newString, disableWordDiff = false, lineCompareMethod = DiffMethod.CHARS, linesOffset = 0, showLines = [], deferWordDiff = false) => {
|
|
493
|
+
const fallback = () => computeLineInformation(oldString, newString, disableWordDiff, lineCompareMethod, linesOffset, showLines, deferWordDiff);
|
|
493
494
|
// Fall back to synchronous computation if Worker is not available (e.g., in Node.js/test environments)
|
|
494
495
|
if (typeof Worker === 'undefined') {
|
|
495
|
-
return Promise.resolve(
|
|
496
|
+
return Promise.resolve(fallback());
|
|
496
497
|
}
|
|
497
|
-
return new Promise((resolve
|
|
498
|
-
|
|
498
|
+
return new Promise((resolve) => {
|
|
499
|
+
let worker;
|
|
500
|
+
try {
|
|
501
|
+
worker = new Worker(new URL('./computeWorker.js', import.meta.url), { type: 'module' });
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Worker instantiation failed - fall back to synchronous computation
|
|
505
|
+
resolve(fallback());
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
499
508
|
worker.onmessage = (e) => {
|
|
500
509
|
resolve(e.data);
|
|
501
510
|
worker.terminate();
|
|
502
511
|
};
|
|
503
|
-
worker.onerror = (
|
|
504
|
-
|
|
512
|
+
worker.onerror = () => {
|
|
513
|
+
// Worker error - fall back to synchronous computation
|
|
505
514
|
worker.terminate();
|
|
515
|
+
resolve(fallback());
|
|
506
516
|
};
|
|
507
517
|
worker.postMessage({ oldString, newString, disableWordDiff, lineCompareMethod, linesOffset, showLines, deferWordDiff });
|
|
508
518
|
});
|
package/lib/esm/src/index.js
CHANGED
|
@@ -151,6 +151,11 @@ class DiffViewer extends React.Component {
|
|
|
151
151
|
styles;
|
|
152
152
|
// Cache for on-demand word diff computation
|
|
153
153
|
wordDiffCache = new Map();
|
|
154
|
+
// Refs for measuring content column width and character width
|
|
155
|
+
contentColumnRef = React.createRef();
|
|
156
|
+
charMeasureRef = React.createRef();
|
|
157
|
+
stickyHeaderRef = React.createRef();
|
|
158
|
+
resizeObserver = null;
|
|
154
159
|
static defaultProps = {
|
|
155
160
|
oldValue: "",
|
|
156
161
|
newValue: "",
|
|
@@ -174,7 +179,10 @@ class DiffViewer extends React.Component {
|
|
|
174
179
|
scrollableContainerRef: React.createRef(),
|
|
175
180
|
computedDiffResult: {},
|
|
176
181
|
isLoading: false,
|
|
177
|
-
visibleStartRow: 0
|
|
182
|
+
visibleStartRow: 0,
|
|
183
|
+
contentColumnWidth: null,
|
|
184
|
+
charWidth: null,
|
|
185
|
+
cumulativeOffsets: null,
|
|
178
186
|
};
|
|
179
187
|
}
|
|
180
188
|
/**
|
|
@@ -227,8 +235,130 @@ class DiffViewer extends React.Component {
|
|
|
227
235
|
onBlockExpand = (id) => {
|
|
228
236
|
const prevState = this.state.expandedBlocks.slice();
|
|
229
237
|
prevState.push(id);
|
|
230
|
-
this.setState({
|
|
231
|
-
|
|
238
|
+
this.setState({ expandedBlocks: prevState }, () => this.recalculateOffsets());
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Gets the height of the sticky header, if present.
|
|
242
|
+
*/
|
|
243
|
+
getStickyHeaderHeight() {
|
|
244
|
+
return this.stickyHeaderRef.current?.offsetHeight || 0;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Measures the width of a single character in the monospace font.
|
|
248
|
+
* Falls back to 7.2px if measurement fails.
|
|
249
|
+
*/
|
|
250
|
+
measureCharWidth() {
|
|
251
|
+
const span = this.charMeasureRef.current;
|
|
252
|
+
if (!span)
|
|
253
|
+
return 7.2; // fallback
|
|
254
|
+
return span.getBoundingClientRect().width || 7.2;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Measures the available width for content in a content column.
|
|
258
|
+
* Falls back to estimating from container width if direct measurement fails.
|
|
259
|
+
*/
|
|
260
|
+
measureContentColumnWidth() {
|
|
261
|
+
// Try direct measurement first
|
|
262
|
+
const cell = this.contentColumnRef.current;
|
|
263
|
+
if (cell && cell.clientWidth > 0) {
|
|
264
|
+
const style = window.getComputedStyle(cell);
|
|
265
|
+
const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
266
|
+
const width = cell.clientWidth - padding;
|
|
267
|
+
if (width > 0)
|
|
268
|
+
return width;
|
|
269
|
+
}
|
|
270
|
+
// Fallback: estimate from container width
|
|
271
|
+
// In split view: container has 2 content columns + gutters (50px each) + markers (28px each)
|
|
272
|
+
// In unified view: 1 content column + 2 gutters + 1 marker
|
|
273
|
+
const container = this.state.scrollableContainerRef.current;
|
|
274
|
+
if (!container || container.clientWidth <= 0)
|
|
275
|
+
return null;
|
|
276
|
+
const containerWidth = container.clientWidth;
|
|
277
|
+
const gutterWidth = this.props.hideLineNumbers ? 0 : 50;
|
|
278
|
+
const markerWidth = 28;
|
|
279
|
+
const gutterCount = this.props.splitView ? 2 : 2; // left gutter(s)
|
|
280
|
+
const markerCount = this.props.splitView ? 2 : 1;
|
|
281
|
+
const contentColumns = this.props.splitView ? 2 : 1;
|
|
282
|
+
const fixedWidth = gutterCount * gutterWidth + markerCount * markerWidth;
|
|
283
|
+
const availableWidth = containerWidth - fixedWidth;
|
|
284
|
+
return Math.max(100, availableWidth / contentColumns); // minimum 100px
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Gets the text length from a value that may be a string or DiffInformation array.
|
|
288
|
+
*/
|
|
289
|
+
getTextLength(value) {
|
|
290
|
+
if (!value)
|
|
291
|
+
return 0;
|
|
292
|
+
if (typeof value === 'string')
|
|
293
|
+
return value.length;
|
|
294
|
+
return value.reduce((sum, d) => sum + (typeof d.value === 'string' ? d.value.length : 0), 0);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Builds cumulative vertical offsets for each line based on character count and column width.
|
|
298
|
+
* This allows accurate scroll position calculations with variable row heights.
|
|
299
|
+
*/
|
|
300
|
+
buildCumulativeOffsets(lineInformation, lineBlocks, blocks, expandedBlocks, showDiffOnly, charWidth, columnWidth, splitView) {
|
|
301
|
+
const offsets = [0];
|
|
302
|
+
const seenBlocks = new Set();
|
|
303
|
+
for (let i = 0; i < lineInformation.length; i++) {
|
|
304
|
+
const line = lineInformation[i];
|
|
305
|
+
if (showDiffOnly) {
|
|
306
|
+
const blockIndex = lineBlocks[i];
|
|
307
|
+
if (blockIndex !== undefined && !expandedBlocks.includes(blockIndex)) {
|
|
308
|
+
const isLastLine = blocks[blockIndex].endLine === i;
|
|
309
|
+
if (!seenBlocks.has(blockIndex) && isLastLine) {
|
|
310
|
+
seenBlocks.add(blockIndex);
|
|
311
|
+
offsets.push(offsets[offsets.length - 1] + DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
312
|
+
}
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Calculate visual rows for this line
|
|
317
|
+
const leftLen = line.left?.value ? this.getTextLength(line.left.value) : 0;
|
|
318
|
+
const rightLen = line.right?.value ? this.getTextLength(line.right.value) : 0;
|
|
319
|
+
const maxLen = splitView ? Math.max(leftLen, rightLen) : (leftLen || rightLen);
|
|
320
|
+
const charsPerRow = Math.floor(columnWidth / charWidth);
|
|
321
|
+
const visualRows = charsPerRow > 0 ? Math.max(1, Math.ceil(maxLen / charsPerRow)) : 1;
|
|
322
|
+
offsets.push(offsets[offsets.length - 1] + visualRows * DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
323
|
+
}
|
|
324
|
+
return offsets;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Binary search to find the line index at a given scroll offset.
|
|
328
|
+
*/
|
|
329
|
+
findLineAtOffset(scrollTop, offsets) {
|
|
330
|
+
let low = 0;
|
|
331
|
+
let high = offsets.length - 2;
|
|
332
|
+
while (low < high) {
|
|
333
|
+
const mid = Math.floor((low + high + 1) / 2);
|
|
334
|
+
if (offsets[mid] <= scrollTop) {
|
|
335
|
+
low = mid;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
high = mid - 1;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return low;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Recalculates cumulative offsets based on current measurements.
|
|
345
|
+
* Called on resize and when blocks are expanded/collapsed.
|
|
346
|
+
*/
|
|
347
|
+
recalculateOffsets = () => {
|
|
348
|
+
if (!this.props.infiniteLoading)
|
|
349
|
+
return;
|
|
350
|
+
const columnWidth = this.measureContentColumnWidth();
|
|
351
|
+
const charWidth = this.measureCharWidth();
|
|
352
|
+
if (!columnWidth)
|
|
353
|
+
return;
|
|
354
|
+
const cacheKey = this.getMemoisedKey();
|
|
355
|
+
const { lineInformation, lineBlocks, blocks } = this.state.computedDiffResult[cacheKey] ?? {};
|
|
356
|
+
if (!lineInformation)
|
|
357
|
+
return;
|
|
358
|
+
const offsets = this.buildCumulativeOffsets(lineInformation, lineBlocks, blocks, this.state.expandedBlocks, this.props.showDiffOnly, charWidth, columnWidth, this.props.splitView);
|
|
359
|
+
this.setState({ cumulativeOffsets: offsets, contentColumnWidth: columnWidth, charWidth }, () => {
|
|
360
|
+
// Force a scroll position update to recalculate visible rows with new offsets
|
|
361
|
+
this.onScroll();
|
|
232
362
|
});
|
|
233
363
|
};
|
|
234
364
|
/**
|
|
@@ -394,7 +524,7 @@ class DiffViewer extends React.Component {
|
|
|
394
524
|
[this.styles.diffRemoved]: removed,
|
|
395
525
|
[this.styles.diffChanged]: changed,
|
|
396
526
|
[this.styles.highlightedLine]: highlightLine,
|
|
397
|
-
}), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { className: cn(this.styles.content, {
|
|
527
|
+
}), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { ref: prefix === LineNumberPrefix.LEFT && !this.state.cumulativeOffsets ? this.contentColumnRef : undefined, className: cn(this.styles.content, {
|
|
398
528
|
[this.styles.emptyLine]: !content,
|
|
399
529
|
[this.styles.diffAdded]: added,
|
|
400
530
|
[this.styles.diffRemoved]: removed,
|
|
@@ -538,7 +668,13 @@ class DiffViewer extends React.Component {
|
|
|
538
668
|
...prev,
|
|
539
669
|
computedDiffResult: this.state.computedDiffResult,
|
|
540
670
|
isLoading: false,
|
|
541
|
-
}))
|
|
671
|
+
}), () => {
|
|
672
|
+
// Trigger offset recalculation after diff is computed and rendered
|
|
673
|
+
// Use requestAnimationFrame to ensure DOM is ready for measurement
|
|
674
|
+
if (this.props.infiniteLoading) {
|
|
675
|
+
requestAnimationFrame(() => this.recalculateOffsets());
|
|
676
|
+
}
|
|
677
|
+
});
|
|
542
678
|
};
|
|
543
679
|
// Estimated row height based on lineHeight: 1.6em with 12px base font
|
|
544
680
|
static ESTIMATED_ROW_HEIGHT = 19;
|
|
@@ -551,7 +687,13 @@ class DiffViewer extends React.Component {
|
|
|
551
687
|
const container = this.state.scrollableContainerRef.current;
|
|
552
688
|
if (!container || !this.props.infiniteLoading)
|
|
553
689
|
return;
|
|
554
|
-
|
|
690
|
+
// Account for sticky header height in scroll calculations
|
|
691
|
+
const headerHeight = this.getStickyHeaderHeight();
|
|
692
|
+
const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
|
|
693
|
+
const { cumulativeOffsets } = this.state;
|
|
694
|
+
const newStartRow = cumulativeOffsets
|
|
695
|
+
? this.findLineAtOffset(contentScrollTop, cumulativeOffsets)
|
|
696
|
+
: Math.floor(contentScrollTop / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
555
697
|
// Only update state if the start row changed (avoid unnecessary re-renders)
|
|
556
698
|
if (newStartRow !== this.state.visibleStartRow) {
|
|
557
699
|
this.setState({ visibleStartRow: newStartRow });
|
|
@@ -562,7 +704,7 @@ class DiffViewer extends React.Component {
|
|
|
562
704
|
*/
|
|
563
705
|
renderDiff = () => {
|
|
564
706
|
const { splitView, infiniteLoading, showDiffOnly } = this.props;
|
|
565
|
-
const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef } = this.state;
|
|
707
|
+
const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef, cumulativeOffsets } = this.state;
|
|
566
708
|
const cacheKey = this.getMemoisedKey();
|
|
567
709
|
const { lineInformation = [], lineBlocks = [], blocks = [] } = computedDiffResult[cacheKey] ?? {};
|
|
568
710
|
// Calculate visible range for virtualization
|
|
@@ -571,9 +713,32 @@ class DiffViewer extends React.Component {
|
|
|
571
713
|
const buffer = 5; // render extra rows above/below viewport
|
|
572
714
|
if (infiniteLoading && scrollableContainerRef.current) {
|
|
573
715
|
const container = scrollableContainerRef.current;
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
716
|
+
// Account for sticky header height in scroll calculations
|
|
717
|
+
const headerHeight = this.getStickyHeaderHeight();
|
|
718
|
+
const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
|
|
719
|
+
if (cumulativeOffsets) {
|
|
720
|
+
// Variable height mode: use binary search to find visible range
|
|
721
|
+
const totalHeight = cumulativeOffsets[cumulativeOffsets.length - 1] || 0;
|
|
722
|
+
const lastRowIndex = cumulativeOffsets.length - 2;
|
|
723
|
+
visibleRowStart = Math.max(0, this.findLineAtOffset(contentScrollTop, cumulativeOffsets) - buffer);
|
|
724
|
+
visibleRowEnd = this.findLineAtOffset(contentScrollTop + container.clientHeight, cumulativeOffsets) + buffer;
|
|
725
|
+
// IMPORTANT: The calculated offsets may overestimate row heights (based on char count),
|
|
726
|
+
// but actual CSS rendering might produce shorter rows. To prevent empty space,
|
|
727
|
+
// ensure we render at least enough rows to fill the viewport using ESTIMATED_ROW_HEIGHT
|
|
728
|
+
// as a conservative minimum.
|
|
729
|
+
const minRowsToFillViewport = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
730
|
+
visibleRowEnd = Math.max(visibleRowEnd, visibleRowStart + minRowsToFillViewport + buffer);
|
|
731
|
+
// Also ensure we render all rows when near the bottom
|
|
732
|
+
if (contentScrollTop + container.clientHeight >= totalHeight - buffer * DiffViewer.ESTIMATED_ROW_HEIGHT) {
|
|
733
|
+
visibleRowEnd = lastRowIndex + buffer;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
// Fixed height fallback
|
|
738
|
+
const viewportRows = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
739
|
+
visibleRowStart = Math.max(0, visibleStartRow - buffer);
|
|
740
|
+
visibleRowEnd = visibleStartRow + viewportRows + buffer;
|
|
741
|
+
}
|
|
577
742
|
}
|
|
578
743
|
// First pass: build a map of lineIndex -> renderedRowIndex
|
|
579
744
|
// This accounts for code folding where some lines don't render or render as fold indicators
|
|
@@ -611,6 +776,7 @@ class DiffViewer extends React.Component {
|
|
|
611
776
|
const diffNodes = [];
|
|
612
777
|
let topPadding = 0;
|
|
613
778
|
let firstVisibleFound = false;
|
|
779
|
+
let lastRenderedRowIndex = -1;
|
|
614
780
|
seenBlocks.clear();
|
|
615
781
|
for (let lineIndex = 0; lineIndex < lineInformation.length; lineIndex++) {
|
|
616
782
|
const line = lineInformation[lineIndex];
|
|
@@ -628,9 +794,13 @@ class DiffViewer extends React.Component {
|
|
|
628
794
|
}
|
|
629
795
|
// Calculate top padding from the first visible row
|
|
630
796
|
if (!firstVisibleFound) {
|
|
631
|
-
topPadding =
|
|
797
|
+
topPadding = cumulativeOffsets
|
|
798
|
+
? cumulativeOffsets[rowIndex] || 0
|
|
799
|
+
: rowIndex * DiffViewer.ESTIMATED_ROW_HEIGHT;
|
|
632
800
|
firstVisibleFound = true;
|
|
633
801
|
}
|
|
802
|
+
// Track the last rendered row for bottom padding calculation
|
|
803
|
+
lastRenderedRowIndex = rowIndex;
|
|
634
804
|
// Render the line
|
|
635
805
|
if (showDiffOnly) {
|
|
636
806
|
const blockIndex = lineBlocks[lineIndex];
|
|
@@ -650,12 +820,37 @@ class DiffViewer extends React.Component {
|
|
|
650
820
|
? this.renderSplitView(line, lineIndex)
|
|
651
821
|
: this.renderInlineView(line, lineIndex));
|
|
652
822
|
}
|
|
823
|
+
// Calculate total content height
|
|
824
|
+
const totalContentHeight = cumulativeOffsets
|
|
825
|
+
? cumulativeOffsets[cumulativeOffsets.length - 1] || 0
|
|
826
|
+
: totalRenderedRows * DiffViewer.ESTIMATED_ROW_HEIGHT;
|
|
827
|
+
// Calculate bottom padding: space after the last rendered row
|
|
828
|
+
const bottomPadding = cumulativeOffsets && lastRenderedRowIndex >= 0
|
|
829
|
+
? totalContentHeight - (cumulativeOffsets[lastRenderedRowIndex + 1] || totalContentHeight)
|
|
830
|
+
: 0;
|
|
653
831
|
return {
|
|
654
832
|
diffNodes,
|
|
655
833
|
blocks,
|
|
656
834
|
lineInformation,
|
|
657
835
|
totalRenderedRows,
|
|
658
836
|
topPadding,
|
|
837
|
+
bottomPadding,
|
|
838
|
+
totalContentHeight,
|
|
839
|
+
renderedCount: diffNodes.length,
|
|
840
|
+
// Debug info
|
|
841
|
+
debug: {
|
|
842
|
+
visibleRowStart,
|
|
843
|
+
visibleRowEnd,
|
|
844
|
+
totalRows: totalRenderedRows,
|
|
845
|
+
offsetsLength: cumulativeOffsets?.length ?? 0,
|
|
846
|
+
renderedCount: diffNodes.length,
|
|
847
|
+
scrollTop: scrollableContainerRef.current?.scrollTop ?? 0,
|
|
848
|
+
headerHeight: this.getStickyHeaderHeight(),
|
|
849
|
+
contentScrollTop: scrollableContainerRef.current
|
|
850
|
+
? Math.max(0, scrollableContainerRef.current.scrollTop - this.getStickyHeaderHeight())
|
|
851
|
+
: 0,
|
|
852
|
+
clientHeight: scrollableContainerRef.current?.clientHeight ?? 0,
|
|
853
|
+
}
|
|
659
854
|
};
|
|
660
855
|
};
|
|
661
856
|
componentDidUpdate(prevProps) {
|
|
@@ -669,7 +864,8 @@ class DiffViewer extends React.Component {
|
|
|
669
864
|
this.setState((prev) => ({
|
|
670
865
|
...prev,
|
|
671
866
|
isLoading: true,
|
|
672
|
-
visibleStartRow: 0
|
|
867
|
+
visibleStartRow: 0,
|
|
868
|
+
cumulativeOffsets: null,
|
|
673
869
|
}));
|
|
674
870
|
this.memoisedCompute();
|
|
675
871
|
}
|
|
@@ -680,6 +876,19 @@ class DiffViewer extends React.Component {
|
|
|
680
876
|
isLoading: true
|
|
681
877
|
}));
|
|
682
878
|
this.memoisedCompute();
|
|
879
|
+
// Set up ResizeObserver for recalculating offsets on container resize
|
|
880
|
+
if (typeof ResizeObserver !== 'undefined' && this.props.infiniteLoading) {
|
|
881
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
882
|
+
requestAnimationFrame(() => this.recalculateOffsets());
|
|
883
|
+
});
|
|
884
|
+
const container = this.state.scrollableContainerRef.current;
|
|
885
|
+
if (container) {
|
|
886
|
+
this.resizeObserver.observe(container);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
componentWillUnmount() {
|
|
891
|
+
this.resizeObserver?.disconnect();
|
|
683
892
|
}
|
|
684
893
|
render = () => {
|
|
685
894
|
const { oldValue, newValue, useDarkTheme, leftTitle, rightTitle, splitView, compareMethod, hideLineNumbers, nonce, } = this.props;
|
|
@@ -735,9 +944,12 @@ class DiffViewer extends React.Component {
|
|
|
735
944
|
overflowX: 'hidden',
|
|
736
945
|
height: this.props.infiniteLoading.containerHeight
|
|
737
946
|
} : {};
|
|
738
|
-
|
|
947
|
+
// Only apply noWrap when infiniteLoading is enabled but we don't have cumulative offsets yet
|
|
948
|
+
// Once offsets are calculated, we enable pre-wrap for proper text wrapping
|
|
949
|
+
const shouldNoWrap = !!this.props.infiniteLoading && !this.state.cumulativeOffsets;
|
|
739
950
|
const tableElement = (_jsxs("table", { className: cn(this.styles.diffContainer, {
|
|
740
951
|
[this.styles.splitView]: splitView,
|
|
952
|
+
[this.styles.noWrap]: shouldNoWrap,
|
|
741
953
|
}), onMouseUp: () => {
|
|
742
954
|
const elements = document.getElementsByClassName("right");
|
|
743
955
|
for (let i = 0; i < elements.length; i++) {
|
|
@@ -750,13 +962,37 @@ class DiffViewer extends React.Component {
|
|
|
750
962
|
element.classList.remove(this.styles.noSelect);
|
|
751
963
|
}
|
|
752
964
|
}, children: [_jsxs("colgroup", { children: [!this.props.hideLineNumbers && _jsx("col", { width: "50px" }), !splitView && !this.props.hideLineNumbers && _jsx("col", { width: "50px" }), this.props.renderGutter && _jsx("col", { width: "50px" }), _jsx("col", { width: "28px" }), _jsx("col", { width: "auto" }), splitView && (_jsxs(_Fragment, { children: [!this.props.hideLineNumbers && _jsx("col", { width: "50px" }), this.props.renderGutter && _jsx("col", { width: "50px" }), _jsx("col", { width: "28px" }), _jsx("col", { width: "auto" })] }))] }), _jsx("tbody", { children: nodes.diffNodes })] }));
|
|
753
|
-
return (_jsxs("div", { style: { ...scrollDivStyle, position: 'relative' }, onScroll: this.onScroll, ref: this.state.scrollableContainerRef, children: [(!this.props.hideSummary || leftTitle || rightTitle) && (_jsxs("div", { className: this.styles.stickyHeader, children: [!this.props.hideSummary && (_jsxs("div", { className: this.styles.summary, role: "banner", children: [_jsx("button", { type: "button", className: this.styles.allExpandButton, onClick: () => {
|
|
965
|
+
return (_jsxs("div", { style: { ...scrollDivStyle, position: 'relative' }, onScroll: this.onScroll, ref: this.state.scrollableContainerRef, children: [(!this.props.hideSummary || leftTitle || rightTitle) && (_jsxs("div", { ref: this.stickyHeaderRef, className: this.styles.stickyHeader, children: [!this.props.hideSummary && (_jsxs("div", { className: this.styles.summary, role: "banner", children: [_jsx("button", { type: "button", className: this.styles.allExpandButton, onClick: () => {
|
|
754
966
|
this.setState({
|
|
755
967
|
expandedBlocks: allExpanded
|
|
756
968
|
? []
|
|
757
969
|
: nodes.blocks.map((b) => b.index),
|
|
758
|
-
});
|
|
759
|
-
}, children: allExpanded ? _jsx(Fold, {}) : _jsx(Expand, {}) }), " ", totalChanges, _jsx("div", { style: { display: "flex", gap: "1px" }, children: blocks }), this.props.summary ? _jsx("span", { children: this.props.summary }) : null] })), (leftTitle || rightTitle) && (_jsxs("div", { className: this.styles.columnHeaders, children: [_jsx("div", { className: this.styles.titleBlock, children: leftTitle ? (_jsx("pre", { className: this.styles.contentText, children: leftTitle })) : null }), splitView && (_jsx("div", { className: this.styles.titleBlock, children: rightTitle ? (_jsx("pre", { className: this.styles.contentText, children: rightTitle })) : null }))] }))] })), this.state.isLoading && LoadingElement && _jsx(LoadingElement, {}), this.props.infiniteLoading ? (_jsx("div", { style: {
|
|
970
|
+
}, () => this.recalculateOffsets());
|
|
971
|
+
}, children: allExpanded ? _jsx(Fold, {}) : _jsx(Expand, {}) }), " ", totalChanges, _jsx("div", { style: { display: "flex", gap: "1px" }, children: blocks }), this.props.summary ? _jsx("span", { children: this.props.summary }) : null] })), (leftTitle || rightTitle) && (_jsxs("div", { className: this.styles.columnHeaders, children: [_jsx("div", { className: this.styles.titleBlock, children: leftTitle ? (_jsx("pre", { className: this.styles.contentText, children: leftTitle })) : null }), splitView && (_jsx("div", { className: this.styles.titleBlock, children: rightTitle ? (_jsx("pre", { className: this.styles.contentText, children: rightTitle })) : null }))] }))] })), this.state.isLoading && LoadingElement && _jsx(LoadingElement, {}), this.props.infiniteLoading ? (_jsx("div", { style: {
|
|
972
|
+
paddingTop: nodes.topPadding,
|
|
973
|
+
paddingBottom: nodes.bottomPadding,
|
|
974
|
+
}, children: tableElement })) : (tableElement), _jsx("span", { ref: this.charMeasureRef, style: {
|
|
975
|
+
position: 'absolute',
|
|
976
|
+
top: 0,
|
|
977
|
+
left: '-9999px',
|
|
978
|
+
visibility: 'hidden',
|
|
979
|
+
whiteSpace: 'pre',
|
|
980
|
+
fontFamily: 'monospace',
|
|
981
|
+
fontSize: 12,
|
|
982
|
+
}, "aria-hidden": "true", children: "M" }), this.props.infiniteLoading && this.props.showDebugInfo && (_jsxs("div", { style: {
|
|
983
|
+
position: 'fixed',
|
|
984
|
+
top: 10,
|
|
985
|
+
right: 10,
|
|
986
|
+
background: 'rgba(0,0,0,0.85)',
|
|
987
|
+
color: '#0f0',
|
|
988
|
+
padding: '10px',
|
|
989
|
+
fontFamily: 'monospace',
|
|
990
|
+
fontSize: '11px',
|
|
991
|
+
zIndex: 9999,
|
|
992
|
+
borderRadius: '4px',
|
|
993
|
+
maxWidth: '300px',
|
|
994
|
+
lineHeight: 1.4,
|
|
995
|
+
}, children: [_jsx("div", { style: { fontWeight: 'bold', marginBottom: '5px', color: '#fff' }, children: "Debug Info" }), _jsxs("div", { children: ["scrollTop: ", nodes.debug.scrollTop] }), _jsxs("div", { children: ["headerHeight: ", nodes.debug.headerHeight] }), _jsxs("div", { children: ["contentScrollTop: ", nodes.debug.contentScrollTop] }), _jsxs("div", { children: ["clientHeight: ", nodes.debug.clientHeight] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["visibleRowStart: ", nodes.debug.visibleRowStart] }), _jsxs("div", { children: ["visibleRowEnd: ", nodes.debug.visibleRowEnd] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["totalRows: ", nodes.debug.totalRows] }), _jsxs("div", { children: ["offsetsLength: ", nodes.debug.offsetsLength] }), _jsxs("div", { children: ["renderedCount: ", nodes.debug.renderedCount] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["topPadding: ", nodes.topPadding.toFixed(0)] }), _jsxs("div", { children: ["bottomPadding: ", nodes.bottomPadding.toFixed(0)] }), _jsxs("div", { children: ["totalContentHeight: ", nodes.totalContentHeight.toFixed(0)] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px', color: '#ff0' }, children: [_jsxs("div", { children: ["cumulativeOffsets: ", this.state.cumulativeOffsets ? 'SET' : 'NULL'] }), _jsxs("div", { children: ["columnWidth: ", this.state.contentColumnWidth?.toFixed(0) ?? 'N/A', "px"] }), _jsxs("div", { children: ["charWidth: ", this.state.charWidth?.toFixed(2) ?? 'N/A', "px"] }), _jsxs("div", { children: ["charsPerRow: ", this.state.contentColumnWidth && this.state.charWidth ? Math.floor(this.state.contentColumnWidth / this.state.charWidth) : 'N/A'] })] }), this.state.cumulativeOffsets && (_jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px', color: '#0ff', fontSize: '10px' }, children: [_jsxs("div", { children: ["offsets[", nodes.debug.visibleRowEnd, "]: ", this.state.cumulativeOffsets[nodes.debug.visibleRowEnd]?.toFixed(0) ?? 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows - 1, "]: ", this.state.cumulativeOffsets[nodes.debug.totalRows - 1]?.toFixed(0) ?? 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows, "]: ", this.state.cumulativeOffsets[nodes.debug.totalRows]?.toFixed(0) ?? 'N/A'] }), _jsxs("div", { style: { marginTop: '3px' }, children: ["viewportEnd: ", (nodes.debug.contentScrollTop + nodes.debug.clientHeight).toFixed(0)] }), _jsxs("div", { style: { marginTop: '3px', color: '#f0f' }, children: ["scrollHeight: ", this.state.scrollableContainerRef.current?.scrollHeight ?? 'N/A'] }), _jsxs("div", { children: ["maxScrollTop: ", (this.state.scrollableContainerRef.current?.scrollHeight ?? 0) - nodes.debug.clientHeight] })] }))] }))] }));
|
|
760
996
|
};
|
|
761
997
|
}
|
|
762
998
|
export default DiffViewer;
|
package/lib/esm/src/styles.js
CHANGED
|
@@ -67,6 +67,7 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
67
67
|
const { css, cx } = createEmotion({ key: "react-diff", nonce });
|
|
68
68
|
const content = css({
|
|
69
69
|
width: "auto",
|
|
70
|
+
overflow: "hidden",
|
|
70
71
|
label: "content",
|
|
71
72
|
});
|
|
72
73
|
const splitView = css({
|
|
@@ -108,6 +109,9 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
108
109
|
},
|
|
109
110
|
label: "diff-container",
|
|
110
111
|
borderCollapse: "collapse",
|
|
112
|
+
"@media (max-width: 768px)": {
|
|
113
|
+
minWidth: "unset",
|
|
114
|
+
},
|
|
111
115
|
});
|
|
112
116
|
const lineContent = css({
|
|
113
117
|
overflow: "hidden",
|
|
@@ -125,6 +129,16 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
125
129
|
userSelect: "none",
|
|
126
130
|
label: "unselectable",
|
|
127
131
|
});
|
|
132
|
+
const noWrap = css({
|
|
133
|
+
label: "no-wrap",
|
|
134
|
+
pre: {
|
|
135
|
+
whiteSpace: "pre",
|
|
136
|
+
},
|
|
137
|
+
[`.${contentText}`]: {
|
|
138
|
+
whiteSpace: "pre",
|
|
139
|
+
lineBreak: "auto",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
128
142
|
const allExpandButton = css({
|
|
129
143
|
background: "transparent",
|
|
130
144
|
border: "none",
|
|
@@ -371,6 +385,7 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
371
385
|
blockDeletion,
|
|
372
386
|
wordRemoved,
|
|
373
387
|
noSelect: unselectable,
|
|
388
|
+
noWrap,
|
|
374
389
|
codeFoldGutter,
|
|
375
390
|
codeFoldExpandButton,
|
|
376
391
|
codeFoldContentContainer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-diff-viewer-continued",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Continuation of a simple and beautiful text diff viewer component made with diff and React",
|
|
6
6
|
"keywords": [
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"node": ">= 16"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
|
-
"build": "tsc --project tsconfig.json && tsc --project tsconfig.esm.json",
|
|
68
|
+
"build": "tsc --project tsconfig.json && tsc --project tsconfig.esm.json && sed -i 's/computeWorker\\.ts/computeWorker.js/g' lib/cjs/src/compute-lines.js lib/esm/src/compute-lines.js",
|
|
69
69
|
"build:examples": "vite build examples",
|
|
70
70
|
"publish:examples": "NODE_ENV=production pnpm run build:examples && gh-pages -d examples/dist -r $GITHUB_REPO_URL",
|
|
71
71
|
"publish:examples:local": "NODE_ENV=production pnpm run build:examples && gh-pages -d examples/dist",
|