react-diff-viewer-continued 4.1.0 → 4.1.2
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 +8 -1
- package/.github/workflows/release.yml +3 -0
- package/CHANGELOG.md +13 -0
- package/lib/cjs/src/compute-lines.d.ts +0 -12
- package/lib/cjs/src/compute-lines.js +58 -9
- package/lib/cjs/src/index.d.ts +44 -0
- package/lib/cjs/src/index.js +270 -24
- package/lib/cjs/src/styles.d.ts +1 -0
- package/lib/cjs/src/styles.js +15 -0
- package/lib/cjs/src/workerBundle.d.ts +5 -0
- package/lib/cjs/src/workerBundle.js +7 -0
- package/lib/esm/src/compute-lines.js +58 -9
- package/lib/esm/src/index.js +265 -23
- package/lib/esm/src/styles.js +15 -0
- package/lib/esm/src/workerBundle.js +7 -0
- package/package.json +4 -2
- package/scripts/build-worker.js +50 -0
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
|
/**
|
|
@@ -256,10 +281,6 @@ class DiffViewer extends React.Component {
|
|
|
256
281
|
this.renderWordDiff = (diffArray, renderer) => {
|
|
257
282
|
var _a, _b;
|
|
258
283
|
const showHighlight = this.shouldHighlightWordDiff();
|
|
259
|
-
const { compareMethod } = this.props;
|
|
260
|
-
// Don't apply syntax highlighting for JSON/YAML - their word diffs are computed
|
|
261
|
-
// on-demand from raw strings and syntax highlighting creates messy fragmented tokens.
|
|
262
|
-
const skipSyntaxHighlighting = compareMethod === DiffMethod.JSON || compareMethod === DiffMethod.YAML;
|
|
263
284
|
// Reconstruct the full line from diff chunks
|
|
264
285
|
const fullLine = diffArray
|
|
265
286
|
.map((d) => (typeof d.value === "string" ? d.value : ""))
|
|
@@ -270,9 +291,9 @@ class DiffViewer extends React.Component {
|
|
|
270
291
|
if (fullLine.length > MAX_LINE_LENGTH) {
|
|
271
292
|
return [_jsx("span", { children: fullLine }, "long-line")];
|
|
272
293
|
}
|
|
273
|
-
// If we have a renderer
|
|
274
|
-
//
|
|
275
|
-
if (renderer
|
|
294
|
+
// If we have a renderer, try to highlight the full line first,
|
|
295
|
+
// then apply diff styling to preserve proper tokenization.
|
|
296
|
+
if (renderer) {
|
|
276
297
|
// Get the syntax-highlighted content
|
|
277
298
|
const highlighted = renderer(fullLine);
|
|
278
299
|
// Check if the renderer uses dangerouslySetInnerHTML (common with Prism, highlight.js, etc.)
|
|
@@ -379,7 +400,7 @@ class DiffViewer extends React.Component {
|
|
|
379
400
|
[this.styles.diffRemoved]: removed,
|
|
380
401
|
[this.styles.diffChanged]: changed,
|
|
381
402
|
[this.styles.highlightedLine]: highlightLine,
|
|
382
|
-
}), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { className: cn(this.styles.content, {
|
|
403
|
+
}), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { ref: prefix === LineNumberPrefix.LEFT && !this.state.cumulativeOffsets ? this.contentColumnRef : undefined, className: cn(this.styles.content, {
|
|
383
404
|
[this.styles.emptyLine]: !content,
|
|
384
405
|
[this.styles.diffAdded]: added,
|
|
385
406
|
[this.styles.diffRemoved]: removed,
|
|
@@ -517,7 +538,13 @@ class DiffViewer extends React.Component {
|
|
|
517
538
|
: Math.round(this.props.extraLinesSurroundingDiff);
|
|
518
539
|
const { lineBlocks, blocks } = computeHiddenBlocks(lineInformation, diffLines, extraLines);
|
|
519
540
|
this.state.computedDiffResult[cacheKey] = { lineInformation, lineBlocks, blocks };
|
|
520
|
-
this.setState((prev) => (Object.assign(Object.assign({}, prev), { computedDiffResult: this.state.computedDiffResult, isLoading: false })))
|
|
541
|
+
this.setState((prev) => (Object.assign(Object.assign({}, prev), { computedDiffResult: this.state.computedDiffResult, isLoading: false })), () => {
|
|
542
|
+
// Trigger offset recalculation after diff is computed and rendered
|
|
543
|
+
// Use requestAnimationFrame to ensure DOM is ready for measurement
|
|
544
|
+
if (this.props.infiniteLoading) {
|
|
545
|
+
requestAnimationFrame(() => this.recalculateOffsets());
|
|
546
|
+
}
|
|
547
|
+
});
|
|
521
548
|
});
|
|
522
549
|
/**
|
|
523
550
|
* Handles scroll events on the scrollable container.
|
|
@@ -528,7 +555,13 @@ class DiffViewer extends React.Component {
|
|
|
528
555
|
const container = this.state.scrollableContainerRef.current;
|
|
529
556
|
if (!container || !this.props.infiniteLoading)
|
|
530
557
|
return;
|
|
531
|
-
|
|
558
|
+
// Account for sticky header height in scroll calculations
|
|
559
|
+
const headerHeight = this.getStickyHeaderHeight();
|
|
560
|
+
const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
|
|
561
|
+
const { cumulativeOffsets } = this.state;
|
|
562
|
+
const newStartRow = cumulativeOffsets
|
|
563
|
+
? this.findLineAtOffset(contentScrollTop, cumulativeOffsets)
|
|
564
|
+
: Math.floor(contentScrollTop / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
532
565
|
// Only update state if the start row changed (avoid unnecessary re-renders)
|
|
533
566
|
if (newStartRow !== this.state.visibleStartRow) {
|
|
534
567
|
this.setState({ visibleStartRow: newStartRow });
|
|
@@ -538,9 +571,9 @@ class DiffViewer extends React.Component {
|
|
|
538
571
|
* Generates the entire diff view with virtualization support.
|
|
539
572
|
*/
|
|
540
573
|
this.renderDiff = () => {
|
|
541
|
-
var _a;
|
|
574
|
+
var _a, _b, _c, _d, _e, _f;
|
|
542
575
|
const { splitView, infiniteLoading, showDiffOnly } = this.props;
|
|
543
|
-
const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef } = this.state;
|
|
576
|
+
const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef, cumulativeOffsets } = this.state;
|
|
544
577
|
const cacheKey = this.getMemoisedKey();
|
|
545
578
|
const { lineInformation = [], lineBlocks = [], blocks = [] } = (_a = computedDiffResult[cacheKey]) !== null && _a !== void 0 ? _a : {};
|
|
546
579
|
// Calculate visible range for virtualization
|
|
@@ -549,9 +582,32 @@ class DiffViewer extends React.Component {
|
|
|
549
582
|
const buffer = 5; // render extra rows above/below viewport
|
|
550
583
|
if (infiniteLoading && scrollableContainerRef.current) {
|
|
551
584
|
const container = scrollableContainerRef.current;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
585
|
+
// Account for sticky header height in scroll calculations
|
|
586
|
+
const headerHeight = this.getStickyHeaderHeight();
|
|
587
|
+
const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
|
|
588
|
+
if (cumulativeOffsets) {
|
|
589
|
+
// Variable height mode: use binary search to find visible range
|
|
590
|
+
const totalHeight = cumulativeOffsets[cumulativeOffsets.length - 1] || 0;
|
|
591
|
+
const lastRowIndex = cumulativeOffsets.length - 2;
|
|
592
|
+
visibleRowStart = Math.max(0, this.findLineAtOffset(contentScrollTop, cumulativeOffsets) - buffer);
|
|
593
|
+
visibleRowEnd = this.findLineAtOffset(contentScrollTop + container.clientHeight, cumulativeOffsets) + buffer;
|
|
594
|
+
// IMPORTANT: The calculated offsets may overestimate row heights (based on char count),
|
|
595
|
+
// but actual CSS rendering might produce shorter rows. To prevent empty space,
|
|
596
|
+
// ensure we render at least enough rows to fill the viewport using ESTIMATED_ROW_HEIGHT
|
|
597
|
+
// as a conservative minimum.
|
|
598
|
+
const minRowsToFillViewport = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
599
|
+
visibleRowEnd = Math.max(visibleRowEnd, visibleRowStart + minRowsToFillViewport + buffer);
|
|
600
|
+
// Also ensure we render all rows when near the bottom
|
|
601
|
+
if (contentScrollTop + container.clientHeight >= totalHeight - buffer * DiffViewer.ESTIMATED_ROW_HEIGHT) {
|
|
602
|
+
visibleRowEnd = lastRowIndex + buffer;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
// Fixed height fallback
|
|
607
|
+
const viewportRows = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
608
|
+
visibleRowStart = Math.max(0, visibleStartRow - buffer);
|
|
609
|
+
visibleRowEnd = visibleStartRow + viewportRows + buffer;
|
|
610
|
+
}
|
|
555
611
|
}
|
|
556
612
|
// First pass: build a map of lineIndex -> renderedRowIndex
|
|
557
613
|
// This accounts for code folding where some lines don't render or render as fold indicators
|
|
@@ -589,6 +645,7 @@ class DiffViewer extends React.Component {
|
|
|
589
645
|
const diffNodes = [];
|
|
590
646
|
let topPadding = 0;
|
|
591
647
|
let firstVisibleFound = false;
|
|
648
|
+
let lastRenderedRowIndex = -1;
|
|
592
649
|
seenBlocks.clear();
|
|
593
650
|
for (let lineIndex = 0; lineIndex < lineInformation.length; lineIndex++) {
|
|
594
651
|
const line = lineInformation[lineIndex];
|
|
@@ -606,9 +663,13 @@ class DiffViewer extends React.Component {
|
|
|
606
663
|
}
|
|
607
664
|
// Calculate top padding from the first visible row
|
|
608
665
|
if (!firstVisibleFound) {
|
|
609
|
-
topPadding =
|
|
666
|
+
topPadding = cumulativeOffsets
|
|
667
|
+
? cumulativeOffsets[rowIndex] || 0
|
|
668
|
+
: rowIndex * DiffViewer.ESTIMATED_ROW_HEIGHT;
|
|
610
669
|
firstVisibleFound = true;
|
|
611
670
|
}
|
|
671
|
+
// Track the last rendered row for bottom padding calculation
|
|
672
|
+
lastRenderedRowIndex = rowIndex;
|
|
612
673
|
// Render the line
|
|
613
674
|
if (showDiffOnly) {
|
|
614
675
|
const blockIndex = lineBlocks[lineIndex];
|
|
@@ -628,15 +689,41 @@ class DiffViewer extends React.Component {
|
|
|
628
689
|
? this.renderSplitView(line, lineIndex)
|
|
629
690
|
: this.renderInlineView(line, lineIndex));
|
|
630
691
|
}
|
|
692
|
+
// Calculate total content height
|
|
693
|
+
const totalContentHeight = cumulativeOffsets
|
|
694
|
+
? cumulativeOffsets[cumulativeOffsets.length - 1] || 0
|
|
695
|
+
: totalRenderedRows * DiffViewer.ESTIMATED_ROW_HEIGHT;
|
|
696
|
+
// Calculate bottom padding: space after the last rendered row
|
|
697
|
+
const bottomPadding = cumulativeOffsets && lastRenderedRowIndex >= 0
|
|
698
|
+
? totalContentHeight - (cumulativeOffsets[lastRenderedRowIndex + 1] || totalContentHeight)
|
|
699
|
+
: 0;
|
|
631
700
|
return {
|
|
632
701
|
diffNodes,
|
|
633
702
|
blocks,
|
|
634
703
|
lineInformation,
|
|
635
704
|
totalRenderedRows,
|
|
636
705
|
topPadding,
|
|
706
|
+
bottomPadding,
|
|
707
|
+
totalContentHeight,
|
|
708
|
+
renderedCount: diffNodes.length,
|
|
709
|
+
// Debug info
|
|
710
|
+
debug: {
|
|
711
|
+
visibleRowStart,
|
|
712
|
+
visibleRowEnd,
|
|
713
|
+
totalRows: totalRenderedRows,
|
|
714
|
+
offsetsLength: (_b = cumulativeOffsets === null || cumulativeOffsets === void 0 ? void 0 : cumulativeOffsets.length) !== null && _b !== void 0 ? _b : 0,
|
|
715
|
+
renderedCount: diffNodes.length,
|
|
716
|
+
scrollTop: (_d = (_c = scrollableContainerRef.current) === null || _c === void 0 ? void 0 : _c.scrollTop) !== null && _d !== void 0 ? _d : 0,
|
|
717
|
+
headerHeight: this.getStickyHeaderHeight(),
|
|
718
|
+
contentScrollTop: scrollableContainerRef.current
|
|
719
|
+
? Math.max(0, scrollableContainerRef.current.scrollTop - this.getStickyHeaderHeight())
|
|
720
|
+
: 0,
|
|
721
|
+
clientHeight: (_f = (_e = scrollableContainerRef.current) === null || _e === void 0 ? void 0 : _e.clientHeight) !== null && _f !== void 0 ? _f : 0,
|
|
722
|
+
}
|
|
637
723
|
};
|
|
638
724
|
};
|
|
639
725
|
this.render = () => {
|
|
726
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
640
727
|
const { oldValue, newValue, useDarkTheme, leftTitle, rightTitle, splitView, compareMethod, hideLineNumbers, nonce, } = this.props;
|
|
641
728
|
if (typeof compareMethod === "string" &&
|
|
642
729
|
compareMethod !== DiffMethod.JSON) {
|
|
@@ -690,9 +777,12 @@ class DiffViewer extends React.Component {
|
|
|
690
777
|
overflowX: 'hidden',
|
|
691
778
|
height: this.props.infiniteLoading.containerHeight
|
|
692
779
|
} : {};
|
|
693
|
-
|
|
780
|
+
// Only apply noWrap when infiniteLoading is enabled but we don't have cumulative offsets yet
|
|
781
|
+
// Once offsets are calculated, we enable pre-wrap for proper text wrapping
|
|
782
|
+
const shouldNoWrap = !!this.props.infiniteLoading && !this.state.cumulativeOffsets;
|
|
694
783
|
const tableElement = (_jsxs("table", { className: cn(this.styles.diffContainer, {
|
|
695
784
|
[this.styles.splitView]: splitView,
|
|
785
|
+
[this.styles.noWrap]: shouldNoWrap,
|
|
696
786
|
}), onMouseUp: () => {
|
|
697
787
|
const elements = document.getElementsByClassName("right");
|
|
698
788
|
for (let i = 0; i < elements.length; i++) {
|
|
@@ -705,13 +795,42 @@ class DiffViewer extends React.Component {
|
|
|
705
795
|
element.classList.remove(this.styles.noSelect);
|
|
706
796
|
}
|
|
707
797
|
}, 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: () => {
|
|
798
|
+
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
799
|
this.setState({
|
|
710
800
|
expandedBlocks: allExpanded
|
|
711
801
|
? []
|
|
712
802
|
: 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: {
|
|
803
|
+
}, () => this.recalculateOffsets());
|
|
804
|
+
}, 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: {
|
|
805
|
+
height: nodes.totalContentHeight,
|
|
806
|
+
position: 'relative',
|
|
807
|
+
}, children: _jsx("div", { style: {
|
|
808
|
+
position: 'absolute',
|
|
809
|
+
top: nodes.topPadding,
|
|
810
|
+
left: 0,
|
|
811
|
+
right: 0,
|
|
812
|
+
}, children: tableElement }) })) : (tableElement), _jsx("span", { ref: this.charMeasureRef, style: {
|
|
813
|
+
position: 'absolute',
|
|
814
|
+
top: 0,
|
|
815
|
+
left: '-9999px',
|
|
816
|
+
visibility: 'hidden',
|
|
817
|
+
whiteSpace: 'pre',
|
|
818
|
+
fontFamily: 'monospace',
|
|
819
|
+
fontSize: 12,
|
|
820
|
+
}, "aria-hidden": "true", children: "M" }), this.props.infiniteLoading && this.props.showDebugInfo && (_jsxs("div", { style: {
|
|
821
|
+
position: 'fixed',
|
|
822
|
+
top: 10,
|
|
823
|
+
right: 10,
|
|
824
|
+
background: 'rgba(0,0,0,0.85)',
|
|
825
|
+
color: '#0f0',
|
|
826
|
+
padding: '10px',
|
|
827
|
+
fontFamily: 'monospace',
|
|
828
|
+
fontSize: '11px',
|
|
829
|
+
zIndex: 9999,
|
|
830
|
+
borderRadius: '4px',
|
|
831
|
+
maxWidth: '300px',
|
|
832
|
+
lineHeight: 1.4,
|
|
833
|
+
}, 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
834
|
};
|
|
716
835
|
this.state = {
|
|
717
836
|
expandedBlocks: [],
|
|
@@ -719,9 +838,117 @@ class DiffViewer extends React.Component {
|
|
|
719
838
|
scrollableContainerRef: React.createRef(),
|
|
720
839
|
computedDiffResult: {},
|
|
721
840
|
isLoading: false,
|
|
722
|
-
visibleStartRow: 0
|
|
841
|
+
visibleStartRow: 0,
|
|
842
|
+
contentColumnWidth: null,
|
|
843
|
+
charWidth: null,
|
|
844
|
+
cumulativeOffsets: null,
|
|
723
845
|
};
|
|
724
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Gets the height of the sticky header, if present.
|
|
849
|
+
*/
|
|
850
|
+
getStickyHeaderHeight() {
|
|
851
|
+
var _a;
|
|
852
|
+
return ((_a = this.stickyHeaderRef.current) === null || _a === void 0 ? void 0 : _a.offsetHeight) || 0;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Measures the width of a single character in the monospace font.
|
|
856
|
+
* Falls back to 7.2px if measurement fails.
|
|
857
|
+
*/
|
|
858
|
+
measureCharWidth() {
|
|
859
|
+
const span = this.charMeasureRef.current;
|
|
860
|
+
if (!span)
|
|
861
|
+
return 7.2; // fallback
|
|
862
|
+
return span.getBoundingClientRect().width || 7.2;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Measures the available width for content in a content column.
|
|
866
|
+
* Falls back to estimating from container width if direct measurement fails.
|
|
867
|
+
*/
|
|
868
|
+
measureContentColumnWidth() {
|
|
869
|
+
// Try direct measurement first
|
|
870
|
+
const cell = this.contentColumnRef.current;
|
|
871
|
+
if (cell && cell.clientWidth > 0) {
|
|
872
|
+
const style = window.getComputedStyle(cell);
|
|
873
|
+
const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
874
|
+
const width = cell.clientWidth - padding;
|
|
875
|
+
if (width > 0)
|
|
876
|
+
return width;
|
|
877
|
+
}
|
|
878
|
+
// Fallback: estimate from container width
|
|
879
|
+
// In split view: container has 2 content columns + gutters (50px each) + markers (28px each)
|
|
880
|
+
// In unified view: 1 content column + 2 gutters + 1 marker
|
|
881
|
+
const container = this.state.scrollableContainerRef.current;
|
|
882
|
+
if (!container || container.clientWidth <= 0)
|
|
883
|
+
return null;
|
|
884
|
+
const containerWidth = container.clientWidth;
|
|
885
|
+
const gutterWidth = this.props.hideLineNumbers ? 0 : 50;
|
|
886
|
+
const markerWidth = 28;
|
|
887
|
+
const gutterCount = this.props.splitView ? 2 : 2; // left gutter(s)
|
|
888
|
+
const markerCount = this.props.splitView ? 2 : 1;
|
|
889
|
+
const contentColumns = this.props.splitView ? 2 : 1;
|
|
890
|
+
const fixedWidth = gutterCount * gutterWidth + markerCount * markerWidth;
|
|
891
|
+
const availableWidth = containerWidth - fixedWidth;
|
|
892
|
+
return Math.max(100, availableWidth / contentColumns); // minimum 100px
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Gets the text length from a value that may be a string or DiffInformation array.
|
|
896
|
+
*/
|
|
897
|
+
getTextLength(value) {
|
|
898
|
+
if (!value)
|
|
899
|
+
return 0;
|
|
900
|
+
if (typeof value === 'string')
|
|
901
|
+
return value.length;
|
|
902
|
+
return value.reduce((sum, d) => sum + (typeof d.value === 'string' ? d.value.length : 0), 0);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Builds cumulative vertical offsets for each line based on character count and column width.
|
|
906
|
+
* This allows accurate scroll position calculations with variable row heights.
|
|
907
|
+
*/
|
|
908
|
+
buildCumulativeOffsets(lineInformation, lineBlocks, blocks, expandedBlocks, showDiffOnly, charWidth, columnWidth, splitView) {
|
|
909
|
+
var _a, _b;
|
|
910
|
+
const offsets = [0];
|
|
911
|
+
const seenBlocks = new Set();
|
|
912
|
+
for (let i = 0; i < lineInformation.length; i++) {
|
|
913
|
+
const line = lineInformation[i];
|
|
914
|
+
if (showDiffOnly) {
|
|
915
|
+
const blockIndex = lineBlocks[i];
|
|
916
|
+
if (blockIndex !== undefined && !expandedBlocks.includes(blockIndex)) {
|
|
917
|
+
const isLastLine = blocks[blockIndex].endLine === i;
|
|
918
|
+
if (!seenBlocks.has(blockIndex) && isLastLine) {
|
|
919
|
+
seenBlocks.add(blockIndex);
|
|
920
|
+
offsets.push(offsets[offsets.length - 1] + DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
921
|
+
}
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
// Calculate visual rows for this line
|
|
926
|
+
const leftLen = ((_a = line.left) === null || _a === void 0 ? void 0 : _a.value) ? this.getTextLength(line.left.value) : 0;
|
|
927
|
+
const rightLen = ((_b = line.right) === null || _b === void 0 ? void 0 : _b.value) ? this.getTextLength(line.right.value) : 0;
|
|
928
|
+
const maxLen = splitView ? Math.max(leftLen, rightLen) : (leftLen || rightLen);
|
|
929
|
+
const charsPerRow = Math.floor(columnWidth / charWidth);
|
|
930
|
+
const visualRows = charsPerRow > 0 ? Math.max(1, Math.ceil(maxLen / charsPerRow)) : 1;
|
|
931
|
+
offsets.push(offsets[offsets.length - 1] + visualRows * DiffViewer.ESTIMATED_ROW_HEIGHT);
|
|
932
|
+
}
|
|
933
|
+
return offsets;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Binary search to find the line index at a given scroll offset.
|
|
937
|
+
*/
|
|
938
|
+
findLineAtOffset(scrollTop, offsets) {
|
|
939
|
+
let low = 0;
|
|
940
|
+
let high = offsets.length - 2;
|
|
941
|
+
while (low < high) {
|
|
942
|
+
const mid = Math.floor((low + high + 1) / 2);
|
|
943
|
+
if (offsets[mid] <= scrollTop) {
|
|
944
|
+
low = mid;
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
high = mid - 1;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return low;
|
|
951
|
+
}
|
|
725
952
|
componentDidUpdate(prevProps) {
|
|
726
953
|
if (prevProps.oldValue !== this.props.oldValue ||
|
|
727
954
|
prevProps.newValue !== this.props.newValue ||
|
|
@@ -730,13 +957,32 @@ class DiffViewer extends React.Component {
|
|
|
730
957
|
prevProps.linesOffset !== this.props.linesOffset) {
|
|
731
958
|
// Clear word diff cache when diff changes
|
|
732
959
|
this.wordDiffCache.clear();
|
|
733
|
-
|
|
960
|
+
// Reset scroll position to top
|
|
961
|
+
const container = this.state.scrollableContainerRef.current;
|
|
962
|
+
if (container) {
|
|
963
|
+
container.scrollTop = 0;
|
|
964
|
+
}
|
|
965
|
+
this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true, visibleStartRow: 0, cumulativeOffsets: null })));
|
|
734
966
|
this.memoisedCompute();
|
|
735
967
|
}
|
|
736
968
|
}
|
|
737
969
|
componentDidMount() {
|
|
738
970
|
this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true })));
|
|
739
971
|
this.memoisedCompute();
|
|
972
|
+
// Set up ResizeObserver for recalculating offsets on container resize
|
|
973
|
+
if (typeof ResizeObserver !== 'undefined' && this.props.infiniteLoading) {
|
|
974
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
975
|
+
requestAnimationFrame(() => this.recalculateOffsets());
|
|
976
|
+
});
|
|
977
|
+
const container = this.state.scrollableContainerRef.current;
|
|
978
|
+
if (container) {
|
|
979
|
+
this.resizeObserver.observe(container);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
componentWillUnmount() {
|
|
984
|
+
var _a;
|
|
985
|
+
(_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
740
986
|
}
|
|
741
987
|
}
|
|
742
988
|
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,
|