From dafee8fd79f67f73e0a0f1a718b80999b98477bd Mon Sep 17 00:00:00 2001 From: hahaaha <390519764@qq.com> Date: Tue, 23 Feb 2021 18:28:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Da=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E9=87=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/menus/link/create-panel-conf.ts | 24 +++++ src/menus/link/util.ts | 155 ++++++++++++++++++++++++++++ test/unit/menus/link.test.ts | 47 --------- test/unit/menus/link/link.test.ts | 49 +++++++++ test/unit/menus/link/util.test.ts | 128 +++++++++++++++++++++++ 5 files changed, 356 insertions(+), 47 deletions(-) create mode 100644 src/menus/link/util.ts delete mode 100644 test/unit/menus/link.test.ts create mode 100644 test/unit/menus/link/link.test.ts create mode 100644 test/unit/menus/link/util.test.ts diff --git a/src/menus/link/create-panel-conf.ts b/src/menus/link/create-panel-conf.ts index c4895a4..caf2191 100644 --- a/src/menus/link/create-panel-conf.ts +++ b/src/menus/link/create-panel-conf.ts @@ -8,6 +8,7 @@ import { PanelConf } from '../menu-constructors/Panel' import { getRandom } from '../../utils/util' import $, { DomElement } from '../../utils/dom-core' import isActive from './is-active' +import { insertHtml } from './util' export default function (editor: Editor, text: string, link: string): PanelConf { // panel 中需要用到的id @@ -125,12 +126,35 @@ export default function (editor: Editor, text: string, link: string): PanelConf selector: '#' + btnOkId, type: 'click', fn: () => { + // 获取选取 + editor.selection.restoreSelection() + const topNode = editor.selection + .getSelectionRangeTopNodes()[0] + .getNode() + const selection = window.getSelection() // 执行插入链接 const $link = $('#' + inputLinkId) const $text = $('#' + inputTextId) let link = $link.val().trim() let text = $text.val().trim() + let html: string = '' + if (selection && !selection?.isCollapsed) + html = insertHtml(selection, topNode)?.trim() + + // 去除html的tag标签 + let htmlText = html?.replace(/<.*?>/g, '') + let htmlTextLen = htmlText?.length ?? 0 + // 当input中的text的长度大于等于选区的文字时 + // 需要判断两者相同的长度的text内容是否相同 + // 相同则只需把多余的部分添加上去即可,否则使用input中的内容 + if (htmlTextLen <= text.length) { + let startText = text.substring(0, htmlTextLen) + let endText = text.substring(htmlTextLen) + if (htmlText === startText) { + text = html + endText + } + } // 链接为空,则不插入 if (!link) return // 文本为空,则用链接代替 diff --git a/src/menus/link/util.ts b/src/menus/link/util.ts new file mode 100644 index 0000000..67cc4ef --- /dev/null +++ b/src/menus/link/util.ts @@ -0,0 +1,155 @@ +/** + * 获取除了包裹在整行区域的顶级Node + * @param node 最外层node下的某个childNode + * @param topText 最外层node中文本内容 + */ +function getTopNode(node: Node, topText: string): Node { + let pointerNode: Node = node + let topNode: Node = node + do { + if (pointerNode.textContent === topText) break + topNode = pointerNode + if (pointerNode.parentNode) { + pointerNode = pointerNode?.parentNode + } + } while (pointerNode?.nodeName !== 'P') + + return topNode +} + +/** + * 生成html的string形式 + * @param tagName 标签名 + * @param content 需要包裹的内容 + */ +function makeHtmlString(node: Node, content: string): string { + let tagName = node.nodeName + let attr = '' + if (node.nodeType === 3) { + return content + } + if (node.nodeType === 1) { + const style = (node as Element).getAttribute('style') + const face = (node as Element).getAttribute('face') + const color = (node as Element).getAttribute('color') + if (style) attr = attr + ` style="${style}"` + if (face) attr = attr + ` face="${face}"` + if (color) attr = attr + ` color="${color}"` + } + tagName = tagName.toLowerCase() + return `<${tagName}${attr}>${content}` +} + +/** + * 生成开始或者结束位置的html字符片段 + * @param topText 选区所在的行的文本内容 + * @param node 选区给出的node节点 + * @param startPos node文本内容选取的开始位置 + * @param endPos node文本内容选取的结束位置 + */ +function createPartHtml(topText: string, node: Node, startPos: number, endPost?: number): string { + let selectionContent = node.textContent?.substring(startPos, endPost) + let pointerNode = node + let content = '' + do { + content = makeHtmlString(pointerNode, selectionContent ?? '') + selectionContent = content + if (pointerNode.parentElement) pointerNode = pointerNode?.parentElement + } while (pointerNode.textContent !== topText) + + return content +} + +/** + * 生成需要插入的html内容的字符串形式 + * @param selection 选区对象 + * @param topNode 选区所在行的顶级node节点 + */ +function insertHtml(selection: Selection, topNode: Node): string { + const { anchorNode, focusNode, anchorOffset: anchorPos, focusOffset: focusPos } = selection + const topText = topNode.textContent ?? '' + const TagArr = getContainerTag(topNode) + + let content: string = '' + let startContent: string = '' + let middleContent: string = '' + let endContent: string = '' + + let startNode = anchorNode + let endNode = focusNode + // 用来保存 anchorNode的非p最外层节点 + let pointerNode = anchorNode + + // 节点是同一个的处理 + if (anchorNode?.isEqualNode(focusNode ?? null)) { + let innerContent = createPartHtml(topText, anchorNode, anchorPos, focusPos) + innerContent = addContainer(TagArr, innerContent) + return innerContent + } + + // 选中开始位置节点的处理 + if (anchorNode) startContent = createPartHtml(topText, anchorNode, anchorPos ?? 0) + + // 结束位置节点的处理 + if (focusNode) endContent = createPartHtml(topText, focusNode, 0, focusPos) + + // 将指针节点位置放置到开始的节点 + if (anchorNode) { + // 获取start的非p顶级node + startNode = getTopNode(anchorNode, topText) + } + if (focusNode) { + // 获取end的非p顶级node + endNode = getTopNode(focusNode, topText) + } + + // 处于开始和结束节点位置之间的节点的处理 + pointerNode = startNode?.nextSibling ?? anchorNode + while (!pointerNode?.isEqualNode(endNode ?? null)) { + const pointerNodeName = pointerNode?.nodeName + if (pointerNodeName === '#text') { + middleContent = middleContent + pointerNode?.textContent + } else { + let htmlString = pointerNode?.firstChild?.parentElement?.innerHTML + if (pointerNode) + middleContent = middleContent + makeHtmlString(pointerNode, htmlString ?? '') + } + pointerNode = pointerNode?.nextSibling ?? pointerNode + } + + content = `${startContent}${middleContent}${endContent}` + + // 增加最外层包裹标签 + content = addContainer(TagArr, content) + + return content +} +/** + * 获取包裹在最外层的非p Node tagName 数组 + * @param node 选区所在行的node节点 + */ +function getContainerTag(node: Node): Node[] { + const topText = node.textContent ?? '' + let tagArr = [] + while (node?.textContent === topText) { + if (node.nodeName !== 'P') { + tagArr.push(node) + } + node = node.childNodes[0] + } + return tagArr +} + +/** + * 为内容增加包裹标签 + * @param tagArr 最外层包裹的tag数组,索引越小tag越在外面 + * @param content tag要包裹的内容 + */ +function addContainer(tagArr: Node[], content: string): string { + tagArr.forEach(v => { + content = makeHtmlString(v, content) + }) + return content +} + +export { getTopNode, makeHtmlString, createPartHtml, insertHtml } diff --git a/test/unit/menus/link.test.ts b/test/unit/menus/link.test.ts deleted file mode 100644 index 78969a1..0000000 --- a/test/unit/menus/link.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @description link 菜单 test - * @author wangfupeng - */ - -import $ from 'jquery' -import Editor from '../../../src/editor' -import createEditor from '../../helpers/create-editor' -import mockCmdFn from '../../helpers/command-mock' -import Link from '../../../src/menus/link/index' -import { getMenuInstance } from '../../helpers/menus' -import Panel from '../../../src/menus/menu-constructors/Panel' - -let editor: Editor -let linkMenu: Link - -test('link 菜单:点击弹出 panel', () => { - editor = createEditor(document, 'div1') - linkMenu = getMenuInstance(editor, Link) as Link - linkMenu.clickHandler() - expect(linkMenu.panel).not.toBeNull() -}) - -test('link 菜单:插入链接', () => { - const panel = linkMenu.panel as Panel - const panelElem = panel.$container.elems[0] - const $panelElem = $(panelElem) // jquery 对象 - - // panel 里的 input 和 button 元素 - const $btnInsert = $panelElem.find(":button[id^='btn-ok']") // id 以 'btn-ok' 的 button - // const $btnDel = $panelElem.find(":button[id^='btn-del']") - const $inputLink = $panelElem.find(":input[id^='input-link']") - const $inputText = $panelElem.find(":input[id^='input-text']") - - // 插入链接 - mockCmdFn(document) - const text = '文字' - const link = 'http://www.baidu.com/' - $inputText.val(text) - $inputLink.val(link) - $btnInsert.click() - - // 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述 - expect( - editor.$textElem.html().indexOf(`${text}`) - ).toBeGreaterThan(0) -}) diff --git a/test/unit/menus/link/link.test.ts b/test/unit/menus/link/link.test.ts new file mode 100644 index 0000000..8e96385 --- /dev/null +++ b/test/unit/menus/link/link.test.ts @@ -0,0 +1,49 @@ +/** + * @description link 菜单 test + * @author wangfupeng + */ + +import $ from 'jquery' +import Editor from '../../../../src/editor' +import createEditor from '../../../helpers/create-editor' +import mockCmdFn from '../../../helpers/command-mock' +import Link from '../../../../src/menus/link/index' +import { getMenuInstance } from '../../../helpers/menus' +import Panel from '../../../../src/menus/menu-constructors/Panel' + +let editor: Editor +let linkMenu: Link + +describe('link菜单', () => { + test('link 菜单:点击弹出 panel', () => { + editor = createEditor(document, 'div1') + linkMenu = getMenuInstance(editor, Link) as Link + linkMenu.clickHandler() + expect(linkMenu.panel).not.toBeNull() + }) + + test('link 菜单:插入链接', () => { + const panel = linkMenu.panel as Panel + const panelElem = panel.$container.elems[0] + const $panelElem = $(panelElem) // jquery 对象 + + // panel 里的 input 和 button 元素 + const $btnInsert = $panelElem.find(":button[id^='btn-ok']") // id 以 'btn-ok' 的 button + // const $btnDel = $panelElem.find(":button[id^='btn-del']") + const $inputLink = $panelElem.find(":input[id^='input-link']") + const $inputText = $panelElem.find(":input[id^='input-text']") + + // 插入链接 + mockCmdFn(document) + const text = '文字' + const link = 'http://www.baidu.com/' + $inputText.val(text) + $inputLink.val(link) + $btnInsert.click() + + // 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述 + expect( + editor.$textElem.html().indexOf(`${text}`) + ).toBeGreaterThan(0) + }) +}) diff --git a/test/unit/menus/link/util.test.ts b/test/unit/menus/link/util.test.ts new file mode 100644 index 0000000..d455ce1 --- /dev/null +++ b/test/unit/menus/link/util.test.ts @@ -0,0 +1,128 @@ +import { insertHtml } from '../../../../src/menus/link/util' + +/** + * 生成带选区的selection对象 + */ +function createSelection( + anchorNode: Node, + anchorPos: number, + focusNode: Node, + focusPos: number +): Selection { + const selection = window.getSelection() as Selection + const range = new Range() + range.setStart(anchorNode, anchorPos) + range.setEnd(focusNode, focusPos) + selection.removeAllRanges() + selection.addRange(range) + return selection +} + +describe('测试insertHtml函数', () => { + test('选区anchorNode和focusNode是同一个且外层只有p标签包裹', () => { + document.body.innerHTML = `` + const p = document.getElementById('link') as Node + const anchorNode = p.childNodes[0] + const selection = createSelection(anchorNode, 2, anchorNode, 5) + + const htmlString = insertHtml(selection, p) + expect(htmlString).toBe('345') + }) + + test('anchorNode和focusNode父节点p标签,且两者间是一个带标签的节点', () => { + document.body.innerHTML = `` + const p = document.getElementById('link') as Node + const len = p.childNodes.length + const anchorNode = p.childNodes[0] + const focusNode = p.childNodes[len - 1] + const selection = createSelection(anchorNode, 2, focusNode, 3) + + const htmlString = insertHtml(selection, p) + expect(htmlString).toBe('3456test789') + }) + + test('anchorNode和focusNode父节点为p标签,且两者间是多个带标签的节点', () => { + document.body.innerHTML = `` + const p = document.getElementById('link') as Node + const len = p.childNodes.length + const anchorNode = p.childNodes[0] + const focusNode = p.childNodes[len - 1] + const selection = createSelection(anchorNode, 2, focusNode, 3) + + const htmlString = insertHtml(selection, p) + expect(htmlString).toBe('3456testtest1789') + }) + + test('anchorNode和focusNode父节点为p标签,且两者间是带标签的节点以及文本节点', () => { + document.body.innerHTML = `` + const p = document.getElementById('link') as Node + const len = p.childNodes.length + const anchorNode = p.childNodes[0] + const focusNode = p.childNodes[len - 1] + const selection = createSelection(anchorNode, 2, focusNode, 3) + + const htmlString = insertHtml(selection, p) + expect(htmlString).toBe('3456testmiddletest1789') + }) + + test('anchorNode和focusNode父节点为非p标签', () => { + document.body.innerHTML = `` + const p = document.getElementById('link') as Node + const len = p.childNodes.length + const anchorNode = p.childNodes[0].childNodes[0] + const focusNode = p.childNodes[len - 1].childNodes[0] + const selection = createSelection(anchorNode, 2, focusNode, 3) + + const htmlString = insertHtml(selection, p) + expect(htmlString).toBe('34560000789') + }) + + test('选中的行中最外层包裹有除了p之外的其他标签', () => { + document.body.innerHTML = `` + const p = document.getElementById('link') as Node + const anchorNode = p.childNodes[0].childNodes[0].childNodes[0] + const focusNode = p.childNodes[0].childNodes[2].childNodes[0] + const selection = createSelection(anchorNode, 2, focusNode, 3) + + const htmlString = insertHtml(selection, p) + expect(htmlString).toBe('34560000789') + }) + + test('测试背景颜色是否保存', () => { + document.body.innerHTML = `` + const p = document.getElementById('link') as Node + const len = p.childNodes.length + const anchorNode = p.childNodes[0] + const focusNode = p.childNodes[len - 1] + const selection = createSelection(anchorNode, 2, focusNode, 3) + + const htmlString = insertHtml(selection, p) + expect(htmlString).toBe( + '3456test789' + ) + }) + + test('测试字体颜色是否保存', () => { + document.body.innerHTML = `` + const p = document.getElementById('link') as Node + const len = p.childNodes.length + const anchorNode = p.childNodes[0] + const focusNode = p.childNodes[len - 1] + const selection = createSelection(anchorNode, 2, focusNode, 3) + + const htmlString = insertHtml(selection, p) + expect(htmlString).toBe('345678test678') + }) + + test('测试设置后的字体是否保存', () => { + document.body.innerHTML = `` + const p = document.getElementById('link') as Node + const len = p.childNodes.length + const anchorNode = p.childNodes[0] + const focusNode = p.childNodes[len - 1] + const selection = createSelection(anchorNode, 2, focusNode, 3) + + const htmlString = insertHtml(selection, p) + expect(htmlString).toBe('345test678') + }) +})