diff --git a/packages/devui-vue/devui/code-review/src/code-review-types.ts b/packages/devui-vue/devui/code-review/src/code-review-types.ts index 9d9c9b1a19..d8eb92d2d1 100644 --- a/packages/devui-vue/devui/code-review/src/code-review-types.ts +++ b/packages/devui-vue/devui/code-review/src/code-review-types.ts @@ -10,6 +10,18 @@ export interface CommentPosition { left: number; right: number; } +export type ILineNumberTdMap = Record; +export interface IExpandLineNumberInfo { + nextL: string; + nextR: string; + prevL: string; + prevR: string; +} +export interface ICheckedLineDetails { + lefts: number[]; + rights: number[]; + codes: Record | string[]; +} export interface CodeReviewMethods { toggleFold: (status?: boolean) => void; insertComment: (lineNumber: number, lineSide: LineSide, commentDom: HTMLElement) => void; diff --git a/packages/devui-vue/devui/code-review/src/code-review.tsx b/packages/devui-vue/devui/code-review/src/code-review.tsx index c1ea435294..e6beb4678d 100644 --- a/packages/devui-vue/devui/code-review/src/code-review.tsx +++ b/packages/devui-vue/devui/code-review/src/code-review.tsx @@ -1,5 +1,5 @@ /* @jsxImportSource vue */ -import { defineComponent, onMounted, provide, toRefs } from 'vue'; +import { defineComponent, onMounted, provide, toRefs, ref } from 'vue'; import type { SetupContext } from 'vue'; import CodeReviewHeader from './components/code-review-header'; import { CommentIcon } from './components/code-review-icons'; @@ -18,8 +18,7 @@ export default defineComponent({ setup(props: CodeReviewProps, ctx: SetupContext) { const ns = useNamespace('code-review'); const { diffType } = toRefs(props); - const { renderHtml, reviewContentRef, diffFile, onContentClick } = useCodeReview(props, ctx); - const { isFold, toggleFold } = useCodeReviewFold(props, ctx); + const reviewContentRef = ref(); const { commentLeft, commentTop, @@ -28,16 +27,18 @@ export default defineComponent({ onCommentIconClick, insertComment, removeComment, - updateCheckedLineClass, clearCheckedLines, + updateLineNumberMap, + updateCheckedLine, } = useCodeReviewComment(reviewContentRef, props, ctx); + const { renderHtml, diffFile, onContentClick } = useCodeReview(props, ctx, reviewContentRef, updateLineNumberMap, updateCheckedLine); + const { isFold, toggleFold } = useCodeReviewFold(props, ctx); onMounted(() => { ctx.emit('afterViewInit', { toggleFold, insertComment, removeComment, - updateCheckedLineClass, clearCheckedLines, }); }); diff --git a/packages/devui-vue/devui/code-review/src/composables/use-code-review-comment.ts b/packages/devui-vue/devui/code-review/src/composables/use-code-review-comment.ts index 7874c30d08..e7eca463f8 100644 --- a/packages/devui-vue/devui/code-review/src/composables/use-code-review-comment.ts +++ b/packages/devui-vue/devui/code-review/src/composables/use-code-review-comment.ts @@ -1,7 +1,7 @@ -import { ref, toRefs, onUnmounted, watch } from 'vue'; +import { ref, toRefs, onUnmounted } from 'vue'; import type { SetupContext, Ref } from 'vue'; import { useCodeReviewLineSelection } from './use-code-review-line-selection'; -import type { LineSide, CodeReviewProps } from '../code-review-types'; +import type { LineSide, CodeReviewProps, ICheckedLineDetails } from '../code-review-types'; import { useNamespace } from '../../../shared/hooks/use-namespace'; import { notEmptyNode, @@ -14,28 +14,18 @@ import { export function useCodeReviewComment(reviewContentRef: Ref, props: CodeReviewProps, ctx: SetupContext) { const { outputFormat, allowComment, allowChecked } = toRefs(props); const ns = useNamespace('code-review'); - const { onMousedown } = useCodeReviewLineSelection(reviewContentRef, props, afterMouseup); + const { onMousedown, updateLineNumberMap, getCheckedLineDetails, clearCommentClass, updateCheckedLine } = useCodeReviewLineSelection( + reviewContentRef, + props, + afterMouseup + ); const commentLeft = ref(-100); const commentTop = ref(-100); let currentLeftLineNumber = -1; let currentRightLineNumber = -1; + let currentPosition: 'left' | 'right'; let lastLineNumberContainer: HTMLElement | null; - let checkedLineNumberContainer: Array = []; - let currentLeftLineNumbers: Array = []; - let currentRightLineNumbers: Array = []; - let checkedLineCodeString: Array | Record> = {}; - let allTrNodes: NodeListOf = []; - let afterCheckLinesEmitData: Record; - watch( - () => outputFormat.value, - () => { - // 如果出现单栏双栏切换则需要重置选中 - checkedLineNumberContainer = []; - currentLeftLineNumbers = []; - currentRightLineNumbers = []; - checkedLineCodeString = []; - } - ); + const resetLeftTop = () => { commentLeft.value = -100; commentTop.value = -100; @@ -85,6 +75,8 @@ export function useCodeReviewComment(reviewContentRef: Ref, props: commentLeft.value = left; commentTop.value = top; currentLeftLineNumber = parseInt(leftLineNumberContainer.innerText); + currentRightLineNumber = parseInt(rightLineNumberContainer.innerText || '-1'); + currentPosition = 'left'; } else { resetLeftTop(); } @@ -98,7 +90,9 @@ export function useCodeReviewComment(reviewContentRef: Ref, props: const { top, left } = rightLineNumberContainer.getBoundingClientRect(); commentLeft.value = left; commentTop.value = top; + currentLeftLineNumber = parseInt(leftLineNumberContainer.innerText || '-1'); currentRightLineNumber = parseInt(rightLineNumberContainer.innerText); + currentPosition = 'right'; } else { resetLeftTop(); } @@ -117,150 +111,27 @@ export function useCodeReviewComment(reviewContentRef: Ref, props: resetLeftTop(); } }; - // 获取一些公共类和判断 - const getCommonClassAndJudge = () => { - const checkedLine = [currentLeftLineNumbers, currentRightLineNumbers]; - return { - linenumberDom: allTrNodes, - checkedLine, - }; - }; - // 之前每次都先移出所有选中的方法过于浪费性能,增加具体dom节点选中方法(防重复添加) - const addCommentCheckedClass = (Dom: Element) => { - !Dom.classList.contains('comment-checked') && Dom.classList.add('comment-checked'); - }; - // 单栏 - function getSingleCheckedLineCode(shouldRenderClass: boolean) { - const { linenumberDom, checkedLine } = getCommonClassAndJudge(); - const checkedCodeContent = []; - for (let i = 0; i < linenumberDom.length; i++) { - const lineNumberDomLeft = linenumberDom[i].children[0]; - const lineNumberDomRight = linenumberDom[i].children[1]; - if (lineNumberDomLeft || lineNumberDomRight) { - const codeLineNumberLeft = parseInt((lineNumberDomLeft as HTMLElement)?.innerText); - const codeLineNumberRight = parseInt((lineNumberDomRight as HTMLElement)?.innerText); - // 因为存在左边或者右边为空的num所以两边都要循环,但是同一个dom已经过就不需要再赋予 - if (checkedLine[0].includes(codeLineNumberLeft) || checkedLine[1].includes(codeLineNumberRight)) { - checkedLineNumberContainer.push(linenumberDom[i]); - // 两个节点之间可能间隔文本节点 - const codeNode = linenumberDom[i].nextElementSibling as HTMLElement; - checkedCodeContent.push(codeNode?.innerText); - if (shouldRenderClass) { - addCommentCheckedClass(linenumberDom[i]); - addCommentCheckedClass(codeNode); - } - } - } - } - checkedLineCodeString = checkedCodeContent; - } - // 双栏 - function getDoubleCheckedLineCode(shouldRenderClass: boolean) { - const { linenumberDom, checkedLine } = getCommonClassAndJudge(); - const checkedCodeContentLeft = []; - const checkedCodeContentRight = []; - - function checkedFunc(Dom: Element) { - checkedLineNumberContainer.push(Dom); - const codeNode = Dom.nextElementSibling as HTMLElement; - if (shouldRenderClass) { - addCommentCheckedClass(Dom); - addCommentCheckedClass(codeNode); - } - return codeNode?.innerText; - } - for (let i = 0; i < linenumberDom.length; i++) { - // 左右双栏一起遍历 - const codeLineNumber = parseInt(linenumberDom[i]?.innerHTML); - if (linenumberDom[i].classList.contains('d-code-left') && checkedLine[0].includes(codeLineNumber)) { - const lineNumText = checkedFunc(linenumberDom[i]); - checkedCodeContentLeft.push(lineNumText); - continue; - } - if (linenumberDom[i].classList.contains('d-code-right') && checkedLine[1].includes(codeLineNumber)) { - const lineNumText = checkedFunc(linenumberDom[i]); - checkedCodeContentRight.push(lineNumText); - } - } - checkedLineCodeString = { leftCode: checkedCodeContentLeft, rightCode: checkedCodeContentRight }; - } - function getCheckedLineCode(shouldRenderClass: boolean) { - if (props.outputFormat === 'line-by-line') { - return getSingleCheckedLineCode(shouldRenderClass); - } - getDoubleCheckedLineCode(shouldRenderClass); - } - function updateLineNumbers({ lefts, rights }: { lefts: number[]; rights: number[] }) { - currentLeftLineNumbers = lefts; - currentRightLineNumbers = rights; - getCheckedLineCode(false); - afterCheckLinesEmitData = { - left: currentLeftLineNumber, - right: currentRightLineNumber, - details: { - lefts: currentLeftLineNumbers, - rights: currentRightLineNumbers, - codes: checkedLineCodeString, - }, - }; - } - const updateCheckedLineClass = () => { - const lineClassName = props.outputFormat === 'line-by-line' ? '.d2h-code-linenumber' : '.d2h-code-side-linenumber'; - allTrNodes = reviewContentRef.value.querySelectorAll(lineClassName); - getCheckedLineCode(true); - }; - // 还原样式 - const resetCommentClass = () => { - for (let i = 0; i < checkedLineNumberContainer.length; i++) { - checkedLineNumberContainer[i].classList.remove('comment-checked'); - const codeNode = checkedLineNumberContainer[i].nextElementSibling; - (codeNode as HTMLElement)?.classList.remove('comment-checked'); - } - checkedLineNumberContainer = []; - }; // 点击 const commentClick = () => { - interface recordType { - left: number; - right: number; - details?: { - lefts: Array; - rights: Array; - codes: Record> | Record>; - }; - } - let obj: recordType = { left: currentLeftLineNumber, right: currentRightLineNumber }; - if ((currentLeftLineNumbers.length >= 1 || currentRightLineNumbers.length >= 1) && allowChecked.value) { - // 选中模式 - const maxCurrentLeftLineNumber = currentLeftLineNumbers[currentLeftLineNumbers.length - 1]; - const maxCurrentRightLineNumber = currentRightLineNumbers[currentRightLineNumbers.length - 1]; - if (maxCurrentLeftLineNumber === currentLeftLineNumber || maxCurrentRightLineNumber === currentRightLineNumber) { - // 点击添加评论图标触发的事件 - obj = { - left: currentLeftLineNumber, - right: currentRightLineNumber, - details: { - lefts: currentLeftLineNumbers, - rights: currentRightLineNumbers, - codes: checkedLineCodeString, - }, - }; + let obj = { left: currentLeftLineNumber, right: currentRightLineNumber, position: currentPosition }; + const checkedLineDetails = getCheckedLineDetails(); + // 多行选中 + if (checkedLineDetails && allowChecked.value) { + const { lefts, rights } = checkedLineDetails; + const maxCheckedLeftLineNumber = lefts[lefts.length - 1]; + const maxCheckedRightLineNumber = rights[rights.length - 1]; + if (maxCheckedLeftLineNumber === currentLeftLineNumber || maxCheckedRightLineNumber === currentRightLineNumber) { + obj.details = checkedLineDetails; } else { - currentLeftLineNumbers = []; - currentRightLineNumbers = []; - resetCommentClass(); + clearCommentClass(); } } // 点击添加评论图标触发的事件 ctx.emit('addComment', obj); }; - function afterCheckLines() { - ctx.emit('afterCheckLines', afterCheckLinesEmitData); - } - function afterMouseup(lineNumbers: { lefts: number[]; rights: number[] }) { - updateLineNumbers(lineNumbers); - afterCheckLines(); + function afterMouseup(details: ICheckedLineDetails) { + ctx.emit('afterCheckLines', { left: currentLeftLineNumber, right: currentRightLineNumber, position: currentPosition, details }); } // 图标或者单行的点击 const onCommentIconClick = (e: Event) => { @@ -317,16 +188,7 @@ export function useCodeReviewComment(reviewContentRef: Ref, props: }; const clearCheckedLines = () => { - currentLeftLineNumbers = []; - currentRightLineNumbers = []; - checkedLineCodeString = []; - resetCommentClass(); - }; - - const handleMouseDown = (e: MouseEvent) => { - const lineClassName = props.outputFormat === 'line-by-line' ? '.d2h-code-linenumber' : '.d2h-code-side-linenumber'; - allTrNodes = reviewContentRef.value.querySelectorAll(lineClassName); - onMousedown(e); + clearCommentClass(); }; const mouseEvent: Record void> = {}; @@ -335,7 +197,7 @@ export function useCodeReviewComment(reviewContentRef: Ref, props: mouseEvent.onMouseleave = onMouseleave; } if (props.allowChecked) { - mouseEvent.onMousedown = handleMouseDown; + mouseEvent.onMousedown = onMousedown; } window.addEventListener('scroll', resetLeftTop); @@ -348,11 +210,12 @@ export function useCodeReviewComment(reviewContentRef: Ref, props: commentLeft, commentTop, mouseEvent, - updateCheckedLineClass, clearCheckedLines, onCommentMouseLeave, onCommentIconClick, insertComment, removeComment, + updateLineNumberMap, + updateCheckedLine, }; } diff --git a/packages/devui-vue/devui/code-review/src/composables/use-code-review-expand.ts b/packages/devui-vue/devui/code-review/src/composables/use-code-review-expand.ts index 6f73409d49..d77d140947 100644 --- a/packages/devui-vue/devui/code-review/src/composables/use-code-review-expand.ts +++ b/packages/devui-vue/devui/code-review/src/composables/use-code-review-expand.ts @@ -1,6 +1,6 @@ import { toRefs } from 'vue'; import type { Ref } from 'vue'; -import type { CodeReviewProps, ExpandDirection } from '../code-review-types'; +import type { CodeReviewProps, ExpandDirection, IExpandLineNumberInfo } from '../code-review-types'; import { ExpandLineReg, FirstLineReg } from '../const'; import { attachExpandUpDownButton, @@ -14,7 +14,12 @@ import { ifRemoveExpandLineForDoubleColumn, } from '../utils'; -export function useCodeReviewExpand(reviewContentRef: Ref, props: CodeReviewProps) { +export function useCodeReviewExpand( + reviewContentRef: Ref, + props: CodeReviewProps, + updateLineNumberMap: (expandLineNumberInfo: IExpandLineNumberInfo, newCode: string, direction: 'up' | 'down') => void, + updateCheckedLine: (expandLineNumberInfo: IExpandLineNumberInfo, direction: 'up' | 'down') => void +) { const { outputFormat, expandThreshold, expandLoader } = toRefs(props); const processSideBySide = () => { @@ -85,8 +90,12 @@ export function useCodeReviewExpand(reviewContentRef: Ref, props: C // 过滤有效行 const trNodesToBeInserted = trNodes.filter((element) => element !== expandLine); + /* 更新左右行号映射关系 */ + updateLineNumberMap(referenceDom.dataset as unknown as IExpandLineNumberInfo, prefix + code, direction); // 将有效代码行插入页面 insertIncrementLineToPage(referenceDom, trNodesToBeInserted, direction); + /* 若新增行在选中区间,则将新增行高亮 */ + updateCheckedLine(referenceDom.dataset as unknown as IExpandLineNumberInfo, direction); // 判断是否需要移除展开行,代码若已全部展开,不再需要展开行 const removedExpandLine = ifRemoveExpandLineForDoubleColumn(referenceDom, expandLine, direction); @@ -192,6 +201,8 @@ export function useCodeReviewExpand(reviewContentRef: Ref, props: C const trNodesToBeInserted = trNodes.filter((element) => element.children[0].children.length === 2); // 将有效代码行插入页面 insertIncrementLineToPage(referenceDom, trNodesToBeInserted, direction); + /* 若新增行在选中区间,则将新增行高亮 */ + updateCheckedLine(referenceDom.dataset as unknown as IExpandLineNumberInfo, direction); // 判断是否需要移除展开行,代码若已全部展开,不再需要展开行 const removedExpandLine = ifRemoveExpandLine(referenceDom, expandLine, direction); diff --git a/packages/devui-vue/devui/code-review/src/composables/use-code-review-line-selection.ts b/packages/devui-vue/devui/code-review/src/composables/use-code-review-line-selection.ts index 59448cee26..f9edd1df81 100644 --- a/packages/devui-vue/devui/code-review/src/composables/use-code-review-line-selection.ts +++ b/packages/devui-vue/devui/code-review/src/composables/use-code-review-line-selection.ts @@ -1,21 +1,40 @@ +import { watch } from 'vue'; import type { Ref } from 'vue'; -import type { CodeReviewProps } from '../code-review-types'; +import type { CodeReviewProps, CommentPosition, ICheckedLineDetails, IExpandLineNumberInfo, ILineNumberTdMap } from '../code-review-types'; import { useNamespace } from '../../../shared/hooks/use-namespace'; -import { findParentTrNode, getLineNumbers } from '../utils'; +import { + findParentTrNode, + clearCommentChecked, + parseCodeToSingle, + getLineNumberMap, + getLineNumberTdMap, + getDoubleCheckedNumberAndCodes, + getSingleCheckedNumberAndCode, + addCommentCheckedForDouble, + addCommentCheckedForSingle, +} from '../utils'; export function useCodeReviewLineSelection( reviewContentRef: Ref, props: CodeReviewProps, - afterMouseup: (lineNumbers: { lefts: number[]; rights: number[] }) => void + afterMouseup: (details: ICheckedLineDetails) => void ) { const ns = useNamespace('code-review'); let dragging = false; let startTrNode: HTMLElement; let trNodes: HTMLElement[]; - let isClickedLeft: boolean | undefined; + let allTdNodes: HTMLElement[] = []; let shouldClear: boolean; let isMouseMoved: boolean; - let checkedTrNodes: HTMLElement[] = []; + let leftRightLineNumberArr: CommentPosition[] = []; + let leftNumberTdMap: ILineNumberTdMap = {}; + let rightNumberTdMap: ILineNumberTdMap = {}; + let checkedTdNodes: HTMLElement[] = []; + let startPosition: 'left' | 'right'; + let leftMinNum: number; + let leftMaxNum: number; + let rightMinNum: number; + let rightMaxNum: number; const onMousedown = (e: MouseEvent) => { // 鼠标左键按下 @@ -31,14 +50,19 @@ export function useCodeReviewLineSelection( } const parentTrNode = findParentTrNode(e.target as HTMLElement); // 判断点击的是否是展开图标 - if (parentTrNode && parentTrNode?.classList.contains('expand-line')) { + if (parentTrNode && parentTrNode?.classList?.contains('expand-line')) { return; } startTrNode = parentTrNode as HTMLElement; + allTdNodes = []; + for (let i = 0; i < trNodes.length; i++) { + allTdNodes.push(...trNodes[i].children); + } if (props.outputFormat === 'side-by-side') { - isClickedLeft = composedPath.some((item) => item.classList?.contains('d-code-left')); - } else { - isClickedLeft = undefined; + const { left, right } = getLineNumberTdMap(trNodes); + leftNumberTdMap = left; + rightNumberTdMap = right; + startPosition = composedPath.some((item) => item.classList?.contains('d-code-left')) ? 'left' : 'right'; } dragging = true; @@ -55,9 +79,8 @@ export function useCodeReviewLineSelection( if (!dragging) { return; } - isMouseMoved = true; if (shouldClear) { - clearCommentChecked(); + clearCommentChecked(checkedTdNodes); shouldClear = false; } const composedPath = e.composedPath() as HTMLElement[]; @@ -66,33 +89,72 @@ export function useCodeReviewLineSelection( return; } const endTrNode = findParentTrNode(e.target as HTMLElement); + let endPosition: 'left' | 'right'; + if (props.outputFormat === 'side-by-side') { + if (composedPath.some((item) => item.classList?.contains('d-code-left'))) { + endPosition = 'left'; + } + if (composedPath.some((item) => item.classList?.contains('d-code-right'))) { + endPosition = 'right'; + } + } if (!endTrNode) { return; } - let startIndex = trNodes.indexOf(startTrNode); - let endIndex = trNodes.indexOf(endTrNode); - if (endIndex === -1) { + isMouseMoved = true; + const endTrChildren = endTrNode.children; + if ( + (endPosition === 'left' && isNaN(parseInt(endTrChildren[0]?.innerText))) || + (endPosition === 'right' && isNaN(parseInt(endTrChildren[2]?.innerText))) + ) { return; } - if (startIndex > endIndex) { - [startIndex, endIndex] = [endIndex, startIndex]; + + checkedTdNodes = []; + + if (props.outputFormat === 'line-by-line') { + let startIndex = trNodes.indexOf(startTrNode); + let endIndex = trNodes.indexOf(endTrNode); + if (endIndex === -1) { + return; + } + if (startIndex > endIndex) { + [startIndex, endIndex] = [endIndex, startIndex]; + } + for (let i = 0; i < trNodes.length; i++) { + const tdNodes = Array.from(trNodes[i].children) as HTMLElement[]; + if (i >= startIndex && i <= endIndex) { + checkedTdNodes.push(...tdNodes); + } + } } - let position: 'left' | 'right' | 'all'; - if (isClickedLeft === undefined) { - position = 'all'; - } else if (isClickedLeft) { - position = 'left'; - } else { - position = 'right'; + if (props.outputFormat === 'side-by-side') { + const startNum = parseInt((startTrNode.children[startPosition === 'left' ? 0 : 2] as HTMLElement).innerText); + let sIndex = leftRightLineNumberArr.findIndex((item) => item[startPosition] === startNum); + const endNum = parseInt((endTrNode.children[endPosition === 'left' ? 0 : 2] as HTMLElement).innerText); + let eIndex = leftRightLineNumberArr.findIndex((item) => item[endPosition] === endNum); + if (sIndex > eIndex) { + [sIndex, eIndex] = [eIndex, sIndex]; + } + const tempArr = leftRightLineNumberArr.slice(sIndex, eIndex + 1); + for (let i = 0; i < tempArr.length; i++) { + const { left, right } = tempArr[i]; + if (left !== -1) { + checkedTdNodes.push(...leftNumberTdMap[left]); + } + if (right !== -1) { + checkedTdNodes.push(...rightNumberTdMap[right]); + } + } } - checkedTrNodes = []; - for (let i = 0; i < trNodes.length; i++) { - if (i >= startIndex && i <= endIndex) { - toggleCommentCheckedClass(trNodes[i], true, position); - checkedTrNodes.push(trNodes[i]); + + /* 更新节点选中状态 */ + for (let i = 0; i < allTdNodes.length; i++) { + if (checkedTdNodes.includes(allTdNodes[i])) { + allTdNodes[i].classList.add('comment-checked'); } else { - toggleCommentCheckedClass(trNodes[i], false, position); + allTdNodes[i].classList.remove('comment-checked'); } } } @@ -100,42 +162,90 @@ export function useCodeReviewLineSelection( function onMouseup() { dragging = false; if (isMouseMoved) { - afterMouseup(getLineNumbers(checkedTrNodes, props.outputFormat, isClickedLeft ? 'left' : 'right')); + let details: ICheckedLineDetails; + if (props.outputFormat === 'side-by-side') { + details = getDoubleCheckedNumberAndCodes(checkedTdNodes); + } else { + details = getSingleCheckedNumberAndCode(checkedTdNodes); + } + leftMinNum = details.lefts[0]; + leftMaxNum = details.lefts[details.lefts.length - 1]; + rightMinNum = details.rights[0]; + rightMaxNum = details.rights[details.rights.length - 1]; + afterMouseup(details); } + document.removeEventListener('mouseup', onMouseup); document.removeEventListener('mousemove', onMousemove); } - // 清除上次的选中 - function clearCommentChecked() { - for (let i = 0; i < trNodes.length; i++) { - toggleCommentCheckedClass(trNodes[i], false, 'all'); + /* 点击评论时,获取选中行的数据 */ + const getCheckedLineDetails = () => { + if (checkedTdNodes.length) { + return props.outputFormat === 'side-by-side' + ? getDoubleCheckedNumberAndCodes(checkedTdNodes) + : getSingleCheckedNumberAndCode(checkedTdNodes); } - } + }; - function toggleCommentCheckedClass(trNode: HTMLElement, isAddClass: boolean, position: 'left' | 'right' | 'all') { - const tdNodes = Array.from(trNode.children); - let toDoNodes; - if (position === 'all') { - toDoNodes = tdNodes; - } else if (position === 'left') { - toDoNodes = tdNodes.slice(0, 2); + /* 清除选中行 */ + const clearCommentClass = () => { + clearCommentChecked(checkedTdNodes); + checkedTdNodes = []; + }; + + /* 点击展开行后,更新左右行号映射关系 */ + const updateLineNumberMap = (expandLineNumberInfo: IExpandLineNumberInfo, newCode: string, direction: 'down' | 'up') => { + const container = document.createElement('div'); + parseCodeToSingle(container, newCode, props.options); + const { prevL, prevR, nextL, nextR } = expandLineNumberInfo; + const arr = getLineNumberMap(Array.from(container.querySelectorAll('tr'))); + if (direction === 'down') { + const preLeft = Number(prevL) - 1; + const preRight = Number(prevR) - 1; + const index = leftRightLineNumberArr.findIndex((item) => item.left === preLeft && item.right === preRight); + leftRightLineNumberArr.splice(index + 1, 0, ...arr); } else { - toDoNodes = tdNodes.slice(2); + const nextLeft = Number(nextL) + 1; + const nextRight = Number(nextR) + 1; + const index = leftRightLineNumberArr.findIndex((item) => item.left === nextLeft && item.right === nextRight); + leftRightLineNumberArr.splice(index, 0, ...arr); } - if ((position === 'left' || position === 'right') && isNaN(parseInt(toDoNodes[0]?.innerHTML))) { + }; + + /* 点击展开行后,更新选中行的数据 */ + const updateCheckedLine = (expandLineNumberInfo: IExpandLineNumberInfo, direction: 'down' | 'up') => { + const allTrNodes = Array.from(reviewContentRef.value.querySelectorAll('tr')).filter((item) => !item.classList?.contains('expand-line')); + const { prevL, nextL } = expandLineNumberInfo; + const num = direction === 'down' ? Number(prevL) : Number(nextL); + + if (!checkedTdNodes.length || num < leftMinNum || num > leftMaxNum) { return; } - toDoNodes.forEach((item) => { - if (item.tagName === 'TD') { - if (isAddClass) { - item.classList.add('comment-checked'); - } else { - item.classList.remove('comment-checked'); - } + + checkedTdNodes = []; + + for (let i = 0; i < allTrNodes.length; i++) { + const itemTrNode = allTrNodes[i]; + if (props.outputFormat === 'side-by-side') { + checkedTdNodes.push(...addCommentCheckedForDouble(itemTrNode, leftMinNum, leftMaxNum, rightMinNum, rightMaxNum)); + } else { + checkedTdNodes.push(...addCommentCheckedForSingle(itemTrNode, leftMinNum, leftMaxNum, rightMinNum, rightMaxNum)); } - }); - } + } + }; + + watch( + [() => props.outputFormat, () => props.allowChecked], + () => { + if (props.allowChecked && props.outputFormat === 'side-by-side') { + const container = document.createElement('div'); + parseCodeToSingle(container, props.diff, props.options); + leftRightLineNumberArr = getLineNumberMap(Array.from(container.querySelectorAll('tr'))); + } + }, + { immediate: true } + ); - return { onMousedown }; + return { onMousedown, updateLineNumberMap, getCheckedLineDetails, clearCommentClass, updateCheckedLine }; } diff --git a/packages/devui-vue/devui/code-review/src/composables/use-code-review.ts b/packages/devui-vue/devui/code-review/src/composables/use-code-review.ts index 3952e125d8..5bebeac5ef 100644 --- a/packages/devui-vue/devui/code-review/src/composables/use-code-review.ts +++ b/packages/devui-vue/devui/code-review/src/composables/use-code-review.ts @@ -3,16 +3,21 @@ import type { SetupContext, Ref } from 'vue'; import type { DiffFile } from 'diff2html/lib/types'; import * as Diff2Html from 'diff2html'; import { inBrowser } from '../../../shared/utils/common-var'; -import type { CodeReviewProps } from '../code-review-types'; +import type { CodeReviewProps, IExpandLineNumberInfo } from '../code-review-types'; import { useCodeReviewExpand } from './use-code-review-expand'; import { parseDiffCode } from '../utils'; -export function useCodeReview(props: CodeReviewProps, ctx: SetupContext) { +export function useCodeReview( + props: CodeReviewProps, + ctx: SetupContext, + reviewContentRef: Ref, + updateLineNumberMap: (expandLineNumberInfo: IExpandLineNumberInfo, newCode: string, direction: 'up' | 'down') => void, + updateCheckedLine: (expandLineNumberInfo: IExpandLineNumberInfo, direction: 'up' | 'down') => void +) { const { diff, outputFormat, allowExpand, showBlob } = toRefs(props); const renderHtml = ref(''); - const reviewContentRef = ref(); const diffFile: Ref = ref([]); - const { insertExpandButton, onExpandButtonClick } = useCodeReviewExpand(reviewContentRef, props); + const { insertExpandButton, onExpandButtonClick } = useCodeReviewExpand(reviewContentRef, props, updateLineNumberMap, updateCheckedLine); const initDiffContent = () => { diffFile.value = Diff2Html.parse(diff.value); @@ -35,5 +40,5 @@ export function useCodeReview(props: CodeReviewProps, ctx: SetupContext) { watch(diff, initDiffContent, { immediate: true }); - return { renderHtml, reviewContentRef, diffFile, onContentClick }; + return { renderHtml, diffFile, onContentClick }; } diff --git a/packages/devui-vue/devui/code-review/src/utils.ts b/packages/devui-vue/devui/code-review/src/utils.ts index ab35ce4bb3..110a0e2539 100644 --- a/packages/devui-vue/devui/code-review/src/utils.ts +++ b/packages/devui-vue/devui/code-review/src/utils.ts @@ -1,5 +1,12 @@ import { Diff2HtmlUI } from 'diff2html/lib/ui/js/diff2html-ui'; -import type { OutputFormat, ExpandDirection, LineSide, IncrementCodeInsertDirection } from './code-review-types'; +import type { + OutputFormat, + ExpandDirection, + LineSide, + IncrementCodeInsertDirection, + CommentPosition, + ILineNumberTdMap, +} from './code-review-types'; import { UpExpandIcon, DownExpandIcon, AllExpandIcon } from './components/code-review-icons'; import { ExpandLineReg, TemplateMap, TableTrReg, TableTdReg, TableTbodyReg, TableTbodyAttrReg, EmptyDataLangReg } from './const'; @@ -534,34 +541,183 @@ function getFullNumberList(min: number, max: number) { return Array.from({ length: max - min + 1 }, (_, i) => i + min); } -/* 多行选中,返回选中行的左右侧行号 */ -export function getLineNumbers(trNodes: HTMLElement[], outputFormat: OutputFormat, side: LineSide) { - const leftNumbers: number[] = []; - const rightNumbers: number[] = []; +/* 拖拽开始时,清除上次的选中行 */ +export function clearCommentChecked(checkedTdNodes: HTMLElement[]) { + for (let i = 0; i < checkedTdNodes.length; i++) { + checkedTdNodes[i].classList.remove('comment-checked'); + } +} + +/* 渲染为单栏模式,用于后续获取左右行号映射 */ +export function parseCodeToSingle(container: HTMLElement, code: string, options: Record) { + const diff2HtmlUi = new Diff2HtmlUI(container, code, { + drawFileList: false, + outputFormat: 'line-by-line', + highlight: true, + rawTemplates: TemplateMap['line-by-line'], + ...options, + }); + diff2HtmlUi.draw(); +} + +function generateNumberTdObj(tdNodes: HTMLElement[]) { + const lineNumber = parseInt(tdNodes[0].innerText) || -1; + if (lineNumber !== -1) { + return { [lineNumber]: tdNodes }; + } +} +/* 获取行号和对应td的映射关系 */ +export function getLineNumberTdMap(trNodes: HTMLElement[]) { + const left: ILineNumberTdMap = {}; + const right: ILineNumberTdMap = {}; for (let i = 0; i < trNodes.length; i++) { - const itemTrNode = trNodes[i]; - if (outputFormat === 'line-by-line') { - const lineNumberTdNode = Array.from(itemTrNode.children)[0] as HTMLElement; - const leftLineNumber = parseInt((lineNumberTdNode.children[0] as HTMLElement)?.innerText); - const rightLineNumber = parseInt((lineNumberTdNode.children[1] as HTMLElement)?.innerText); - - leftLineNumber && leftNumbers.push(leftLineNumber); - rightLineNumber && rightNumbers.push(rightLineNumber); + const tdNodes = Array.from(trNodes[i].children) as HTMLElement[]; + Object.assign(left, generateNumberTdObj(tdNodes.slice(0, 2))); + Object.assign(right, generateNumberTdObj(tdNodes.slice(2))); + } + + return { left, right }; +} + +/* 获取左右行号映射关系 */ +export function getLineNumberMap(trNodes: HTMLElement[]) { + const result: CommentPosition[] = []; + + for (let i = 0; i < trNodes.length; i++) { + const lineNumberNodes = trNodes[i].children[0].children; // 行号所在的div + if (lineNumberNodes.length === 2) { + const left = parseInt((lineNumberNodes[0] as HTMLElement)?.innerText) || -1; + const right = parseInt((lineNumberNodes[1] as HTMLElement)?.innerText) || -1; + result.push({ left, right }); + } + } + + return result; +} + +/* 获取双栏模式下,选中行的左右行号和代码 */ +export function getDoubleCheckedNumberAndCodes(checkedTdNodes: HTMLElement[]) { + const lefts: number[] = []; + const rights: number[] = []; + const leftCode: string[] = []; + const rightCode: string[] = []; + const leftNumberNodes: HTMLElement[] = []; + const rightNumberNodes: HTMLElement[] = []; + + for (let i = 0; i < checkedTdNodes.length; i++) { + const itemTdNode = checkedTdNodes[i]; + if (itemTdNode.classList.contains('d-code-left')) { + if (itemTdNode.classList.contains('d2h-code-side-linenumber')) { + leftNumberNodes.push(itemTdNode); + } else { + leftCode.push(itemTdNode.innerText); + } } else { - const tdNodes = Array.from(itemTrNode.children) as HTMLElement[]; - const lineNumberTdNode: HTMLElement = tdNodes[side === 'left' ? 0 : 2]; - if (lineNumberTdNode && notEmptyNode(lineNumberTdNode)) { - const lineNumber = parseInt(lineNumberTdNode.innerText); - if (lineNumber) { - side === 'left' ? leftNumbers.push(lineNumber) : rightNumbers.push(lineNumber); - } + if (itemTdNode.classList.contains('d2h-code-side-linenumber')) { + rightNumberNodes.push(itemTdNode); + } else { + rightCode.push(itemTdNode.innerText); } } } - const lefts = leftNumbers.length ? getFullNumberList(leftNumbers[0], leftNumbers[leftNumbers.length - 1]) : leftNumbers; - const rights = rightNumbers.length ? getFullNumberList(rightNumbers[0], rightNumbers[rightNumbers.length - 1]) : rightNumbers; + if (leftNumberNodes.length) { + const leftMinNum = parseInt(leftNumberNodes[0].innerText); + const leftMaxNum = parseInt(leftNumberNodes[leftNumberNodes.length - 1].innerText); + lefts.push(...getFullNumberList(leftMinNum, leftMaxNum)); + } + if (rightNumberNodes.length) { + const rightMinNum = parseInt(rightNumberNodes[0].innerText); + const rightMaxNum = parseInt(rightNumberNodes[rightNumberNodes.length - 1].innerText); + rights.push(...getFullNumberList(rightMinNum, rightMaxNum)); + } + + return { lefts, rights, codes: { leftCode, rightCode } }; +} + +/* 获取单栏模式下,选中行的左右行号和代码 */ +export function getSingleCheckedNumberAndCode(checkedTdNodes: HTMLElement[]) { + const lefts: number[] = []; + const rights: number[] = []; + const codes: string[] = []; + const leftNumbers: number[] = []; + const rightNumbers: number[] = []; + + for (let i = 0; i < checkedTdNodes.length; i++) { + const itemTdNode = checkedTdNodes[i]; + if (itemTdNode.classList.contains('d2h-code-linenumber')) { + const numberChildren = itemTdNode.children as unknown as HTMLElement[]; + const leftNum = parseInt(numberChildren[0].innerText); + const rightNum = parseInt(numberChildren[1].innerText); + !isNaN(leftNum) && leftNumbers.push(leftNum); + !isNaN(rightNum) && rightNumbers.push(rightNum); + } else { + codes.push(itemTdNode.innerText); + } + } + + lefts.push(...getFullNumberList(leftNumbers[0], leftNumbers[leftNumbers.length - 1])); + rights.push(...getFullNumberList(rightNumbers[0], rightNumbers[rightNumbers.length - 1])); + + return { lefts, rights, codes }; +} + +/* 双栏模式,点击展开行后,为新增的行设置选中样式 */ +export function addCommentCheckedForDouble( + trNode: HTMLElement, + leftMinNum: number, + leftMaxNum: number, + rightMinNum: number, + rightMaxNum: number +) { + const [leftNumTd, leftCodeTd, rightNumTd, rightCodeTd] = trNode.children as unknown as HTMLElement[]; + const leftNum = parseInt(leftNumTd.innerText); + const rightNum = parseInt(rightNumTd.innerText); + const result: HTMLElement[] = []; + + if (!isNaN(leftNum) && leftNum >= leftMinNum && leftNum <= leftMaxNum) { + if (!leftNumTd.classList.contains('comment-checked')) { + leftNumTd.classList.add('comment-checked'); + leftCodeTd.classList.add('comment-checked'); + } + result.push(leftNumTd, leftCodeTd); + } + if (!isNaN(rightNum) && rightNum >= rightMinNum && rightNum <= rightMaxNum) { + if (!rightNumTd.classList.contains('comment-checked')) { + rightNumTd.classList.add('comment-checked'); + rightCodeTd.classList.add('comment-checked'); + } + result.push(rightNumTd, rightCodeTd); + } + + return result; +} + +/* 单栏模式,点击展开行后,为新增的行设置选中样式 */ +export function addCommentCheckedForSingle( + trNode: HTMLElement, + leftMinNum: number, + leftMaxNum: number, + rightMinNum: number, + rightMaxNum: number +) { + const [numTd, codeTd] = trNode.children as unknown as HTMLElement[]; + const [leftNumNode, rightNumNode] = numTd.children as unknown as HTMLElement[]; + const leftNum = parseInt(leftNumNode.innerText); + const rightNum = parseInt(rightNumNode.innerText); + const result: HTMLElement[] = []; + + if ( + (!isNaN(leftNum) && leftNum >= leftMinNum && leftNum <= leftMaxNum) || + (!isNaN(rightNum) && rightNum >= rightMinNum && rightNum <= rightMaxNum) + ) { + if (!numTd.classList.contains('comment-checked')) { + numTd.classList.add('comment-checked'); + codeTd.classList.add('comment-checked'); + } + result.push(numTd, codeTd); + } - return { lefts, rights }; + return result; } diff --git a/packages/devui-vue/devui/editor-md/src/composables/use-editor-md.ts b/packages/devui-vue/devui/editor-md/src/composables/use-editor-md.ts index 65d0c0a00e..5fb37fd4cf 100644 --- a/packages/devui-vue/devui/editor-md/src/composables/use-editor-md.ts +++ b/packages/devui-vue/devui/editor-md/src/composables/use-editor-md.ts @@ -316,6 +316,24 @@ export function useEditorMd(props: EditorMdProps, ctx: SetupContext) { setTimeout(() => { ctx.emit('contentChange', editorIns.getValue()); }, 100); + + containerRef.value.addEventListener('keydown', (e: KeyboardEvent) => { + let keyCombination = ''; + if (e.ctrlKey) { + keyCombination += 'Ctrl-'; + } + if (e.altKey) { + keyCombination += 'Alt-'; + } + if (e.shiftKey) { + keyCombination += 'Shift-'; + } + keyCombination += e.key.toUpperCase(); + if (shortKeys[keyCombination] && typeof shortKeys[keyCombination] === 'function') { + e.preventDefault(); + shortKeys[keyCombination](); + } + }); }; const onPaste = (e: ClipboardEvent) => { diff --git a/packages/devui-vue/docs/components/code-review/index.md b/packages/devui-vue/docs/components/code-review/index.md index 831b38849b..7ae1fa29cf 100644 --- a/packages/devui-vue/docs/components/code-review/index.md +++ b/packages/devui-vue/docs/components/code-review/index.md @@ -279,7 +279,6 @@ export default defineComponent({ ::: - ### 多选代码行用法 本示例将展示在开启多选代码行,多选后单击最后一个选中的行,添加评论,并且将选中代码行和代码块放入评论内容中。 @@ -501,7 +500,6 @@ export default defineComponent({ ' @@\n ' + code.slice(0, Math.min(Math.abs(lStart - lEnd - 1), 10)).join(' '); update(content); - codeReviewIns.updateCheckedLineClass(); }; return { diff, outputFormat, isFullscreen, onChange, onAddComment, afterViewInit, onContentRefresh, codeLoader }; @@ -571,6 +569,7 @@ export default defineComponent({ } ``` + ::: ### CodeReview 参数 @@ -634,6 +633,7 @@ line-by-line 模式,left 表示左侧一栏的行号,right 表示右侧一 interface CommentPosition { left: number; right: number; + position?: 'left'|'right' // 双栏模式,点击的左侧还是右侧 } ``` @@ -667,9 +667,6 @@ interface CodeReviewMethods { // 删除评论的方法,传入行号、left/right removeComment: (lineNumber: number, lineSide: LineSide) => void; - // 更新选中行样式,直接调用一般用于展开时更新选中行样式,像示例中一样使用 - updateCheckedLineClass: (); - // 清除选中行样式 clearCheckedLines: () => void; } diff --git a/packages/devui-vue/package.json b/packages/devui-vue/package.json index 01b89be49b..52c49a5fc2 100644 --- a/packages/devui-vue/package.json +++ b/packages/devui-vue/package.json @@ -1,6 +1,6 @@ { "name": "vue-devui", - "version": "1.6.27", + "version": "1.6.28", "license": "MIT", "description": "DevUI components based on Vite and Vue3", "keywords": [