Skip to content

Commit

Permalink
feat(CodeReview): line-by-line模式支持评论功能 (#1645)
Browse files Browse the repository at this point in the history
  • Loading branch information
xingyan95 authored Aug 9, 2023
1 parent 71ac08d commit 8ff1946
Show file tree
Hide file tree
Showing 13 changed files with 706 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -422,4 +422,4 @@ export function useCodeEditor(props: CodeEditorProps, ctx: SetupContext) {
}

return { editorEl };
}
}
17 changes: 17 additions & 0 deletions packages/devui-vue/devui/code-review/src/code-review-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import type { ExtractPropTypes, InjectionKey, PropType, SetupContext, Ref } from
import type { DiffFile } from 'diff2html/lib/types';

export type OutputFormat = 'line-by-line' | 'side-by-side';
export type ExpandDirection = 'up' | 'down' | 'updown' | 'all';
export type LineSide = 'left' | 'right';
export interface CommentPosition {
left: number;
right: number;
}
export interface CodeReviewMethods {
toggleFold: (status?: boolean) => void;
insertComment: (lineNumber: number, lineSide: LineSide, commentDom: HTMLElement) => void;
removeComment: (lineNumber: number, lineSide: LineSide) => void;
}

export const codeReviewProps = {
diff: {
Expand All @@ -17,10 +28,16 @@ export const codeReviewProps = {
type: String as PropType<OutputFormat>,
default: 'line-by-line',
},
// 展开所有代码行的阈值,低于此阈值全部展开,高于此阈值分向上和向下两个操作展开
expandAllThreshold: {
type: Number,
default: 50,
},
};
export type CodeReviewProps = ExtractPropTypes<typeof codeReviewProps>;

export interface CodeReviewContext {
reviewContentRef: Ref<HTMLElement>;
diffInfo: DiffFile;
isFold: Ref<boolean>;
rootCtx: SetupContext;
Expand Down
39 changes: 39 additions & 0 deletions packages/devui-vue/devui/code-review/src/code-review.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@
}

&__content {
line-height: 20px;

table {
margin: 0;
}

tr {
border: none;
}
Expand All @@ -79,6 +85,16 @@
padding: 0;
}

td {
&.expand-icon-wrapper {
text-align: center;
}

.expand-icon {
display: inline-block;
}
}

.d2h-file-header {
display: none;
}
Expand All @@ -87,8 +103,31 @@
border: none;
}

.d2h-code-line-ctn {
word-break: break-all;
word-wrap: break-word !important;
white-space: break-spaces !important;
display: inline-block !important;
line-break: anywhere;
}

&.hide-content {
display: none;
}
}

.comment-icon {
position: fixed;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
background-color: $devui-base-bg;
transform: translateX(-50%);
box-shadow: 0 0 1px 1px rgba(37, 43, 58, 0.16);
cursor: pointer;
}
}
38 changes: 34 additions & 4 deletions packages/devui-vue/devui/code-review/src/code-review.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,54 @@
import { defineComponent } from 'vue';
import { defineComponent, onMounted } from 'vue';
import type { SetupContext } from 'vue';
import CodeReviewHeader from './components/code-review-header';
import { CommentIcon } from './components/code-review-icons';
import { codeReviewProps } from './code-review-types';
import type { CodeReviewProps } from './code-review-types';
import { useNamespace } from '../../shared/hooks/use-namespace';
import { useCodeReview } from './composables/use-code-review';
import { useCodeReviewComment } from './composables/use-code-review-comment';
import './code-review.scss';

export default defineComponent({
name: 'DCodeReview',
props: codeReviewProps,
emits: ['foldChange'],
emits: ['foldChange', 'addComment', 'afterViewInit'],
setup(props: CodeReviewProps, ctx: SetupContext) {
const ns = useNamespace('code-review');
const { renderHtml, isFold } = useCodeReview(props, ctx);
const { renderHtml, isFold, reviewContentRef, toggleFold } = useCodeReview(props, ctx);
const {
commentLeft,
commentTop,
onMouseEnter,
onMouseMove,
onMouseleave,
onCommentMouseLeave,
onCommentIconClick,
insertComment,
removeComment,
} = useCodeReviewComment(reviewContentRef, ctx);

onMounted(() => {
ctx.emit('afterViewInit', { toggleFold, insertComment, removeComment });
});

return () => (
<div class={ns.b()}>
<CodeReviewHeader onClick={() => (isFold.value = !isFold.value)} />
<div class={[ns.e('content'), { 'hide-content': isFold.value }]} v-html={renderHtml.value}></div>
<div
class={[ns.e('content'), { 'hide-content': isFold.value }]}
v-html={renderHtml.value}
ref={reviewContentRef}
onMouseenter={onMouseEnter}
onMousemove={onMouseMove}
onMouseleave={onMouseleave}></div>
<div
class="comment-icon"
style={{ left: commentLeft.value + 'px', top: commentTop.value + 'px' }}
onClick={onCommentIconClick}
onMouseleave={onCommentMouseLeave}>
<CommentIcon />
</div>
</div>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,83 @@ export function CopyIcon(): JSX.Element {
</svg>
);
}

export function CommentIcon(): JSX.Element {
return (
<svg width="12px" height="12px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path
d={`M14,1 C15.1045695,1 16,1.8954305 16,3 L16,11 C16,12.1045695 15.1045695,13 14,13 L11,13 L8,16 L5,13 L2,13
C0.8954305,13 0,12.1045695 0,11 L0,3 C0,1.8954305 0.8954305,1 2,1 L14,1 Z M14,3 L2,3 L2,11 L5,11 C5.47149598,11
5.92582641,11.1664898 6.28439337,11.4669131 L6.41421356,11.5857864 L8,13.171 L9.58578644,11.5857864
C9.91918444,11.2523884 10.3581707,11.0488544 10.8241472,11.0077406 L11,11 L14,11 L14,3 Z M8,6 C8.55228475,6
9,6.44771525 9,7 C9,7.55228475 8.55228475,8 8,8 C7.44771525,8 7,7.55228475 7,7 C7,6.44771525 7.44771525,6
8,6 Z M11,6 C11.5522847,6 12,6.44771525 12,7 C12,7.55228475 11.5522847,8 11,8 C10.4477153,8 10,7.55228475
10,7 C10,6.44771525 10.4477153,6 11,6 Z M5,6 C5.55228475,6 6,6.44771525 6,7 C6,7.55228475 5.55228475,8
5,8 C4.44771525,8 4,7.55228475 4,7 C4,6.44771525 4.44771525,6 5,6 Z`}
fill="#5e7ce0"
fill-rule="nonzero"></path>
</g>
</svg>
);
}

export function UpExpandIcon(): string {
return `<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-1365.000000,-11.000000)">
<g transform="translate(1365.000000,11.000000)">
<rect x="0" y="0" width="16" height="16"></rect>
<g
transform="translate(8.000000, 8.000000) scale(1, -1) translate(-8.000000, -8.000000) translate(1.000000, 4.000000)"
fill="#71757F">
<path
d='M0.5,0 L13.5,0 C13.7761424,0 14,0.223857625 14,0.5 C14,0.776142375 13.7761424,1 13.5,1 L0.5,1 C0.223857625,1
0,0.776142375 0,0.5 C0,0.223857625 0.223857625,0 0.5,0 Z'></path>
<polygon
transform="translate(7.000000, 5.5000000) scale(1, -1) translate(-7.000000, -5.5000000)"
points="7 3 10 8 4 8"></polygon>
</g>
</g>
</g>
</g>
</svg>`;
}

export function DownExpandIcon(): string {
return `<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-1344.000000,-11.000000)">
<g transform="translate(1344.000000,11.000000)">
<rect x="0" y="0" width="16" height="16"></rect>
<g transform="translate(1.000000, 5.000000)" fill="#71757F">
<path
d="M0.5,0 L13.5,0 C13.7761424,0 14,0.223857625 14,0.5 C14,0.776142375 13.7761424,1 13.5,1 L0.5,1 C0.223857625,1
0,0.776142375 0,0.5 C0,0.223857625 0.223857625,0 0.5,0 Z"></path>
<polygon
transform="translate(7.000000,5.500000) scale(1, -1) translate(-7.000000, -5.500000)"
points="7 3 10 8 4 8"></polygon>
</g>
</g>
</g>
</g>
</svg>`;
}

export function AllExpandIcon(): string {
return `<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-1301.000000,-11.000000)">
<rect x="1301" y="11" width="16" height="16"></rect>
<path
d="M1302.5,18 L1315.5,18 C1315.77614,18 1316,18.2238576 1316,18.5 1316,18.7761424 1315.77614,19 1315.5,19 L1302.5,19
C1302.22386,19 1302,18.7761424 1302,18.5 C1302,18.2238576 1302.22386,18 1302.5,18 Z" fill="#71757F"></path>
<polygon fill="#71757F" points="1309 11 1312 16 1306 16"></polygon>
<polygon
fill="#71757F"
transform="translate(1309.000000, 23.500000) scale(1, -1) translate(-1309.000000, -23.500000)"
points="1309 21 1312 26 1306 26"></polygon>
</g>
</g>
</svg>`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { ref } from 'vue';
import type { SetupContext, Ref } from 'vue';
import type { LineSide } from '../code-review-types';
import { useNamespace } from '../../../shared/hooks/use-namespace';
import { notEmptyNode, addCommentToPage } from '../utils';

export function useCodeReviewComment(reviewContentRef: Ref<HTMLElement>, ctx: SetupContext) {
const ns = useNamespace('code-review');
const commentLeft = ref(-100);
const commentTop = ref(-100);
let currentLeftLineNumber = -1;
let currentRightLineNumber = -1;
let currentHoverTr: HTMLElement;
let containerRect: DOMRect;

const resetLeftTop = () => {
commentLeft.value = -100;
commentTop.value = -100;
currentLeftLineNumber = -1;
currentRightLineNumber = -1;
};

const onMouseEnter = (e: MouseEvent) => {
containerRect = (e.currentTarget as HTMLElement).getBoundingClientRect();
};

const onMouseMove = (e: MouseEvent) => {
const composedPath = e.composedPath() as HTMLElement[];
const trNode = composedPath.find((item) => item.tagName === 'TR');
if (trNode) {
const lineNumberContainer = Array.from(trNode.children)[0] as HTMLElement;
if (notEmptyNode(lineNumberContainer)) {
const { top, left } = lineNumberContainer.getBoundingClientRect();
commentLeft.value = left;
commentTop.value = top;
currentLeftLineNumber = parseInt((lineNumberContainer.children[0] as HTMLElement)?.innerText) || -1;
currentRightLineNumber = parseInt((lineNumberContainer.children[1] as HTMLElement)?.innerText) || -1;
currentHoverTr = trNode;
} else {
resetLeftTop();
}
}
};
const onMouseleave = (e: MouseEvent) => {
if (!(e.relatedTarget as HTMLElement)?.classList.contains('comment-icon')) {
resetLeftTop();
}
};

const onCommentMouseLeave = (e: MouseEvent) => {
if (!(e.relatedTarget as HTMLElement)?.classList.contains(ns.e('content'))) {
resetLeftTop();
}
};

const onCommentIconClick = () => {
ctx.emit('addComment', { left: currentLeftLineNumber, right: currentRightLineNumber });
};

const findReferenceDom = (lineNumber: number, lineSide: LineSide) => {
const trNodes = Array.from(reviewContentRef.value.querySelectorAll('tr'));
for (const index in trNodes) {
const lineIndex = parseInt(index);
const lineNumberBox = Array.from(trNodes[lineIndex].children)[0] as HTMLElement;
if (notEmptyNode(lineNumberBox)) {
const oldLineNumber = parseInt((lineNumberBox.children[0] as HTMLElement)?.innerText ?? -1);
const newLineNumber = parseInt((lineNumberBox.children[1] as HTMLElement)?.innerText ?? -1);

if ((lineSide === 'left' && oldLineNumber === lineNumber) || (lineSide === 'right' && newLineNumber === lineNumber)) {
return trNodes[lineIndex];
}
}
}
};

const insertComment = (lineNumber: number, lineSide: LineSide, commentDom: HTMLElement) => {
const lineHost = findReferenceDom(lineNumber, lineSide);
lineHost && addCommentToPage(lineHost, commentDom, lineSide);
};

const removeComment = (lineNumber: number, lineSide: LineSide) => {
const lineHost = findReferenceDom(lineNumber, lineSide);
let nextLineHost = lineHost?.nextElementSibling;
while (nextLineHost) {
const classList = nextLineHost?.classList;
if (classList?.contains('comment-block') && classList.contains(lineSide)) {
nextLineHost.remove();
return;
}
nextLineHost = nextLineHost.nextElementSibling;
}
};

return {
commentLeft,
commentTop,
onMouseEnter,
onMouseMove,
onMouseleave,
onCommentMouseLeave,
onCommentIconClick,
insertComment,
removeComment,
};
}
Loading

0 comments on commit 8ff1946

Please sign in to comment.