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 6a86b07983..5a87a0e21c 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, updateLineNumbers, afterCheckLines); + 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,168 +111,27 @@ export function useCodeReviewComment(reviewContentRef: Ref, props: resetLeftTop(); } }; - // 获代码行 取值方法 - const getLineNumbers = (currentNumber: number, currentNumbers: Array, moveDirection: 'up' | 'down') => { - if (currentNumber === -1) { - // 当前行没数据不代表之前选中的没数据,此时返回原来的 - return currentNumbers; - } - if (currentNumbers.length === 0) { - return [currentNumber]; - } - const numbers = [...currentNumbers]; - let max = Math.max(...numbers); - let min = Math.min(...numbers); - if (moveDirection === 'down') { - max = currentNumber; - } - if (moveDirection === 'up') { - min = currentNumber; - } - return Array.from({ length: max - min + 1 }, (_, i) => i + min); - }; - // 获取一些公共类和判断 - 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(moveDirection: 'up' | 'down') { - currentLeftLineNumbers = - currentLeftLineNumber === -1 ? currentLeftLineNumbers : getLineNumbers(currentLeftLineNumber, currentLeftLineNumbers, moveDirection); - currentRightLineNumbers = - currentRightLineNumber === -1 - ? currentRightLineNumbers - : getLineNumbers(currentRightLineNumber, currentRightLineNumbers, moveDirection); - getCheckedLineCode(false); - afterCheckLinesEmitData = { - left: currentLeftLineNumber, - right: currentRightLineNumber, - details: { - lefts: currentLeftLineNumbers, - rights: currentRightLineNumbers, - codes: checkedLineCodeString, - }, - }; - } - const updateCheckedLineClass = () => { - 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(details: ICheckedLineDetails) { + ctx.emit('afterCheckLines', { left: currentLeftLineNumber, right: currentRightLineNumber, position: currentPosition, details }); } // 图标或者单行的点击 const onCommentIconClick = (e: Event) => { @@ -335,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> = {}; @@ -353,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); @@ -366,11 +210,12 @@ export function useCodeReviewComment(reviewContentRef: Ref, props: commentLeft, commentTop, mouseEvent, - updateCheckedLineClass, clearCheckedLines, onCommentMouseLeave, onCommentIconClick, insertComment, removeComment, + updateLineNumberMap, + updateCheckedLine, }; -} +} \ No newline at end of file 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 38edc38257..ad2e3ea70a 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,22 +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 } from '../utils'; +import { + findParentTrNode, + clearCommentChecked, + parseCodeToSingle, + getLineNumberMap, + getLineNumberTdMap, + getDoubleCheckedNumberAndCodes, + getSingleCheckedNumberAndCode, + addCommentCheckedForDouble, + addCommentCheckedForSingle, +} from '../utils'; export function useCodeReviewLineSelection( reviewContentRef: Ref, props: CodeReviewProps, - mouseMoveCb: (moveDirection: 'up' | 'down') => void, - mouseupCb: () => 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 startClientY: number; + 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) => { // 鼠标左键按下 @@ -32,20 +50,24 @@ 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; shouldClear = true; isMouseMoved = false; - startClientY = e.clientY; e.preventDefault(); e.stopPropagation(); document.addEventListener('mousemove', onMousemove); @@ -57,9 +79,8 @@ export function useCodeReviewLineSelection( if (!dragging) { return; } - isMouseMoved = true; if (shouldClear) { - clearCommentChecked(); + clearCommentChecked(checkedTdNodes); shouldClear = false; } const composedPath = e.composedPath() as HTMLElement[]; @@ -68,32 +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; } - mouseMoveCb(e.clientY > startClientY ? 'down' : 'up'); - 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]); + } + } } - for (let i = 0; i < trNodes.length; i++) { - if (i >= startIndex && i <= endIndex) { - toggleCommentCheckedClass(trNodes[i], true, position); + + /* 更新节点选中状态 */ + 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'); } } } @@ -101,42 +162,90 @@ export function useCodeReviewLineSelection( function onMouseup() { dragging = false; if (isMouseMoved) { - mouseupCb(); + 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 }; +} \ No newline at end of file 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 74180edba5..d8408bf387 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'; @@ -528,3 +535,189 @@ export function findParentTrNode(node: HTMLElement | null) { } return findParentTrNode(node.parentElement); } + +/* 根据最大最小行号,获取从小到大的完整行号列表 */ +function getFullNumberList(min: number, max: number) { + return Array.from({ length: max - min + 1 }, (_, i) => i + min); +} + +/* 拖拽开始时,清除上次的选中行 */ +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 = tdNodes[0]?.innerText ? 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 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 { + if (itemTdNode.classList.contains('d2h-code-side-linenumber')) { + rightNumberNodes.push(itemTdNode); + } else { + rightCode.push(itemTdNode.innerText); + } + } + } + + 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 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..a6655aaf46 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 @@ -2,7 +2,7 @@ import cloneDeep from 'lodash/cloneDeep'; import { computed, nextTick, onMounted, reactive, Ref, ref, SetupContext, toRefs, watch, onBeforeUnmount } from 'vue'; import { debounce } from '../../../shared/utils'; import { EditorMdProps, Mode } from '../editor-md-types'; -import { DEFAULT_TOOLBARS } from '../toolbar-config'; +import { ALT_KEY, DEFAULT_TOOLBARS } from '../toolbar-config'; import { parseHTMLStringToDomList } from '../utils'; import { refreshEditorCursor, _enforceMaxLength } from './helper'; import { throttle } from 'lodash'; @@ -289,8 +289,8 @@ export function useEditorMd(props: EditorMdProps, ctx: SetupContext) { const tempToolbars = { ...toolbars, ...customToolbars?.value }; for (const key of Object.keys(tempToolbars)) { const toolbarItem = tempToolbars[key]; - if (toolbarItem.shortKey && flatToolbarConfig.includes(toolbarItem.id)) { - shortKeys[toolbarItem.shortKey.replace(/\+/g, '-')] = toolbarItem.handler?.bind(null, editorIns, toolbarItem.params); + if (toolbarItem.shortKeyWithCode && flatToolbarConfig.includes(toolbarItem.id)) { + shortKeys[toolbarItem.shortKeyWithCode.replace(/\+/g, '-')] = toolbarItem.handler?.bind(null, editorIns, toolbarItem.params); } } @@ -316,6 +316,28 @@ 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.metaKey) { + keyCombination += '⌘-'; + } + if (e.altKey) { + keyCombination += `${ALT_KEY}-`; + } + if (e.shiftKey) { + keyCombination += 'Shift-'; + } + + keyCombination += e.keyCode; + if (shortKeys[keyCombination] && typeof shortKeys[keyCombination] === 'function') { + e.preventDefault(); + shortKeys[keyCombination](); + } + }); }; const onPaste = (e: ClipboardEvent) => { diff --git a/packages/devui-vue/devui/editor-md/src/toolbar-config.ts b/packages/devui-vue/devui/editor-md/src/toolbar-config.ts index d3711660ed..bc4b0c665c 100644 --- a/packages/devui-vue/devui/editor-md/src/toolbar-config.ts +++ b/packages/devui-vue/devui/editor-md/src/toolbar-config.ts @@ -31,6 +31,7 @@ export interface IToolbarItemConfig { template?: any; component?: any; shortKey?: string; + shortKeyWithCode?: string; params?: { [key: string]: any }; handler?(editor?: any, params?: any): void; } @@ -277,13 +278,17 @@ class ToolBarHandler { static color = (): void => {}; } +export const CTRL_KEY = navigator?.platform?.indexOf('Mac') !== -1 ? '⌘' : 'Ctrl'; +export const ALT_KEY = navigator?.platform?.indexOf('Mac') !== -1 ? '⌥' : 'Alt'; + export const DEFAULT_TOOLBARS: Record = { undo: { id: 'undo', name: 'undo', type: 'button', icon: UNDO_ICON, - shortKey: 'Ctrl+Z', + shortKey: `${CTRL_KEY}+Z`, + shortKeyWithCode: `${CTRL_KEY}+90`, handler: ToolBarHandler.undo, }, redo: { @@ -291,7 +296,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'redo', type: 'button', icon: REDO_ICON, - shortKey: 'Ctrl+Y', + shortKey: `${CTRL_KEY}+Y`, + shortKeyWithCode: `${CTRL_KEY}+89`, handler: ToolBarHandler.redo, }, bold: { @@ -299,7 +305,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'bold', type: 'button', icon: BOLD_ICON, - shortKey: 'Ctrl+B', + shortKey: `${CTRL_KEY}+B`, + shortKeyWithCode: `${CTRL_KEY}+66`, handler: ToolBarHandler.bold, }, italic: { @@ -307,7 +314,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'italic', type: 'button', icon: ITALIC_ICON, - shortKey: 'Ctrl+I', + shortKey: `${CTRL_KEY}+I`, + shortKeyWithCode: `${CTRL_KEY}+73`, handler: ToolBarHandler.italic, }, strike: { @@ -315,7 +323,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'strike', type: 'button', icon: STRIKE_ICON, - shortKey: 'Ctrl+D', + shortKey: `${CTRL_KEY}+D`, + shortKeyWithCode: `${CTRL_KEY}+68`, handler: ToolBarHandler.strike, }, h1: { @@ -323,7 +332,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'h1', type: 'button', icon: H1_ICON, - shortKey: 'Ctrl+1', + shortKey: `${CTRL_KEY}+1`, + shortKeyWithCode: `${CTRL_KEY}+49`, handler: ToolBarHandler.h1, }, h2: { @@ -331,7 +341,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'h2', type: 'button', icon: H2_ICON, - shortKey: 'Ctrl+2', + shortKey: `${CTRL_KEY}+2`, + shortKeyWithCode: `${CTRL_KEY}+50`, handler: ToolBarHandler.h2, }, ul: { @@ -339,7 +350,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'unorderedlist', type: 'button', icon: LIST_UNORDERED_ICON, - shortKey: 'Ctrl+U', + shortKey: `${CTRL_KEY}+U`, + shortKeyWithCode: `${CTRL_KEY}+85`, handler: ToolBarHandler.ul, }, ol: { @@ -347,7 +359,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'orderedlist', type: 'button', icon: LIST_ORDERED_ICON, - shortKey: 'Ctrl+O', + shortKey: `${CTRL_KEY}+O`, + shortKeyWithCode: `${CTRL_KEY}+79`, handler: ToolBarHandler.ol, }, checklist: { @@ -355,7 +368,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'checklist', type: 'button', icon: LIST_CHECK_ICON, - shortKey: 'Ctrl+Alt+C', + shortKey: `${CTRL_KEY}+${ALT_KEY}+C`, + shortKeyWithCode: `${CTRL_KEY}+${ALT_KEY}+67`, handler: ToolBarHandler.checkList, }, underline: { @@ -363,7 +377,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'underline', type: 'button', icon: UNDERLINE_ICON, - shortKey: 'Ctrl+R', + shortKey: `${CTRL_KEY}+R`, + shortKeyWithCode: `${CTRL_KEY}+82`, handler: ToolBarHandler.underline, }, font: { @@ -379,7 +394,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'link', type: 'button', icon: LINK_ICON, - shortKey: 'Ctrl+L', + shortKey: `${CTRL_KEY}+L`, + shortKeyWithCode: `${CTRL_KEY}+76`, handler: ToolBarHandler.link, }, image: { @@ -387,7 +403,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'image', type: 'button', icon: IMAGE_ICON, - shortKey: 'Ctrl+G', + shortKey: `${CTRL_KEY}+G`, + shortKeyWithCode: `${CTRL_KEY}+71`, params: { imageUploadToServer: false }, handler: ToolBarHandler.image, }, @@ -397,7 +414,8 @@ export const DEFAULT_TOOLBARS: Record = { type: 'button', icon: FILE_ICON, params: {}, - shortKey: 'Ctrl+F', + shortKey: `${CTRL_KEY}+F`, + shortKeyWithCode: `${CTRL_KEY}+70`, handler: ToolBarHandler.file, }, code: { @@ -405,7 +423,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'code', type: 'button', icon: CODE_ICON, - shortKey: 'Ctrl+K', + shortKey: `${CTRL_KEY}+K`, + shortKeyWithCode: `${CTRL_KEY}+75`, handler: ToolBarHandler.code, }, table: { @@ -413,7 +432,8 @@ export const DEFAULT_TOOLBARS: Record = { name: 'table', type: 'button', icon: TABLE_ICON, - shortKey: 'Ctrl+Alt+T', + shortKey: `${CTRL_KEY}+${ALT_KEY}+T`, + shortKeyWithCode: `${CTRL_KEY}+${ALT_KEY}+84`, handler: ToolBarHandler.table, }, fullscreen: { diff --git a/packages/devui-vue/devui/git-graph/src/git-graph-class.ts b/packages/devui-vue/devui/git-graph/src/git-graph-class.ts index c620811109..05d5da3a71 100644 --- a/packages/devui-vue/devui/git-graph/src/git-graph-class.ts +++ b/packages/devui-vue/devui/git-graph/src/git-graph-class.ts @@ -72,12 +72,12 @@ export class GitGraph { this.graphHeight = (this.element as HTMLElement).getBoundingClientRect().height; this.graphWidth = (this.element as HTMLElement).getBoundingClientRect().width; - // 按提交数据计算画布高度,并留出下方150,右边300空白,保证悬浮框不超出画布 + // 按提交数据计算画布高度,并留出下方150,右边500空白,保证悬浮框不超出画布 const ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150); - const cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300); + const cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 500); this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.svg.setAttribute('height', ch + ''); - this.svg.setAttribute('width', '100%'); + this.svg.setAttribute('width', cw + ''); this.element?.appendChild(this.svg); this.barHeight = Math.max(this.graphHeight, this.unitTime * this.commits.length + 320); @@ -237,7 +237,7 @@ export class GitGraph { r: 4, fill: '#fff', strokeWidth: 1, - stroke: this.colors[commit.space], + stroke: this.colors[commit.space % 20], style: 'cursor: pointer;' }; this.setNodeAttr(circle, attrs); @@ -265,7 +265,7 @@ export class GitGraph { this.svg.appendChild(img); if (!this.messageBoxWidth) { - this.messageBoxWidth = this.svg.getBoundingClientRect.width - (avatar_box_x + 40); + this.messageBoxWidth = this.svg.getBoundingClientRect().width - (avatar_box_x + 40); } // 画竖线 let route = ['M', avatar_box_x + 15, avatar_box_y - 20, 'L', avatar_box_x + 15, avatar_box_y]; @@ -292,17 +292,30 @@ export class GitGraph { commit.author.name = commit.author.name.substr(0, this.maxNameLength) + '...'; } - const commitText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + const commitText = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); const commitAttrs = { x: avatar_box_x + 40, - y: y + 4, + y: y - 8, 'text-anchor': 'start', style: 'cursor: pointer;text-anchor: start;', - fill: isdark ? '#e8e8e8' : '#2e2e2e', - 'font-size': 14, + width: this.messageBoxWidth, + height: 20, }; this.setNodeAttr(commitText, commitAttrs); + const textArr = { + style: 'width: 100%; height: 20px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;', + title: commit.message, + }; + + const text = document.createElement('div'); + this.setNodeAttr(text, textArr); + + text.innerText = commit.message.replace(/\n/g, ' '); + commitText.appendChild(text); + + this.svg.appendChild(commitText); + const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); tspan.appendChild(document.createTextNode(commit.message.replace(/\n/g, ' '))); commitText.appendChild(tspan); @@ -339,9 +352,9 @@ export class GitGraph { parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space); parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]); if (parentCommit.space <= commit.space) { - color = this.colors[commit.space]; + color = this.colors[commit.space % 20]; } else { - color = this.colors[parentCommit.space]; + color = this.colors[parentCommit.space % 20]; } if (parent[1] === commit.space) { offset = [0, 5]; @@ -438,7 +451,7 @@ export class GitGraph { const rectAttrs = { fill: this.isDark ? '#4C4C4C' : '#fff', - stroke: this.colors[commit.space], + stroke: this.colors[commit.space % 20], 'stroke-width': '1px', d: path.join(' '), transform: `matrix(1,0,0,1,-${textbox.width + 26},0)`, @@ -446,7 +459,7 @@ export class GitGraph { const newAttrs = { transform: `matrix(1,0,0,1,-${textbox.width + 26},0)`, - fill: this.colors[commit.space], + fill: this.colors[commit.space % 20], }; this.setNodeAttr(text, newAttrs); diff --git a/packages/devui-vue/devui/git-graph/src/git-graph.scss b/packages/devui-vue/devui/git-graph/src/git-graph.scss new file mode 100644 index 0000000000..28e11539b2 --- /dev/null +++ b/packages/devui-vue/devui/git-graph/src/git-graph.scss @@ -0,0 +1,3 @@ +.d-graph-wrapper { + overflow-x: auto; +} diff --git a/packages/devui-vue/devui/git-graph/src/git-graph.tsx b/packages/devui-vue/devui/git-graph/src/git-graph.tsx index 95614bb715..ee2f044160 100644 --- a/packages/devui-vue/devui/git-graph/src/git-graph.tsx +++ b/packages/devui-vue/devui/git-graph/src/git-graph.tsx @@ -1,7 +1,7 @@ import { defineComponent, onMounted, ref, SetupContext, nextTick } from "vue"; import { GitGraphProps, gitGraphProps } from "./git-graph-types"; import useGitGraph from "./use-git-graph"; - +import './git-graph.scss'; export default defineComponent({ name: 'DGitGraph', 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 1d38592356..52c49a5fc2 100644 --- a/packages/devui-vue/package.json +++ b/packages/devui-vue/package.json @@ -1,6 +1,6 @@ { "name": "vue-devui", - "version": "1.6.25", + "version": "1.6.28", "license": "MIT", "description": "DevUI components based on Vite and Vue3", "keywords": [