From f1112763f115ad2fe133a2cd271048768047350e Mon Sep 17 00:00:00 2001 From: cycleccc <2991205548@qq.com> Date: Wed, 9 Oct 2024 18:05:32 +0800 Subject: [PATCH] Fix test error (#236) * test(code element): add language property * style: comply with new ESLint rules * test: add more null value checks * fix: merge error * Create rotten-bags-remain.md --- .changeset/rotten-bags-remain.md | 7 + .eslintrc.cjs | 5 +- .../code-block/code-block-menu.test.ts | 47 ++-- .../__tests__/code-block/elem-to-html.test.ts | 2 +- .../__tests__/code-block/plugin.test.ts | 4 +- .../__tests__/code-block/render-elem.test.ts | 2 +- .../__tests__/color/color-menus.test.ts | 3 +- .../__tests__/emotion/emotion-menu.test.ts | 2 +- .../menu/font-family-menu.test.ts | 2 +- .../menu/font-size-menu.test.ts | 2 +- .../__tests__/format-painter/plugin.test.ts | 5 +- .../__tests__/header/helper.test.ts | 2 +- .../indent/menu/decrease-indent-menu.test.ts | 2 +- .../indent/menu/increase-indent-menu.test.ts | 2 +- .../__tests__/justify/menus.test.ts | 2 +- .../line-height/line-height-menu.test.ts | 2 +- .../__tests__/link/helper.test.ts | 1 + .../__tests__/text-style/menu/menus.test.ts | 1 + .../__tests__/todo/menu/todo-menu.test.ts | 1 + .../src/modules/image/render-elem.tsx | 2 +- .../__tests__/parse-html.test.ts | 4 +- .../__tests__/config/editor-config.test.ts | 2 +- .../__tests__/editor/plugins/with-dom.test.ts | 6 +- packages/core/src/editor/dom-editor.ts | 6 +- packages/core/src/menus/bar/HoverBar.ts | 78 +++++-- packages/core/src/menus/helpers/position.ts | 126 ++++++---- packages/core/src/text-area/syncSelection.ts | 52 +++-- packages/core/src/utils/dom.ts | 221 ++++++++++-------- 28 files changed, 339 insertions(+), 252 deletions(-) create mode 100644 .changeset/rotten-bags-remain.md diff --git a/.changeset/rotten-bags-remain.md b/.changeset/rotten-bags-remain.md new file mode 100644 index 000000000..59082b6f3 --- /dev/null +++ b/.changeset/rotten-bags-remain.md @@ -0,0 +1,7 @@ +--- +"@wangeditor-next/basic-modules": patch +"@wangeditor-next/code-highlight": patch +"@wangeditor-next/core": patch +--- + +Fix test error diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 07594a674..9e60f0326 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -30,8 +30,9 @@ module.exports = { 'cypress/globals': true, }, globals: { - globalThis:'readonly', - vi: "readonly" + globalThis: 'readonly', + vi: 'readonly', + expect: 'readonly', }, extends: [ 'plugin:@typescript-eslint/eslint-recommended', diff --git a/packages/basic-modules/__tests__/code-block/code-block-menu.test.ts b/packages/basic-modules/__tests__/code-block/code-block-menu.test.ts index 1324a0cfe..54ea7589e 100644 --- a/packages/basic-modules/__tests__/code-block/code-block-menu.test.ts +++ b/packages/basic-modules/__tests__/code-block/code-block-menu.test.ts @@ -6,6 +6,7 @@ import { Editor, Element, Transforms } from 'slate' import createEditor from '../../../../tests/utils/create-editor' +import { content } from '../../../code-highlight/__tests__/content' import CodeBlockMenu from '../../src/modules/code-block/menu/CodeBlockMenu' describe('code-block menu', () => { @@ -13,16 +14,6 @@ describe('code-block menu', () => { let editor: any let startLocation: any - const codeElem = { - type: 'code', - language: 'javascript', - children: [{ text: 'var' }], - } - const preElem = { - type: 'pre', - children: [codeElem], - } - beforeEach(() => { editor = createEditor() startLocation = Editor.start(editor, []) @@ -34,19 +25,19 @@ describe('code-block menu', () => { }) it('getValue and isActive', async () => { - editor.select(startLocation) - expect(menu.isActive(editor)).toBeFalsy() - expect(menu.getValue(editor)).toBe('') + const editor1 = createEditor({ + content, + }) - editor.insertNode(preElem) // 插入 code node - editor.select({ + editor.select(startLocation) + editor1.select({ path: [1, 0, 0], // 选中 code node - offset: 3, - }) - setTimeout(() => { - expect(menu.isActive(editor)).toBeTruthy() - expect(menu.getValue(editor)).toBe('javascript') + offset: 0, }) + expect(menu.isActive(editor1)).toBeTruthy() + expect(menu.getValue(editor1)).toBe('javascript') + expect(menu.isActive(editor)).toBeFalsy() + expect(menu.getValue(editor)).toBe('') }) it('is disabled', () => { @@ -56,7 +47,7 @@ describe('code-block menu', () => { Transforms.setNodes(editor, { type: 'header1' } as Partial) expect(menu.isDisabled(editor)).toBeTruthy() // 非 p pre ,则禁用 - editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] }) + editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }], language: 'javascript' }] }) expect(menu.isDisabled(editor)).toBeFalsy() // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code }) @@ -74,18 +65,20 @@ describe('code-block menu', () => { }) it('exec - to paragraph', () => { - editor.select(startLocation) - editor.insertNode(preElem) // 插入 code node - editor.select({ + const editor1 = createEditor({ + content, + }) + + editor1.select({ path: [1, 0, 0], // 选中 code node offset: 3, }) - menu.exec(editor, '') // 取消 code-block - const preList = editor.getElemsByTypePrefix('pre') + menu.exec(editor1, '') // 取消 code-block + const preList = editor1.getElemsByTypePrefix('pre') expect(preList.length).toBe(0) - const codeLis = editor.getElemsByTypePrefix('code') + const codeLis = editor1.getElemsByTypePrefix('code') expect(codeLis.length).toBe(0) }) diff --git a/packages/basic-modules/__tests__/code-block/elem-to-html.test.ts b/packages/basic-modules/__tests__/code-block/elem-to-html.test.ts index 8f5c6fa9e..bfb3a2cc7 100644 --- a/packages/basic-modules/__tests__/code-block/elem-to-html.test.ts +++ b/packages/basic-modules/__tests__/code-block/elem-to-html.test.ts @@ -8,7 +8,7 @@ import { codeToHtmlConf, preToHtmlConf } from '../../src/modules/code-block/elem describe('code-block - elem to html', () => { it('code to html', () => { expect(codeToHtmlConf.type).toBe('code') - const elem = { type: 'code', children: [] } + const elem = { type: 'code', children: [], language: '' } const html = codeToHtmlConf.elemToHtml(elem, 'hello') expect(html).toBe('hello') diff --git a/packages/basic-modules/__tests__/code-block/plugin.test.ts b/packages/basic-modules/__tests__/code-block/plugin.test.ts index edac3eaef..254124e1f 100644 --- a/packages/basic-modules/__tests__/code-block/plugin.test.ts +++ b/packages/basic-modules/__tests__/code-block/plugin.test.ts @@ -28,6 +28,7 @@ describe('code-block plugin', () => { const codeElem = { type: 'code', children: [{ text: ' var' }], + language: '', } const preElem = { type: 'pre', @@ -81,6 +82,7 @@ describe('code-block plugin', () => { const codeElem = { type: 'code', children: [{ text: 'var' }], + language: '', } // eslint-disable-next-line @typescript-eslint/no-shadow const preElem = { @@ -137,7 +139,7 @@ describe('code-block plugin', () => { it('normalizeNode - pre node 不能是第一个节点,否则前面插入 p', () => { editor.select(startLocation) - editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] }) + editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }], language: '' }] }) const pList = editor.getElemsByTypePrefix('paragraph') diff --git a/packages/basic-modules/__tests__/code-block/render-elem.test.ts b/packages/basic-modules/__tests__/code-block/render-elem.test.ts index 0e3f5648c..c6125dbfa 100644 --- a/packages/basic-modules/__tests__/code-block/render-elem.test.ts +++ b/packages/basic-modules/__tests__/code-block/render-elem.test.ts @@ -12,7 +12,7 @@ describe('code-block render elem', () => { it('render code elem', () => { expect(renderCodeConf.type).toBe('code') - const elem = { type: 'code', children: [] } + const elem = { type: 'code', children: [], language: '' } const vnode = renderCodeConf.renderElem(elem, null, editor) expect(vnode.sel).toBe('code') diff --git a/packages/basic-modules/__tests__/color/color-menus.test.ts b/packages/basic-modules/__tests__/color/color-menus.test.ts index 912236c30..8d01e5d55 100644 --- a/packages/basic-modules/__tests__/color/color-menus.test.ts +++ b/packages/basic-modules/__tests__/color/color-menus.test.ts @@ -7,6 +7,7 @@ import { Editor } from 'slate' import { isHTMLElememt } from '../../../../packages/core/src/utils/dom' import createEditor from '../../../../tests/utils/create-editor' +import { preNode } from '../../../code-highlight/__tests__/content' import BgColorMenu from '../../src/modules/color/menu/BgColorMenu' import ColorMenu from '../../src/modules/color/menu/ColorMenu' @@ -60,7 +61,7 @@ describe('color menus', () => { expect(menu.isDisabled(editor)).toBeFalsy() }) - editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] }) + editor.insertNode(preNode) menus.forEach(({ menu }) => { expect(menu.isDisabled(editor)).toBeTruthy() }) diff --git a/packages/basic-modules/__tests__/emotion/emotion-menu.test.ts b/packages/basic-modules/__tests__/emotion/emotion-menu.test.ts index 114e5ed72..6186bbf98 100644 --- a/packages/basic-modules/__tests__/emotion/emotion-menu.test.ts +++ b/packages/basic-modules/__tests__/emotion/emotion-menu.test.ts @@ -36,7 +36,7 @@ describe('font family menu', () => { editor.select(startLocation) expect(menu.isDisabled(editor)).toBeFalsy() - editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] }) + editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }], language: '' }] }) expect(menu.isDisabled(editor)).toBeTruthy() // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code }) diff --git a/packages/basic-modules/__tests__/font-size-family/menu/font-family-menu.test.ts b/packages/basic-modules/__tests__/font-size-family/menu/font-family-menu.test.ts index 366ae745c..1155ebe37 100644 --- a/packages/basic-modules/__tests__/font-size-family/menu/font-family-menu.test.ts +++ b/packages/basic-modules/__tests__/font-size-family/menu/font-family-menu.test.ts @@ -47,7 +47,7 @@ describe('font family menu', () => { editor.select(startLocation) expect(menu.isDisabled(editor)).toBeFalsy() - editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] }) + editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }], language: '' }] }) expect(menu.isDisabled(editor)).toBeTruthy() // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code }) diff --git a/packages/basic-modules/__tests__/font-size-family/menu/font-size-menu.test.ts b/packages/basic-modules/__tests__/font-size-family/menu/font-size-menu.test.ts index db6adf38d..793a011fd 100644 --- a/packages/basic-modules/__tests__/font-size-family/menu/font-size-menu.test.ts +++ b/packages/basic-modules/__tests__/font-size-family/menu/font-size-menu.test.ts @@ -47,7 +47,7 @@ describe('font family menu', () => { editor.select(startLocation) expect(menu.isDisabled(editor)).toBeFalsy() - editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] }) + editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }], language: '' }] }) expect(menu.isDisabled(editor)).toBeTruthy() // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code }) diff --git a/packages/basic-modules/__tests__/format-painter/plugin.test.ts b/packages/basic-modules/__tests__/format-painter/plugin.test.ts index 4155c22ba..669fc45e9 100644 --- a/packages/basic-modules/__tests__/format-painter/plugin.test.ts +++ b/packages/basic-modules/__tests__/format-painter/plugin.test.ts @@ -6,13 +6,14 @@ describe('format painter plugin', () => { let editor: any beforeEach(() => { - editor = withFormatPainter(createEditor()) + editor = withFormatPainter(createEditor( + { content: [{ type: 'paragraph', children: [{ text: 'Hello World' }] }] }, + )) vi.spyOn(document, 'addEventListener') vi.spyOn(document, 'removeEventListener') editor.focus() - editor.insertText('Hello World') editor.select({ anchor: { path: [0, 0], offset: 0 }, focus: { path: [0, 0], offset: 5 }, diff --git a/packages/basic-modules/__tests__/header/helper.test.ts b/packages/basic-modules/__tests__/header/helper.test.ts index 7c4fdff4c..28b4a28c9 100644 --- a/packages/basic-modules/__tests__/header/helper.test.ts +++ b/packages/basic-modules/__tests__/header/helper.test.ts @@ -37,7 +37,7 @@ describe('header helper', () => { Transforms.setNodes(editor, { type: 'header1' }) expect(isMenuDisabled(editor)).toBeFalsy() - editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] }) + editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }], language: '' }] }) expect(isMenuDisabled(editor)).toBeTruthy() // 只能用于 p header // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code }) diff --git a/packages/basic-modules/__tests__/indent/menu/decrease-indent-menu.test.ts b/packages/basic-modules/__tests__/indent/menu/decrease-indent-menu.test.ts index ec9c52c6d..3da36a548 100644 --- a/packages/basic-modules/__tests__/indent/menu/decrease-indent-menu.test.ts +++ b/packages/basic-modules/__tests__/indent/menu/decrease-indent-menu.test.ts @@ -31,7 +31,7 @@ describe('decrease indent menu', () => { Transforms.setNodes(editor, { type: 'header1', children: [] }) expect(menu.isDisabled(editor)).toBeTruthy() // 没有 indent 则 disabled - editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] }) + editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }], language: '' }] }) expect(menu.isDisabled(editor)).toBeTruthy() // 除了 p header 之外,其他 type 不可用 indent // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code }) diff --git a/packages/basic-modules/__tests__/indent/menu/increase-indent-menu.test.ts b/packages/basic-modules/__tests__/indent/menu/increase-indent-menu.test.ts index acb18c5f8..705f50edd 100644 --- a/packages/basic-modules/__tests__/indent/menu/increase-indent-menu.test.ts +++ b/packages/basic-modules/__tests__/indent/menu/increase-indent-menu.test.ts @@ -31,7 +31,7 @@ describe('increase indent menu', () => { Transforms.setNodes(editor, { type: 'header1', children: [] }) expect(menu.isDisabled(editor)).toBeFalsy() - editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] }) + editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }], language: '' }] }) expect(menu.isDisabled(editor)).toBeTruthy() // 除了 p header 之外,其他 type 不可用 indent Transforms.setNodes(editor, { type: 'header1', children: [{ text: 'hello' }] }) menu.exec(editor, '') diff --git a/packages/basic-modules/__tests__/justify/menus.test.ts b/packages/basic-modules/__tests__/justify/menus.test.ts index 0e90c2ea5..c7d3b09ac 100644 --- a/packages/basic-modules/__tests__/justify/menus.test.ts +++ b/packages/basic-modules/__tests__/justify/menus.test.ts @@ -45,7 +45,7 @@ describe('justify menus', () => { editor.select(startLocation) expect(centerMenu.isDisabled(editor)).toBeFalsy() - editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] }) + editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }], language: '' }] }) expect(centerMenu.isDisabled(editor)).toBeTruthy() Transforms.removeNodes(editor, { mode: 'highest' }) diff --git a/packages/basic-modules/__tests__/line-height/line-height-menu.test.ts b/packages/basic-modules/__tests__/line-height/line-height-menu.test.ts index ffd94353e..5009604d5 100644 --- a/packages/basic-modules/__tests__/line-height/line-height-menu.test.ts +++ b/packages/basic-modules/__tests__/line-height/line-height-menu.test.ts @@ -63,7 +63,7 @@ describe('line-height menu', () => { Transforms.setNodes(editor, { type: 'list-item' }) expect(menu.isDisabled(editor)).toBeFalsy() - editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }] }] }) + editor.insertNode({ type: 'pre', children: [{ type: 'code', children: [{ text: 'var' }], language: '' }] }) expect(menu.isDisabled(editor)).toBeTruthy() // Transforms.removeNodes(editor, { mode: 'highest' }) // 移除 pre/code }) diff --git a/packages/basic-modules/__tests__/link/helper.test.ts b/packages/basic-modules/__tests__/link/helper.test.ts index e1abc9746..859f9716b 100644 --- a/packages/basic-modules/__tests__/link/helper.test.ts +++ b/packages/basic-modules/__tests__/link/helper.test.ts @@ -71,6 +71,7 @@ describe('link module helper', () => { { type: 'code', children: [{ text: 'var' }], + language: 'javascript', }, ], }) diff --git a/packages/basic-modules/__tests__/text-style/menu/menus.test.ts b/packages/basic-modules/__tests__/text-style/menu/menus.test.ts index ef326cf4c..8d71ac981 100644 --- a/packages/basic-modules/__tests__/text-style/menu/menus.test.ts +++ b/packages/basic-modules/__tests__/text-style/menu/menus.test.ts @@ -67,6 +67,7 @@ describe('text style menus', () => { { type: 'code', children: [{ text: 'var' }], + language: '', } as Element, ], } as Element) diff --git a/packages/basic-modules/__tests__/todo/menu/todo-menu.test.ts b/packages/basic-modules/__tests__/todo/menu/todo-menu.test.ts index b07d2d38a..b83dcc915 100644 --- a/packages/basic-modules/__tests__/todo/menu/todo-menu.test.ts +++ b/packages/basic-modules/__tests__/todo/menu/todo-menu.test.ts @@ -86,6 +86,7 @@ describe('todo-menu', () => { { type: 'code', children: [{ text: 'hello' }], + language: '', }, ], }) diff --git a/packages/basic-modules/src/modules/image/render-elem.tsx b/packages/basic-modules/src/modules/image/render-elem.tsx index b2f4a6142..0f867c147 100644 --- a/packages/basic-modules/src/modules/image/render-elem.tsx +++ b/packages/basic-modules/src/modules/image/render-elem.tsx @@ -183,7 +183,7 @@ function renderResizeContainer( if (parentNode == null) { return } const parentNodeDom = DomEditor.toDOMNode(editor, parentNode) - const rect = parentNodeDom.getBoundingClientRect() + const rect = parentNodeDom.getBoundingClientRect ? parentNodeDom.getBoundingClientRect() : { width: 0 } // 获取元素的计算样式 const style = window.getComputedStyle(parentNodeDom) // 获取左右 padding 和 border 的宽度 diff --git a/packages/code-highlight/__tests__/parse-html.test.ts b/packages/code-highlight/__tests__/parse-html.test.ts index 50b044601..be90e4200 100644 --- a/packages/code-highlight/__tests__/parse-html.test.ts +++ b/packages/code-highlight/__tests__/parse-html.test.ts @@ -13,7 +13,7 @@ describe('code highlight - parse style html', () => { it('v5 format', () => { const $code = $('') // v5 html format - const code = { type: 'code', children: [{ text: 'var a = 100;' }] } + const code = { type: 'code', children: [{ text: 'var a = 100;' }], language: '' } const res = parseCodeStyleHtml($code[0], code, editor) @@ -26,7 +26,7 @@ describe('code highlight - parse style html', () => { it('v4 format', () => { const $code = $('') // v4 html format - const code = { type: 'code', children: [{ text: 'var a = 100;' }] } + const code = { type: 'code', children: [{ text: 'var a = 100;' }], language: '' } const res = parseCodeStyleHtml($code[0], code, editor) diff --git a/packages/core/__tests__/config/editor-config.test.ts b/packages/core/__tests__/config/editor-config.test.ts index d21a70386..e24cd29ec 100644 --- a/packages/core/__tests__/config/editor-config.test.ts +++ b/packages/core/__tests__/config/editor-config.test.ts @@ -147,6 +147,6 @@ describe('editor config', () => { setTimeout(() => { expect(fn).toHaveBeenCalledWith(editor) - }, 20) + }, 100) }) }) diff --git a/packages/core/__tests__/editor/plugins/with-dom.test.ts b/packages/core/__tests__/editor/plugins/with-dom.test.ts index d49361126..b72e1851a 100644 --- a/packages/core/__tests__/editor/plugins/with-dom.test.ts +++ b/packages/core/__tests__/editor/plugins/with-dom.test.ts @@ -104,10 +104,10 @@ describe('editor DOM API', () => { }) it('foucus', () => { - const editor = createEditor() + const editor = createEditor( + { content: [{ type: 'paragraph', children: [{ text: 'Hello' }] }] }, + ) - editor.focus() - editor.insertText('hello') editor.focus() // 测试选区定位到开始 expect(editor.selection).toStrictEqual({ diff --git a/packages/core/src/editor/dom-editor.ts b/packages/core/src/editor/dom-editor.ts index a3f97f028..bcce8def1 100644 --- a/packages/core/src/editor/dom-editor.ts +++ b/packages/core/src/editor/dom-editor.ts @@ -520,9 +520,11 @@ export const DomEditor = { // Calculate how far into the text node the `nearestNode` is, so that we // can determine what the offset relative to the text node is. - if (leafNode) { + const window = DomEditor.getWindow(editor) + + if (leafNode && window.document.createRange) { textNode = leafNode.closest('[data-slate-node="text"]')! - const window = DomEditor.getWindow(editor) + const range = window.document.createRange() range.setStart(textNode, 0) diff --git a/packages/core/src/menus/bar/HoverBar.ts b/packages/core/src/menus/bar/HoverBar.ts index 694bed14f..d74dabbac 100644 --- a/packages/core/src/menus/bar/HoverBar.ts +++ b/packages/core/src/menus/bar/HoverBar.ts @@ -4,19 +4,24 @@ */ import debounce from 'lodash.debounce' -import { Editor, Node, Element, Text, Path, Range } from 'slate' +import { + Editor, Element, Node, Path, Range, Text, +} from 'slate' + +import { CustomElement } from '../../../../custom-types' +import { DomEditor } from '../../editor/dom-editor' +import { IDomEditor } from '../../editor/interface' +import { i18nListenLanguage } from '../../i18n' import $ from '../../utils/dom' -import { MENU_ITEM_FACTORIES } from '../register' import { promiseResolveThen } from '../../utils/util' -import { IDomEditor } from '../../editor/interface' -import { DomEditor } from '../../editor/dom-editor' -import { HOVER_BAR_TO_EDITOR, BAR_ITEM_TO_EDITOR } from '../../utils/weak-maps' -import { IBarItem, createBarItem } from '../bar-item/index' +import { BAR_ITEM_TO_EDITOR, HOVER_BAR_TO_EDITOR } from '../../utils/weak-maps' +import { createBarItem, IBarItem } from '../bar-item/index' import { gen$barItemDivider } from '../helpers/helpers' -import { getPositionBySelection, getPositionByNode, correctPosition } from '../helpers/position' -import { IButtonMenu, ISelectMenu, IDropPanelMenu, IModalMenu } from '../interface' -import { CustomElement } from '../../../../custom-types' -import { i18nListenLanguage } from '../../i18n' +import { correctPosition, getPositionByNode, getPositionBySelection } from '../helpers/position' +import { + IButtonMenu, IDropPanelMenu, IModalMenu, ISelectMenu, +} from '../interface' +import { MENU_ITEM_FACTORIES } from '../register' type MenuType = IButtonMenu | ISelectMenu | IDropPanelMenu | IModalMenu @@ -27,28 +32,37 @@ type MenuType = IButtonMenu | ISelectMenu | IDropPanelMenu | IModalMenu */ function isSelectedText(editor: IDomEditor, n: Node) { const { selection } = editor - if (selection == null) return false // 无选区 - if (Range.isCollapsed(selection)) return false // 未选中文字,选区的是折叠的 + + if (selection == null) { return false } // 无选区 + if (Range.isCollapsed(selection)) { return false } // 未选中文字,选区的是折叠的 const selectedElems = DomEditor.getSelectedElems(editor) const notMatch = selectedElems.some((elem: CustomElement) => { - if (editor.isVoid(elem)) return true + if (editor.isVoid(elem)) { return true } const { type } = elem - if (['pre', 'code', 'table'].includes(type)) return true + + if (['pre', 'code', 'table'].includes(type)) { return true } + return false }) - if (notMatch) return false - if (Text.isText(n)) return true // 匹配 text node + if (notMatch) { return false } + + if (Text.isText(n)) { return true } // 匹配 text node return false } class HoverBar { private readonly $elem = $('
') + private menus: { [key: string]: MenuType } = {} + private hoverbarItems: IBarItem[] = [] + private prevSelectedNode: Node | null = null // 上一次选中的 node + private isShow = false + private lngListen: () => void = () => {} constructor() { @@ -59,8 +73,10 @@ class HoverBar { // 将 elem 渲染为 DOM const $elem = this.$elem // @ts-ignore + $elem.on('mousedown', e => e.preventDefault(), { passive: false }) // 防止点击失焦 const textarea = DomEditor.getTextarea(editor) + textarea.$textAreaContainer.append($elem) // 绑定 editor onchange @@ -68,6 +84,7 @@ class HoverBar { // 滚动时隐藏 const hideAndClean = this.hideAndClean.bind(this) + editor.on('scroll', hideAndClean) // fullScreen 时隐藏 @@ -83,6 +100,7 @@ class HoverBar { this.hideAndClean() // xxx const editor = this.getEditorInstance() + editor.deselect() }) } @@ -93,6 +111,7 @@ class HoverBar { hideAndClean() { const $elem = this.$elem + $elem.removeClass('w-e-bar-show').addClass('w-e-bar-hidden') // 及时先清空内容,否则影响下次 @@ -110,10 +129,12 @@ class HoverBar { const $elem = this.$elem let isBottom = false - const { innerHeight } = window + const innerHeight = typeof window !== 'undefined' ? window.innerHeight : null const minDistance = 360 // 距离底部最小 360px + if (innerHeight && innerHeight >= minDistance) { const { bottom } = $elem[0].getBoundingClientRect() + if (innerHeight - bottom < minDistance) { // hoverbar 距离底部不足 360 isBottom = true @@ -149,6 +170,7 @@ class HoverBar { if (key === '|') { // 分割线 const $divider = gen$barItemDivider() + $elem.append($divider) return } @@ -169,6 +191,7 @@ class HoverBar { if (menu == null) { // 缓存获取失败,则重新创建 const factory = MENU_ITEM_FACTORIES[key] + if (factory == null) { throw new Error(`Not found menu item factory by key '${key}'`) } @@ -181,13 +204,15 @@ class HoverBar { menus[key] = menu } - //替换 icon svg + // 替换 icon svg const menuConf = editor.getMenuConfig(key) + if (menuConf && menuConf.iconSvg !== undefined) { menu.iconSvg = menuConf.iconSvg } const barItem = createBarItem(key, menu) + this.hoverbarItems.push(barItem) // 保存 barItem 和 editor 的关系 @@ -195,17 +220,20 @@ class HoverBar { // 添加 DOM const $elem = this.$elem + $elem.append(barItem.$elem) } private setPosition(node: Node) { const editor = this.getEditorInstance() const $elem = this.$elem + $elem.attr('style', '') // 先清空 style ,再重新设置 if (Element.isElement(node)) { // 根据 elem node 定位 const positionStyle = getPositionByNode(editor, node, 'bar') + $elem.css(positionStyle) correctPosition(editor, $elem) // 修正 position 避免超出 textContainer 边界 return @@ -213,6 +241,7 @@ class HoverBar { if (Text.isText(node)) { // text node ,根据选区定位 const positionStyle = getPositionBySelection(editor) + $elem.css(positionStyle) correctPosition(editor, $elem) // 修正 position 避免超出 textContainer 边界 return @@ -237,14 +266,13 @@ class HoverBar { let matchNode: Node | null = null let matchMenuKeys: string[] = [] + // eslint-disable-next-line guard-for-in for (const elemType in keysConf) { const conf = keysConf[elemType] const { match, menuKeys = [] } = conf // 定义了 match 则用 match 。未定义 match 则用 elemType - const matchFn = match - ? match - : (editor: IDomEditor, n: Node) => DomEditor.checkNodeType(n, elemType) + const matchFn = match || ((_editor: IDomEditor, n: Node) => DomEditor.checkNodeType(n, elemType)) const [nodeEntry] = Editor.nodes(editor, { match: n => matchFn(editor, n), @@ -260,7 +288,7 @@ class HoverBar { } // 未匹配成功 - if (matchNode == null || matchMenuKeys.length === 0) return null + if (matchNode == null || matchMenuKeys.length === 0) { return null } // 匹配成功 return { @@ -286,6 +314,7 @@ class HoverBar { if (isShow) { // hoverbar 当前已显示 const samePath = this.isSamePath(node, this.prevSelectedNode) + if (samePath) { // 和之前选中的 node path 相同 —— 满足这些条件,即终止 return @@ -309,7 +338,8 @@ class HoverBar { private getEditorInstance(): IDomEditor { const editor = HOVER_BAR_TO_EDITOR.get(this) - if (editor == null) throw new Error('Can not get editor instance') + + if (editor == null) { throw new Error('Can not get editor instance') } return editor } @@ -318,6 +348,7 @@ class HoverBar { const { hoverbarKeys = {} } = editor.getConfig() const textHoverbarKeys = hoverbarKeys.text + if (textHoverbarKeys && textHoverbarKeys.match == null) { // 对 text hoverbarKeys 增加 match 函数(否则无法判断是否选中了 text) textHoverbarKeys.match = isSelectedText @@ -337,6 +368,7 @@ class HoverBar { const path1 = DomEditor.findPath(null, node1) const path2 = DomEditor.findPath(null, node2) const res = Path.equals(path1, path2) + return res } diff --git a/packages/core/src/menus/helpers/position.ts b/packages/core/src/menus/helpers/position.ts index ae8d1efeb..f22a69591 100644 --- a/packages/core/src/menus/helpers/position.ts +++ b/packages/core/src/menus/helpers/position.ts @@ -3,13 +3,14 @@ * @author wangfupeng */ -import { Node, Element } from 'slate' -import { Dom7Array, getFirstVoidChild } from '../../utils/dom' -import { IDomEditor } from '../../editor/interface' +import { Element, Node } from 'slate' + import { DomEditor } from '../../editor/dom-editor' +import { IDomEditor } from '../../editor/interface' +import { Dom7Array, getFirstVoidChild } from '../../utils/dom' +import { promiseResolveThen } from '../../utils/util' import { NODE_TO_ELEMENT } from '../../utils/weak-maps' import { IPositionStyle } from '../interface' -import { promiseResolveThen } from '../../utils/util' /** * 获取 textContainer 尺寸和定位 @@ -29,7 +30,9 @@ export function getTextContainerRect(editor: IDomEditor): { const height = $textareaContainer.height() const { top, left } = $textareaContainer.offset() - return { top, left, width, height } + return { + top, left, width, height, + } } /** @@ -41,11 +44,13 @@ export function getPositionBySelection(editor: IDomEditor): Partial = {} // 获取 选区 top left 和 container top left 的差值(< 0 则使用 0) - let relativeTop = rangeTop - containerTop - let relativeLeft = rangeLeft - containerLeft + const relativeTop = rangeTop - containerTop + const relativeLeft = rangeLeft - containerLeft // 判断水平位置: modal/bar 显示在选区左侧,还是右侧? if (relativeLeft > containerWidth / 2) { // 选区 left 大于 containerWidth/2 (选区在 container 的右侧),则 modal/bar 显示在选区左侧 - let r = containerWidth - relativeLeft + const r = containerWidth - relativeLeft + positionStyle.right = `${r + 5}px` // 5px 间隔 } else { // 否则(选区在 container 的左侧),modal/bar 显示在选区右侧 @@ -79,12 +88,14 @@ export function getPositionBySelection(editor: IDomEditor): Partial containerHeight / 2) { // 选区 top > containerHeight/2 (选区在 container 的下半部分),则 modal/bar 显示在选区的上面 - let b = containerHeight - relativeTop + const b = containerHeight - relativeTop + positionStyle.bottom = `${b + 5}px` // 5px 间隔 } else { // 否则(选区在 container 的上半部分),则 modal/bar 显示在选区的下面 let t = relativeTop + rangeHeight - if (t < 0) t = 0 + + if (t < 0) { t = 0 } positionStyle.top = `${t + 5}px` // 5px 间隔 } @@ -100,30 +111,37 @@ export function getPositionBySelection(editor: IDomEditor): Partial { // 默认情况下 { top: 0, left: 0 } const defaultStyle = { top: '0', left: '0' } const { selection } = editor - if (selection == null) return defaultStyle // 默认 position + + if (selection == null) { return defaultStyle } // 默认 position // 根据 node 获取 elem const isVoidElem = Element.isElement(node) && editor.isVoid(node) const isInlineElem = Element.isElement(node) && editor.isInline(node) const elem = NODE_TO_ELEMENT.get(node) - if (elem == null) return defaultStyle // 默认 position + + if (elem == null) { return defaultStyle } // 默认 position let { top: elemTop, + // eslint-disable-next-line prefer-const left: elemLeft, height: elemHeight, + // eslint-disable-next-line prefer-const width: elemWidth, } = elem.getBoundingClientRect() + if (isVoidElem) { // void node ,重新计算 top 和 height const voidElem = getFirstVoidChild(elem) + if (voidElem != null) { const { top, height } = voidElem.getBoundingClientRect() + elemTop = top elemHeight = height } @@ -131,7 +149,8 @@ export function getPositionByNode( // 获取 textContainer rect const containerRect = getTextContainerRect(editor) - if (containerRect == null) return defaultStyle // 默认 position + + if (containerRect == null) { return defaultStyle } // 默认 position const { top: containerTop, left: containerLeft, @@ -143,8 +162,8 @@ export function getPositionByNode( const positionStyle: Partial = {} // 获取 elem top left 和 container top left 的差值(< 0 则使用 0) - let relativeTop = elemTop - containerTop - let relativeLeft = elemLeft - containerLeft + const relativeTop = elemTop - containerTop + const relativeLeft = elemLeft - containerLeft if (type === 'bar') { // bar - 1. left 对齐 elem.left ;2. 尽量显示在 elem 上方 @@ -167,39 +186,37 @@ export function getPositionByNode( if (!isVoidElem) { // 非 void node - left 和 elem left 对齐 positionStyle.left = `${relativeLeft}px` - } else { - if (isInlineElem) { - // inline void node 需要计算 - if (relativeLeft > (containerWidth - elemWidth) / 2) { - // elem 在 container 的右侧,则 modal 显示在 elem 左侧 - positionStyle.right = `${containerWidth - relativeLeft + 5}px` - } else { - // 否则 elem 在 container 左侧,则 modal 显示在 elem 右侧 - positionStyle.left = `${relativeLeft + elemWidth + 5}px` - } + } else if (isInlineElem) { + // inline void node 需要计算 + if (relativeLeft > (containerWidth - elemWidth) / 2) { + // elem 在 container 的右侧,则 modal 显示在 elem 左侧 + positionStyle.right = `${containerWidth - relativeLeft + 5}px` } else { - // block void node 水平靠左即可 - positionStyle.left = `20px` + // 否则 elem 在 container 左侧,则 modal 显示在 elem 右侧 + positionStyle.left = `${relativeLeft + elemWidth + 5}px` } + } else { + // block void node 水平靠左即可 + positionStyle.left = '20px' } // 垂直 if (isVoidElem) { // void node - top 和 elem top 对齐 let t = relativeTop - if (t < 0) t = 0 // top 不能小于 0 + + if (t < 0) { t = 0 } // top 不能小于 0 positionStyle.top = `${t}px` - } else { // 非 void node ,计算 top - if (relativeTop > (containerHeight - elemHeight) / 2) { - // elem 在 container 的下半部分,则 modal 显示在 elem 上方 - positionStyle.bottom = `${containerHeight - relativeTop + 5}px` - } else { - // elem 在 container 的上半部分,则 modal 显示在 elem 下方 - let t = relativeTop + elemHeight - if (t < 0) t = 0 - positionStyle.top = `${t + 5}px` - } + } else if (relativeTop > (containerHeight - elemHeight) / 2) { + // elem 在 container 的下半部分,则 modal 显示在 elem 上方 + positionStyle.bottom = `${containerHeight - relativeTop + 5}px` + } else { + // elem 在 container 的上半部分,则 modal 显示在 elem 下方 + let t = relativeTop + elemHeight + + if (t < 0) { t = 0 } + positionStyle.top = `${t + 5}px` } return positionStyle @@ -218,7 +235,8 @@ export function correctPosition(editor: IDomEditor, $positionElem: Dom7Array) { promiseResolveThen(() => { // 获取 textContainer rect const containerRect = getTextContainerRect(editor) - if (containerRect == null) return + + if (containerRect == null) { return } const { top: containerTop, left: containerLeft, @@ -239,12 +257,14 @@ export function correctPosition(editor: IDomEditor, $positionElem: Dom7Array) { if (styleStr.indexOf('top') >= 0) { // 设置了 top ,则有可能超过 textContainer 的下边界 const d = relativeTop + positionElemHeight - containerHeight + if (d > 0) { // 已超过 textContainer 的下边界,则上移 const curTopStr = $positionElem.css('top') - const curTop = parseInt(curTopStr.toString()) + const curTop = parseInt(curTopStr.toString(), 10) let newTop = curTop - d - if (newTop < 0) newTop = 0 // 不能超过 textContainer 上边界 + + if (newTop < 0) { newTop = 0 } // 不能超过 textContainer 上边界 $positionElem.css('top', `${newTop}px`) } } @@ -254,8 +274,9 @@ export function correctPosition(editor: IDomEditor, $positionElem: Dom7Array) { if (positionElemTop < 0) { // 已超出了上边界 const curBottomStr = $positionElem.css('bottom') - const curBottom = parseInt(curBottomStr.toString()) + const curBottom = parseInt(curBottomStr.toString(), 10) const newBottom = curBottom - Math.abs(positionElemTop) // 保证上边界和 textContainer 对齐即可,下边界不管 + $positionElem.css('bottom', `${newBottom}px`) } } @@ -263,12 +284,14 @@ export function correctPosition(editor: IDomEditor, $positionElem: Dom7Array) { if (styleStr.indexOf('left') >= 0) { // 设置了 left ,则有可能超过 textContainer 的右边界 const d = relativeLeft + positionElemWidth - containerWidth + if (d > 0) { // 已超过 textContainer 的右边界,需左移 const curLeftStr = $positionElem.css('left') - const curLeft = parseInt(curLeftStr.toString()) + const curLeft = parseInt(curLeftStr.toString(), 10) let newLeft = curLeft - d - if (newLeft < 0) newLeft = 0 // 不能超过 textContainer 左边界 + + if (newLeft < 0) { newLeft = 0 } // 不能超过 textContainer 左边界 $positionElem.css('left', `${newLeft}px`) } } @@ -278,8 +301,9 @@ export function correctPosition(editor: IDomEditor, $positionElem: Dom7Array) { if (positionElemLeft < 0) { // 已超出了左边界 const curRightStr = $positionElem.css('right') - const curRight = parseInt(curRightStr.toString()) + const curRight = parseInt(curRightStr.toString(), 10) const newRight = curRight - Math.abs(positionElemLeft) // 保证左边界和 textContainer 对齐即可,右边界不管 + $positionElem.css('right', `${newRight}px`) } } diff --git a/packages/core/src/text-area/syncSelection.ts b/packages/core/src/text-area/syncSelection.ts index 93501c5c5..6ca914dd8 100644 --- a/packages/core/src/text-area/syncSelection.ts +++ b/packages/core/src/text-area/syncSelection.ts @@ -3,16 +3,16 @@ * @author wangfupeng */ -import { Range, Transforms } from 'slate' import scrollIntoView from 'scroll-into-view-if-needed' +import { Range, Transforms } from 'slate' -import { IDomEditor } from '../editor/interface' import { DomEditor } from '../editor/dom-editor' -import TextArea from './TextArea' -import { EDITOR_TO_ELEMENT, IS_FOCUSED } from '../utils/weak-maps' +import { IDomEditor } from '../editor/interface' +import { DOMElement } from '../utils/dom' import { IS_FIREFOX } from '../utils/ua' +import { EDITOR_TO_ELEMENT, IS_FOCUSED } from '../utils/weak-maps' import { hasEditableTarget, isTargetInsideNonReadonlyVoid } from './helpers' -import { DOMElement } from '../utils/dom' +import TextArea from './TextArea' /** * editor onchange 时,将 editor selection 同步给 DOM @@ -26,21 +26,22 @@ export function editorSelectionToDOM(textarea: TextArea, editor: IDomEditor, foc const root = DomEditor.findDocumentOrShadowRoot(editor) const domSelection = root.getSelection() - if (!domSelection) return - if (textarea.isComposing && !focus) return - if (!editor.isFocused()) return + if (!domSelection) { return } + if (textarea.isComposing && !focus) { return } + if (!editor.isFocused()) { return } const hasDomSelection = domSelection.type !== 'None' // If the DOM selection is properly unset, we're done. - if (!selection && !hasDomSelection) return + if (!selection && !hasDomSelection) { return } // verify that the dom selection is in the editor const editorElement = EDITOR_TO_ELEMENT.get(editor)! let hasDomSelectionInEditor = false + if ( - editorElement.contains(domSelection.anchorNode) && - editorElement.contains(domSelection.focusNode) + editorElement.contains(domSelection.anchorNode) + && editorElement.contains(domSelection.focusNode) ) { hasDomSelectionInEditor = true } @@ -54,12 +55,14 @@ export function editorSelectionToDOM(textarea: TextArea, editor: IDomEditor, foc // (e.g. when clicking on contentEditable:false element) suppressThrow: true, }) + if (slateRange && Range.equals(slateRange, selection)) { let canReturn = true // 选区在 table 时,需要特殊处理 if (Range.isCollapsed(selection)) { const { anchorNode, anchorOffset } = domSelection + if (anchorNode === editorElement) { const childNodes = editorElement.childNodes let tableElem @@ -79,7 +82,7 @@ export function editorSelectionToDOM(textarea: TextArea, editor: IDomEditor, foc } // 其他情况,就此结束 - if (canReturn) return + if (canReturn) { return } } } @@ -99,31 +102,33 @@ export function editorSelectionToDOM(textarea: TextArea, editor: IDomEditor, foc textarea.isUpdatingSelection = true const newDomRange = selection && DomEditor.toDOMRange(editor, selection) + if (newDomRange) { if (Range.isBackward(selection!)) { domSelection.setBaseAndExtent( newDomRange.endContainer, newDomRange.endOffset, newDomRange.startContainer, - newDomRange.startOffset + newDomRange.startOffset, ) } else { domSelection.setBaseAndExtent( newDomRange.startContainer, newDomRange.startOffset, newDomRange.endContainer, - newDomRange.endOffset + newDomRange.endOffset, ) } // 滚动到选区 - let leafEl = newDomRange.startContainer.parentElement! as Element + const leafEl = newDomRange.startContainer.parentElement! as Element const spacer = leafEl.closest('[data-slate-spacer]') // 这个 if 防止选中图片时发生滚动 - if (!spacer) { + if (!spacer && newDomRange.getBoundingClientRect) { leafEl.getBoundingClientRect = newDomRange.getBoundingClientRect.bind(newDomRange) const body = document.body + scrollIntoView(leafEl, { scrollMode: 'if-needed', boundary: config.scroll ? editorElement.parentElement : body, // issue 4215 @@ -157,10 +162,10 @@ export function DOMSelectionToEditor(textarea: TextArea, editor: IDomEditor) { const { isComposing, isUpdatingSelection, isDraggingInternally } = textarea const config = editor.getConfig() - if (config.readOnly) return - if (isComposing) return - if (isUpdatingSelection) return - if (isDraggingInternally) return + if (config.readOnly) { return } + if (isComposing) { return } + if (isUpdatingSelection) { return } + if (isDraggingInternally) { return } const root = DomEditor.findDocumentOrShadowRoot(editor) const { activeElement } = root @@ -182,16 +187,15 @@ export function DOMSelectionToEditor(textarea: TextArea, editor: IDomEditor) { const { anchorNode, focusNode } = domSelection - const anchorNodeSelectable = - hasEditableTarget(editor, anchorNode) || isTargetInsideNonReadonlyVoid(editor, anchorNode) - const focusNodeSelectable = - hasEditableTarget(editor, focusNode) || isTargetInsideNonReadonlyVoid(editor, focusNode) + const anchorNodeSelectable = hasEditableTarget(editor, anchorNode) || isTargetInsideNonReadonlyVoid(editor, anchorNode) + const focusNodeSelectable = hasEditableTarget(editor, focusNode) || isTargetInsideNonReadonlyVoid(editor, focusNode) if (anchorNodeSelectable && focusNodeSelectable) { const range = DomEditor.toSlateRange(editor, domSelection, { exactMatch: false, suppressThrow: false, }) + Transforms.select(editor, range) } else { // 禁用此行,让光标选区继续生效 diff --git a/packages/core/src/utils/dom.ts b/packages/core/src/utils/dom.ts index 8c6284d5d..087605ec2 100644 --- a/packages/core/src/utils/dom.ts +++ b/packages/core/src/utils/dom.ts @@ -3,73 +3,87 @@ * @author wangfupeng */ -import { htmlVoidElements } from 'html-void-elements' import $, { - css, - append, addClass, - removeClass, - hasClass, - on, - focus, + append, attr, + children as dom7Children, + css, + dataset, + Dom7Array, + each, + empty, + find, + focus, + hasClass, + height, hide, - show, + html, + is, // scrollTop, // scrollLeft, - offset, - width, - height, - parent, + offset as dom7Offset, + on, + parent as dom7Parent, parents, - is, - dataset, - val, - text, - removeAttr, - children, - html, remove, - find, - each, - empty, - Dom7Array, + removeAttr, + removeClass, + show, + text as dom7Text, + val, + width, } from 'dom7' +import { htmlVoidElements } from 'html-void-elements' + +import { toString } from './util' + +// ------------------------------- 分割线,以下内容参考 slate-react dom.ts ------------------------------- + +// COMPAT: This is required to prevent TypeScript aliases from doing some very +// weird things for Slate's types with the same name as globals. (2019/11/27) +// https://github.com/microsoft/TypeScript/issues/35002 +import DOMNode = globalThis.Node +import DOMComment = globalThis.Comment +import DOMElement = globalThis.Element +import DOMText = globalThis.Text +import DOMRange = globalThis.Range +import DOMSelection = globalThis.Selection +import DOMStaticRange = globalThis.StaticRange + export { Dom7Array } from 'dom7' -if (css) $.fn.css = css -if (append) $.fn.append = append -if (addClass) $.fn.addClass = addClass -if (removeClass) $.fn.removeClass = removeClass -if (hasClass) $.fn.hasClass = hasClass -if (on) $.fn.on = on -if (focus) $.fn.focus = focus -if (attr) $.fn.attr = attr -if (removeAttr) $.fn.removeAttr = removeAttr -if (hide) $.fn.hide = hide -if (show) $.fn.show = show +if (css) { $.fn.css = css } +if (append) { $.fn.append = append } +if (addClass) { $.fn.addClass = addClass } +if (removeClass) { $.fn.removeClass = removeClass } +if (hasClass) { $.fn.hasClass = hasClass } +if (on) { $.fn.on = on } +if (focus) { $.fn.focus = focus } +if (attr) { $.fn.attr = attr } +if (removeAttr) { $.fn.removeAttr = removeAttr } +if (hide) { $.fn.hide = hide } +if (show) { $.fn.show = show } // if (scrollTop) $.fn.scrollTop = scrollTop // if (scrollLeft) $.fn.scrollLeft = scrollLeft -if (offset) $.fn.offset = offset -if (width) $.fn.width = width -if (height) $.fn.height = height -if (parent) $.fn.parent = parent -if (parents) $.fn.parents = parents -if (is) $.fn.is = is -if (dataset) $.fn.dataset = dataset -if (val) $.fn.val = val -if (text) $.fn.text = text -if (html) $.fn.html = html -if (children) $.fn.children = children -if (remove) $.fn.remove = remove -if (find) $.fn.find = find -if (each) $.fn.each = each -if (empty) $.fn.empty = empty +if (dom7Offset) { $.fn.offset = dom7Offset } +if (width) { $.fn.width = width } +if (height) { $.fn.height = height } +if (dom7Parent) { $.fn.parent = dom7Parent } +if (parents) { $.fn.parents = parents } +if (is) { $.fn.is = is } +if (dataset) { $.fn.dataset = dataset } +if (val) { $.fn.val = val } +if (dom7Text) { $.fn.text = dom7Text } +if (html) { $.fn.html = html } +if (dom7Children) { $.fn.children = dom7Children } +if (remove) { $.fn.remove = remove } +if (find) { $.fn.find = find } +if (each) { $.fn.each = each } +if (empty) { $.fn.empty = empty } export default $ -import { toString } from './util' - export const isDocument = (value: any): value is Document => { return toString(value) === '[object HTMLDocument]' } @@ -83,23 +97,13 @@ export const isDataTransfer = (value: any): value is DataTransfer => { } const HTML_ELEMENT_STR_REG_EXP = /\[object HTML([A-Z][a-z]*)*Element\]/ + export const isHTMLElememt = (value: any): value is HTMLElement => { return HTML_ELEMENT_STR_REG_EXP.test(toString(value)) } - -// ------------------------------- 分割线,以下内容参考 slate-react dom.ts ------------------------------- - -// COMPAT: This is required to prevent TypeScript aliases from doing some very -// weird things for Slate's types with the same name as globals. (2019/11/27) -// https://github.com/microsoft/TypeScript/issues/35002 -import DOMNode = globalThis.Node -import DOMComment = globalThis.Comment -import DOMElement = globalThis.Element -import DOMText = globalThis.Text -import DOMRange = globalThis.Range -import DOMSelection = globalThis.Selection -import DOMStaticRange = globalThis.StaticRange -export { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange } +export { + DOMComment, DOMElement, DOMNode, DOMRange, DOMSelection, DOMStaticRange, DOMText, +} export type DOMPoint = [Node, number] @@ -110,6 +114,13 @@ export const getDefaultView = (value: any): Window | null => { return (value && value.ownerDocument && value.ownerDocument.defaultView) || null } +/** + * Check if a value is a DOM node. + */ +export const isDOMNode = (value: any): value is DOMNode => { + return value != null && typeof value.nodeType === 'number' +} + /** * Check if a DOM node is a comment node. */ @@ -124,13 +135,6 @@ export const isDOMElement = (value: any): value is DOMElement => { return isDOMNode(value) && value.nodeType === 1 } -/** - * Check if a value is a DOM node. - */ -export const isDOMNode = (value: any): value is DOMNode => { - return value != null && typeof value.nodeType === 'number' -} - /** * Check if a value is a DOM selection. */ @@ -150,9 +154,9 @@ export const isDOMText = (value: any): value is DOMText => { */ export const isPlainTextOnlyPaste = (event: ClipboardEvent) => { return ( - event.clipboardData && - event.clipboardData.getData('text/plain') !== '' && - event.clipboardData.types.length === 1 + event.clipboardData + && event.clipboardData.getData('text/plain') !== '' + && event.clipboardData.types.length === 1 ) } @@ -166,8 +170,10 @@ export const normalizeDOMPoint = (domPoint: DOMPoint): DOMPoint => { // including comment nodes, so try to find the right text child node. if (isDOMElement(node) && node.childNodes.length) { let isLast = offset === node.childNodes.length - let index = isLast ? offset - 1 : offset - ;[node, index] = getEditableChildAndIndex(node, index, isLast ? 'backward' : 'forward') + let index = isLast ? offset - 1 : offset; + + // eslint-disable-next-line @typescript-eslint/no-use-before-define + [node, index] = getEditableChildAndIndex(node, index, isLast ? 'backward' : 'forward') // If the editable child found is in front of input offset, we instead seek to its end // 如果编辑区域的内容被发现在输入光标位置前面,也就是光标位置不正常,则修正光标的位置到结尾 @@ -177,6 +183,8 @@ export const normalizeDOMPoint = (domPoint: DOMPoint): DOMPoint => { // can be either text nodes, or other void DOM nodes. while (isDOMElement(node) && node.childNodes.length) { const i = isLast ? node.childNodes.length - 1 : 0 + + // eslint-disable-next-line @typescript-eslint/no-use-before-define node = getEditableChild(node, i, isLast ? 'backward' : 'forward') } @@ -200,8 +208,8 @@ export const hasShadowRoot = () => { */ export const getElementById = (id: string): null | HTMLElement => { return ( - window.document.getElementById(id) ?? - (window.document.activeElement?.shadowRoot?.getElementById(id) || null) + (window && window.document.getElementById(id)) + ?? ((window && window.document.activeElement?.shadowRoot?.getElementById(id)) || null) ) } @@ -211,7 +219,7 @@ export const getElementById = (id: string): null | HTMLElement => { export const getEditableChildAndIndex = ( parent: DOMElement, index: number, - direction: 'forward' | 'backward' + direction: 'forward' | 'backward', ): [DOMNode, number] => { const { childNodes } = parent let child = childNodes[index] @@ -222,9 +230,9 @@ export const getEditableChildAndIndex = ( // While the child is a comment node, or an element node with no children, // keep iterating to find a sibling non-void, non-comment node. while ( - isDOMComment(child) || - (isDOMElement(child) && child.childNodes.length === 0) || - (isDOMElement(child) && child.getAttribute('contenteditable') === 'false') + isDOMComment(child) + || (isDOMElement(child) && child.childNodes.length === 0) + || (isDOMElement(child) && child.getAttribute('contenteditable') === 'false') ) { if (triedForward && triedBackward) { break @@ -260,9 +268,10 @@ export const getEditableChildAndIndex = ( export const getEditableChild = ( parent: DOMElement, index: number, - direction: 'forward' | 'backward' + direction: 'forward' | 'backward', ): DOMNode => { const [child] = getEditableChildAndIndex(parent, index, direction) + return child } @@ -287,10 +296,10 @@ export const getPlainText = (domNode: DOMNode) => { const display = getComputedStyle(domNode).getPropertyValue('display') if ( - display === 'block' || - display === 'list' || - display === 'table-row' || - domNode.tagName === 'BR' + display === 'block' + || display === 'list' + || display === 'table-row' + || domNode.tagName === 'BR' ) { text += '\n' } @@ -306,6 +315,7 @@ export const getPlainText = (domNode: DOMNode) => { export function getFirstVoidChild(elem: DOMElement): DOMElement | null { // 深度优先遍历 const stack: Array = [] + stack.push(elem) let num = 0 @@ -313,19 +323,22 @@ export function getFirstVoidChild(elem: DOMElement): DOMElement | null { // 开始遍历 while (stack.length > 0) { const curElem = stack.pop() - if (curElem == null) break - num++ - if (num > 10000) break + if (curElem == null) { break } + + num += 1 + if (num > 10000) { break } const { nodeName, nodeType } = curElem + if (nodeType === 1) { const name = nodeName.toLowerCase() + if ( - htmlVoidElements.includes(name) || + htmlVoidElements.includes(name) // 补充一些 - name === 'iframe' || - name === 'video' + || name === 'iframe' + || name === 'video' ) { return curElem // 得到 void elem 并返回 } @@ -333,8 +346,9 @@ export function getFirstVoidChild(elem: DOMElement): DOMElement | null { // 继续遍历子节点 const children = curElem.children || [] const length = children.length + if (length) { - for (let i = length - 1; i >= 0; i--) { + for (let i = length - 1; i >= 0; i -= 1) { // 注意,需要**逆序**追加自节点 stack.push(children[i]) } @@ -353,18 +367,20 @@ export function getFirstVoidChild(elem: DOMElement): DOMElement | null { */ export function walkTextNodes( elem: DOMElement, - handler: (textNode: DOMNode, parent: DOMElement) => void + handler: (textNode: DOMNode, parent: DOMElement) => void, ) { // void elem 内部的 text 不处理 - if (isHTMLElememt(elem) && elem.dataset.slateVoid === 'true') return + if (isHTMLElememt(elem) && elem.dataset.slateVoid === 'true') { return } - for (let nodes = elem.childNodes, i = nodes.length; i--; ) { + // eslint-disable-next-line no-cond-assign + for (let nodes = elem.childNodes, i = nodes.length; i -= 1;) { const node = nodes[i] const nodeType = node.nodeType - if (nodeType == 3) { + + if (nodeType === 3) { // 匹配到 text node ,执行函数 handler(node, elem) - } else if (nodeType == 1 || nodeType == 9 || nodeType == 11) { + } else if (nodeType === 1 || nodeType === 9 || nodeType === 11) { // 继续遍历子节点 walkTextNodes(node as DOMElement, handler) } @@ -387,8 +403,9 @@ export enum NodeType { * @param $elem $elem */ export function getTagName($elem: Dom7Array): string { - if ($elem.length === 0) return '' + if ($elem.length === 0) { return '' } const elem = $elem[0] - if (elem.nodeType !== NodeType.ELEMENT_NODE) return '' + + if (elem.nodeType !== NodeType.ELEMENT_NODE) { return '' } return elem.tagName.toLowerCase() }