Skip to content

Commit dc76e04

Browse files
authored
feat(CodeReview): 支持side-by-side模式 feat(CodeReview): 增加是否支持评论和展开折叠代码的参数 feat(CodeReview): 增加blob插槽&增加评论图标可点击区域和hover阴影 (#1658)
1 parent db5b3d4 commit dc76e04

File tree

11 files changed

+710
-236
lines changed

11 files changed

+710
-236
lines changed

packages/devui-vue/devui/code-review/src/code-review-types.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,28 @@ export const codeReviewProps = {
2525
type: Boolean,
2626
default: false,
2727
},
28+
allowComment: {
29+
type: Boolean,
30+
default: true,
31+
},
32+
allowExpand: {
33+
type: Boolean,
34+
default: true,
35+
},
36+
showBlob: {
37+
type: Boolean,
38+
default: false,
39+
},
2840
outputFormat: {
2941
type: String as PropType<OutputFormat>,
3042
default: 'line-by-line',
3143
},
3244
// 展开所有代码行的阈值,低于此阈值全部展开,高于此阈值分向上和向下两个操作展开
33-
expandAllThreshold: {
45+
expandThreshold: {
3446
type: Number,
3547
default: 50,
3648
},
37-
codeLoader: {
49+
expandLoader: {
3850
type: Function as PropType<(interval: Array<number | undefined>, update: (code: string) => void) => void>,
3951
},
4052
};

packages/devui-vue/devui/code-review/src/code-review.scss

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@
102102
text-align: center;
103103
}
104104

105+
&.comment-icon-hover {
106+
background-color: #bfcbf3 !important;
107+
}
108+
105109
.expand-icon {
106110
display: flex;
107111
align-items: center;
@@ -130,15 +134,12 @@
130134
line-height: 24px;
131135
}
132136

133-
.d2h-file-header {
134-
display: none;
135-
}
136-
137137
.d2h-file-wrapper {
138138
border: none;
139139
}
140140

141-
.d2h-code-linenumber:after {
141+
.d2h-code-linenumber::after,
142+
.d2h-code-side-linenumber::after {
142143
content: '';
143144
}
144145

@@ -150,14 +151,36 @@
150151
line-break: anywhere;
151152
}
152153

153-
.d2h-code-line {
154+
.d2h-code-line,
155+
.d2h-code-side-line {
154156
width: 100%;
155157
padding-left: 0;
156158
padding-right: 16px;
157159
}
158160

159-
&.hide-content {
160-
display: none;
161+
&.side-by-side {
162+
tr td:nth-of-type(3) {
163+
border-left: 1px solid #eeeeee;
164+
}
165+
166+
tr.comment-block {
167+
td:last-child {
168+
border-left: 1px solid #eeeeee;
169+
}
170+
171+
.comment-cell {
172+
vertical-align: top;
173+
}
174+
}
175+
176+
.d2h-file-side-diff {
177+
width: 100%;
178+
}
179+
180+
.d2h-code-side-linenumber {
181+
position: static;
182+
display: table-cell;
183+
}
161184
}
162185
}
163186

packages/devui-vue/devui/code-review/src/code-review.tsx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,8 @@ export default defineComponent({
1818
const ns = useNamespace('code-review');
1919
const { renderHtml, reviewContentRef, diffFile, onContentClick } = useCodeReview(props, ctx);
2020
const { isFold, toggleFold } = useCodeReviewFold(props, ctx);
21-
const {
22-
commentLeft,
23-
commentTop,
24-
onMouseEnter,
25-
onMouseMove,
26-
onMouseleave,
27-
onCommentMouseLeave,
28-
onCommentIconClick,
29-
insertComment,
30-
removeComment,
31-
} = useCodeReviewComment(reviewContentRef, ctx);
21+
const { commentLeft, commentTop, mouseEvent, onCommentMouseLeave, onCommentIconClick, insertComment, removeComment } =
22+
useCodeReviewComment(reviewContentRef, props, ctx);
3223

3324
onMounted(() => {
3425
ctx.emit('afterViewInit', { toggleFold, insertComment, removeComment });
@@ -39,21 +30,30 @@ export default defineComponent({
3930
return () => (
4031
<div class={ns.b()}>
4132
<CodeReviewHeader onClick={() => (isFold.value = !isFold.value)} />
42-
<div
43-
class={[ns.e('content'), { 'hide-content': isFold.value }]}
44-
v-html={renderHtml.value}
45-
ref={reviewContentRef}
46-
onClick={onContentClick}
47-
onMouseenter={onMouseEnter}
48-
onMousemove={onMouseMove}
49-
onMouseleave={onMouseleave}></div>
50-
<div
51-
class="comment-icon"
52-
style={{ left: commentLeft.value + 'px', top: commentTop.value + 'px' }}
53-
onClick={onCommentIconClick}
54-
onMouseleave={onCommentMouseLeave}>
55-
<CommentIcon />
33+
<div v-show={!isFold.value}>
34+
{props.showBlob ? (
35+
ctx.slots.blob?.()
36+
) : (
37+
<div
38+
class={[ns.e('content'), props.outputFormat]}
39+
v-html={renderHtml.value}
40+
ref={reviewContentRef}
41+
onClick={(e) => {
42+
onContentClick(e);
43+
onCommentIconClick(e);
44+
}}
45+
{...mouseEvent}></div>
46+
)}
5647
</div>
48+
{props.allowComment && (
49+
<div
50+
class="comment-icon"
51+
style={{ left: commentLeft.value + 'px', top: commentTop.value + 'px' }}
52+
onClick={onCommentIconClick}
53+
onMouseleave={onCommentMouseLeave}>
54+
<CommentIcon />
55+
</div>
56+
)}
5757
</div>
5858
);
5959
},

packages/devui-vue/devui/code-review/src/composables/use-code-review-comment.ts

Lines changed: 120 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,91 @@
1-
import { ref, onUnmounted } from 'vue';
1+
import { ref, toRefs, onUnmounted } from 'vue';
22
import type { SetupContext, Ref } from 'vue';
3-
import type { LineSide } from '../code-review-types';
3+
import type { LineSide, CodeReviewProps } from '../code-review-types';
44
import { useNamespace } from '../../../shared/hooks/use-namespace';
5-
import { notEmptyNode, addCommentToPage } from '../utils';
5+
import {
6+
notEmptyNode,
7+
addCommentToPageForSingleColumn,
8+
addCommentToPageForDoubleColumn,
9+
findReferenceDomForSingleColumn,
10+
findReferenceDomForDoubleColumn,
11+
} from '../utils';
612

7-
export function useCodeReviewComment(reviewContentRef: Ref<HTMLElement>, ctx: SetupContext) {
13+
export function useCodeReviewComment(reviewContentRef: Ref<HTMLElement>, props: CodeReviewProps, ctx: SetupContext) {
14+
const { outputFormat, allowComment } = toRefs(props);
815
const ns = useNamespace('code-review');
916
const commentLeft = ref(-100);
1017
const commentTop = ref(-100);
1118
let currentLeftLineNumber = -1;
1219
let currentRightLineNumber = -1;
13-
let currentHoverTr: HTMLElement;
14-
let containerRect: DOMRect;
20+
let lastLineNumberContainer: HTMLElement | null;
1521

1622
const resetLeftTop = () => {
1723
commentLeft.value = -100;
1824
commentTop.value = -100;
1925
currentLeftLineNumber = -1;
2026
currentRightLineNumber = -1;
21-
};
22-
23-
const onMouseEnter = (e: MouseEvent) => {
24-
containerRect = (e.currentTarget as HTMLElement).getBoundingClientRect();
27+
lastLineNumberContainer?.classList.remove('comment-icon-hover');
28+
lastLineNumberContainer = null;
2529
};
2630

2731
const onMouseMove = (e: MouseEvent) => {
2832
const composedPath = e.composedPath() as HTMLElement[];
2933
const trNode = composedPath.find((item) => item.tagName === 'TR');
3034
if (trNode) {
31-
const lineNumberContainer = Array.from(trNode.children)[0] as HTMLElement;
32-
if (notEmptyNode(lineNumberContainer)) {
33-
const { top, left } = lineNumberContainer.getBoundingClientRect();
34-
commentLeft.value = left;
35-
commentTop.value = top;
36-
currentLeftLineNumber = parseInt((lineNumberContainer.children[0] as HTMLElement)?.innerText) || -1;
37-
currentRightLineNumber = parseInt((lineNumberContainer.children[1] as HTMLElement)?.innerText) || -1;
38-
currentHoverTr = trNode;
35+
if (outputFormat.value === 'line-by-line') {
36+
const lineNumberContainer = Array.from(trNode.children)[0] as HTMLElement;
37+
if (lastLineNumberContainer !== lineNumberContainer) {
38+
lastLineNumberContainer?.classList.remove('comment-icon-hover');
39+
}
40+
if (notEmptyNode(lineNumberContainer)) {
41+
lastLineNumberContainer = lineNumberContainer;
42+
lineNumberContainer.classList.add('comment-icon-hover');
43+
const { top, left } = lineNumberContainer.getBoundingClientRect();
44+
commentLeft.value = left;
45+
commentTop.value = top;
46+
currentLeftLineNumber = parseInt((lineNumberContainer.children[0] as HTMLElement)?.innerText) || -1;
47+
currentRightLineNumber = parseInt((lineNumberContainer.children[1] as HTMLElement)?.innerText) || -1;
48+
} else {
49+
resetLeftTop();
50+
}
3951
} else {
40-
resetLeftTop();
52+
if (trNode.classList.contains('comment-block')) {
53+
return resetLeftTop();
54+
}
55+
const tdNode = composedPath.find((item) => item.tagName === 'TD');
56+
const tdIndex = Array.from(trNode.children).findIndex((item) => item === tdNode);
57+
const tdNodes = Array.from(trNode.children) as HTMLElement[];
58+
const leftLineNumberContainer = tdNodes[0];
59+
const rightLineNumberContainer = tdNodes[2];
60+
if (tdIndex < 2) {
61+
if (lastLineNumberContainer !== leftLineNumberContainer) {
62+
lastLineNumberContainer?.classList.remove('comment-icon-hover');
63+
}
64+
if (notEmptyNode(leftLineNumberContainer)) {
65+
lastLineNumberContainer = leftLineNumberContainer;
66+
leftLineNumberContainer.classList.add('comment-icon-hover');
67+
const { top, left } = leftLineNumberContainer.getBoundingClientRect();
68+
commentLeft.value = left;
69+
commentTop.value = top;
70+
currentLeftLineNumber = parseInt(leftLineNumberContainer.innerText);
71+
} else {
72+
resetLeftTop();
73+
}
74+
} else {
75+
if (lastLineNumberContainer !== rightLineNumberContainer) {
76+
lastLineNumberContainer?.classList.remove('comment-icon-hover');
77+
}
78+
if (rightLineNumberContainer && notEmptyNode(rightLineNumberContainer)) {
79+
lastLineNumberContainer = rightLineNumberContainer;
80+
rightLineNumberContainer.classList.add('comment-icon-hover');
81+
const { top, left } = rightLineNumberContainer.getBoundingClientRect();
82+
commentLeft.value = left;
83+
commentTop.value = top;
84+
currentRightLineNumber = parseInt(rightLineNumberContainer.innerText);
85+
} else {
86+
resetLeftTop();
87+
}
88+
}
4189
}
4290
}
4391
};
@@ -53,44 +101,70 @@ export function useCodeReviewComment(reviewContentRef: Ref<HTMLElement>, ctx: Se
53101
}
54102
};
55103

56-
const onCommentIconClick = () => {
57-
ctx.emit('addComment', { left: currentLeftLineNumber, right: currentRightLineNumber });
58-
};
59-
60-
const findReferenceDom = (lineNumber: number, lineSide: LineSide) => {
61-
const trNodes = Array.from(reviewContentRef.value.querySelectorAll('tr'));
62-
for (const index in trNodes) {
63-
const lineIndex = parseInt(index);
64-
const lineNumberBox = Array.from(trNodes[lineIndex].children)[0] as HTMLElement;
65-
if (notEmptyNode(lineNumberBox)) {
66-
const oldLineNumber = parseInt((lineNumberBox.children[0] as HTMLElement)?.innerText ?? -1);
67-
const newLineNumber = parseInt((lineNumberBox.children[1] as HTMLElement)?.innerText ?? -1);
68-
69-
if ((lineSide === 'left' && oldLineNumber === lineNumber) || (lineSide === 'right' && newLineNumber === lineNumber)) {
70-
return trNodes[lineIndex];
71-
}
104+
const onCommentIconClick = (e: Event) => {
105+
if (e) {
106+
const composedPath = e.composedPath() as HTMLElement[];
107+
const lineNumberBox = composedPath.find(
108+
(item) => item.classList?.contains('comment-icon-hover') || item.classList?.contains('comment-icon')
109+
);
110+
if (!lineNumberBox) {
111+
return;
72112
}
73113
}
114+
const emitObj: Partial<Record<'left' | 'right', number>> = {};
115+
if (outputFormat.value === 'line-by-line') {
116+
emitObj.left = currentLeftLineNumber;
117+
emitObj.right = currentRightLineNumber;
118+
} else if (currentLeftLineNumber !== -1) {
119+
emitObj.left = currentLeftLineNumber;
120+
} else {
121+
emitObj.right = currentRightLineNumber;
122+
}
123+
ctx.emit('addComment', { left: currentLeftLineNumber, right: currentRightLineNumber });
74124
};
75125

76126
const insertComment = (lineNumber: number, lineSide: LineSide, commentDom: HTMLElement) => {
77-
const lineHost = findReferenceDom(lineNumber, lineSide);
78-
lineHost && addCommentToPage(lineHost, commentDom, lineSide);
127+
if (outputFormat.value === 'line-by-line') {
128+
const lineHost = findReferenceDomForSingleColumn(reviewContentRef.value, lineNumber, lineSide);
129+
lineHost && addCommentToPageForSingleColumn(lineHost, commentDom, lineSide);
130+
} else {
131+
const lineHost = findReferenceDomForDoubleColumn(reviewContentRef.value, lineNumber, lineSide);
132+
lineHost && addCommentToPageForDoubleColumn(lineHost, commentDom, lineSide);
133+
}
79134
};
80135

81136
const removeComment = (lineNumber: number, lineSide: LineSide) => {
82-
const lineHost = findReferenceDom(lineNumber, lineSide);
83-
let nextLineHost = lineHost?.nextElementSibling;
84-
while (nextLineHost) {
85-
const classList = nextLineHost?.classList;
86-
if (classList?.contains('comment-block') && classList.contains(lineSide)) {
87-
nextLineHost.remove();
88-
return;
137+
if (outputFormat.value === 'line-by-line') {
138+
const lineHost = findReferenceDomForSingleColumn(reviewContentRef.value, lineNumber, lineSide);
139+
let nextLineHost = lineHost?.nextElementSibling;
140+
while (nextLineHost) {
141+
const classList = nextLineHost?.classList;
142+
if (classList?.contains('comment-block') && classList.contains(lineSide)) {
143+
nextLineHost.remove();
144+
return;
145+
}
146+
nextLineHost = nextLineHost.nextElementSibling;
147+
}
148+
} else {
149+
const lineHost = findReferenceDomForDoubleColumn(reviewContentRef.value, lineNumber, lineSide);
150+
const nextLineHost = lineHost?.nextElementSibling;
151+
if (nextLineHost && nextLineHost.classList.contains('comment-block')) {
152+
const leftChildren = nextLineHost.children[0];
153+
const rightChildren = nextLineHost.children[1];
154+
if (lineSide === 'left') {
155+
leftChildren.children[0].remove();
156+
} else {
157+
rightChildren.children[0].remove();
158+
}
159+
if (!leftChildren.children.length && !rightChildren.children.length) {
160+
nextLineHost.remove();
161+
}
89162
}
90-
nextLineHost = nextLineHost.nextElementSibling;
91163
}
92164
};
93165

166+
const mouseEvent = allowComment.value ? { onMousemove: onMouseMove, onMouseleave: onMouseleave } : {};
167+
94168
window.addEventListener('scroll', resetLeftTop);
95169

96170
onUnmounted(() => {
@@ -100,9 +174,7 @@ export function useCodeReviewComment(reviewContentRef: Ref<HTMLElement>, ctx: Se
100174
return {
101175
commentLeft,
102176
commentTop,
103-
onMouseEnter,
104-
onMouseMove,
105-
onMouseleave,
177+
mouseEvent,
106178
onCommentMouseLeave,
107179
onCommentIconClick,
108180
insertComment,

0 commit comments

Comments
 (0)