Skip to content

Commit

Permalink
feat: elem id add type (#296)
Browse files Browse the repository at this point in the history
* feat: elem id add type

* Create loud-dogs-fly.md

* style: refactoring the empty state checks

* style: enhancing type safety

* style:  simplifying the list element check

* chore: add sh dev build

* perf: consistency with isDOMText

* perf: adding parameter validation

* style: simplify the node type checking

* chore: add build status tracking and summary

* perf: optimizing the selector performance

* chore: enhance build function with better error handling and build options
  • Loading branch information
cycleccc authored Oct 29, 2024
1 parent 576c507 commit f1848d5
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 103 deletions.
6 changes: 6 additions & 0 deletions .changeset/loud-dogs-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@wangeditor-next/editor": patch
"@wangeditor-next/core": patch
---

feat: elem id add type
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"test": "cross-env NODE_OPTIONS=--unhandled-rejections=warn vitest run --passWithNoTests --dangerouslyIgnoreUnhandledErrors",
"test-c": "cross-env NODE_OPTIONS=--unhandled-rejections=warn vitest run --passWithNoTests --dangerouslyIgnoreUnhandledErrors --coverage",
"dev": "turbo run dev",
"dev:sh": "sh scripts/build-base.sh dev",
"build": "turbo run build",
"publish": "yarn changeset publish",
"prerelease": "yarn build",
Expand Down
5 changes: 3 additions & 2 deletions packages/core/__tests__/editor/dom-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { Editor, Range as SlateRange } from 'slate'

import { CustomElement } from '../../../custom-types'
import { DomEditor } from '../../src/editor/dom-editor'
import { IDomEditor } from '../../src/editor/interface'
import { Key } from '../../src/utils/key'
Expand Down Expand Up @@ -148,14 +149,14 @@ describe('Core DomEditor', () => {
})

test('toDOMNode', () => {
const p = editor.children[0]
const p = editor.children[0] as CustomElement

const key = DomEditor.findKey(editor, p)

const domNode = DomEditor.toDOMNode(editor, p)

expect(domNode.tagName).toBe('DIV')
expect(domNode.id).toBe(`w-e-element-${key.id}`)
expect(domNode.id).toBe(`w-e-element-${p.type}-${key.id}`)
})

test('hasDOMNode', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/editor/dom-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import $, {
isDocument,
isDOMElement,
isDOMSelection,
isDOMText,
isShadowRoot,
normalizeDOMPoint,
walkTextNodes,
Expand Down Expand Up @@ -733,7 +734,7 @@ export const DomEditor = {

if (childNodes) {
for (const node of Array.from(childNodes)) {
if (node.nodeType === 3) {
if (isDOMText(node)) {
node.remove()
} else {
break
Expand Down
119 changes: 72 additions & 47 deletions packages/core/src/editor/plugins/with-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,26 @@
* @author wangfupeng
*/

import { Editor, Node, Text, Path, Operation, Range, Transforms, Element, Descendant } from 'slate'
import { DomEditor } from '../dom-editor'
import {
Editor, Element, Node, Operation, Path, Range, Text, Transforms,
} from 'slate'

import { IDomEditor } from '../..'
import { EDITOR_TO_SELECTION, NODE_TO_KEY } from '../../utils/weak-maps'
import node2html from '../../to-html/node2html'
import { IGNORE_TAGS } from '../../constants'
import { htmlToContent } from '../../create/helper'
import { PARSE_ELEM_HTML_CONF, TEXT_TAGS } from '../../parse-html/index'
import parseElemHtml from '../../parse-html/parse-elem-html'
import { genElemId } from '../../render/helper'
import node2html from '../../to-html/node2html'
import $, {
DOMElement, isDOMElement, isDOMText,
isUnprocessedListElement,
} from '../../utils/dom'
import { Key } from '../../utils/key'
import $, { DOMElement, NodeType } from '../../utils/dom'
import { findCurrentLineRange } from '../../utils/line'
import { EDITOR_TO_SELECTION, NODE_TO_KEY } from '../../utils/weak-maps'
import { DomEditor } from '../dom-editor'
import { ElementWithId } from '../interface'
import { PARSE_ELEM_HTML_CONF, TEXT_TAGS } from '../../parse-html/index'
import parseElemHtml from '../../parse-html/parse-elem-html'
import { htmlToContent } from '../../create/helper'
import { IGNORE_TAGS } from '../../constants'

/**
* 把 elem 插入到编辑器
Expand All @@ -29,7 +35,7 @@ function insertElemToEditor(editor: IDomEditor, elem: Element) {
editor.insertNode(elem)

// link 特殊处理,否则后面插入的文字全都在 a 里面 issue#4573
if (elem.type === 'link') editor.insertFragment([{ text: '' }])
if (elem.type === 'link') { editor.insertFragment([{ text: '' }]) }
} else {
// block elem ,另起一行插入 —— 重要
Transforms.insertNodes(editor, elem, { mode: 'highest' })
Expand All @@ -38,11 +44,14 @@ function insertElemToEditor(editor: IDomEditor, elem: Element) {

export const withContent = <T extends Editor>(editor: T) => {
const e = editor as T & IDomEditor
const { onChange, insertText, apply, deleteBackward } = e
const {
onChange, insertText, apply, deleteBackward,
} = e

e.insertText = (text: string) => {
const { readOnly } = e.getConfig()
if (readOnly) return

if (readOnly) { return }

insertText(text)
}
Expand All @@ -60,6 +69,7 @@ export const withContent = <T extends Editor>(editor: T) => {
for (const [node, path] of Editor.levels(e, { at: op.path })) {
// 在当前节点寻找
const key = DomEditor.findKey(e, node)

matches.push([path, key])
}
break
Expand All @@ -72,6 +82,7 @@ export const withContent = <T extends Editor>(editor: T) => {
for (const [node, path] of Editor.levels(e, { at: Path.parent(op.path) })) {
// 在父节点寻找
const key = DomEditor.findKey(e, node)

matches.push([path, key])
}
break
Expand All @@ -82,10 +93,12 @@ export const withContent = <T extends Editor>(editor: T) => {
at: Path.common(Path.parent(op.path), Path.parent(op.newPath)),
})) {
const key = DomEditor.findKey(e, node)

matches.push([path, key])
}
break
}
default:
}

// 执行原本的 apply - 重要!!!
Expand All @@ -94,6 +107,7 @@ export const withContent = <T extends Editor>(editor: T) => {
// 绑定 node 和 key
for (const [path, key] of matches) {
const [node] = Editor.node(e, path)

NODE_TO_KEY.set(node, key)
}
}
Expand Down Expand Up @@ -126,6 +140,7 @@ export const withContent = <T extends Editor>(editor: T) => {
e.onChange = () => {
// 记录当前选区
const { selection } = e

if (selection != null) {
EDITOR_TO_SELECTION.set(e, selection)
}
Expand All @@ -145,19 +160,22 @@ export const withContent = <T extends Editor>(editor: T) => {
e.getHtml = (): string => {
const { children = [] } = e
const html = children.map(child => node2html(child, e)).join('')

return html
}

// 获取 text
e.getText = (): string => {
const { children = [] } = e

return children.map(child => Node.string(child)).join('\n')
}

// 获取选区文字
e.getSelectionText = (): string => {
const { selection } = e
if (selection == null) return ''

if (selection == null) { return '' }
return Editor.string(editor, selection)
}

Expand All @@ -170,14 +188,17 @@ export const withContent = <T extends Editor>(editor: T) => {
at: [],
universal: true,
})
for (let nodeEntry of nodeEntries) {

for (const nodeEntry of nodeEntries) {
const [node] = nodeEntry

if (Element.isElement(node)) {
// 判断 type (前缀 or 全等)
let flag = isPrefix ? node.type.indexOf(type) >= 0 : node.type === type
const flag = isPrefix ? node.type.indexOf(type) >= 0 : node.type === type

if (flag) {
const key = DomEditor.findKey(e, node)
const id = genElemId(key.id)
const id = genElemId(node.type, key.id)

// node + id
elems.push({
Expand All @@ -201,22 +222,20 @@ export const withContent = <T extends Editor>(editor: T) => {
*/
e.isEmpty = () => {
const { children = [] } = e
if (children.length > 1) return false // >1 个顶级节点

if (children.length > 1) { return false } // >1 个顶级节点

const firstNode = children[0]
if (firstNode == null) return true // editor.children 空数组

if (Element.isElement(firstNode) && firstNode.type === 'paragraph') {
const { children: texts = [] } = firstNode
if (texts.length > 1) return false // >1 text node
if (firstNode == null) { return true } // editor.children 空数组

const t = texts[0]
if (t == null) return true // 无 text 节点
if (!Element.isElement(firstNode) || firstNode.type !== 'paragraph') { return false }
const { children: texts = [] } = firstNode

if (Text.isText(t) && t.text === '') return true // 只有一个 text 且是空字符串
}
if (texts.length > 1) { return false } // >1 text node
const t = texts[0]

return false
return t == null || (Text.isText(t) && t.text === '') // 无 text 节点 or 只有一个 text 且是空字符串
}

/**
Expand Down Expand Up @@ -252,55 +271,60 @@ export const withContent = <T extends Editor>(editor: T) => {
* @param isRecursive 是否递归调用(内部使用,使用者不要传参)
*/
e.dangerouslyInsertHtml = (html: string = '', isRecursive = false) => {
if (!html) return
if (!html) { return }

// ------------- 把 html 转换为 DOM nodes -------------
const div = document.createElement('div')

div.innerHTML = html
let domNodes = Array.from(div.childNodes)

// 过滤一下,只保留 elem 和 text ,并却掉一些无用标签(如 style script 等)
domNodes = domNodes.filter(n => {
const { nodeType, nodeName } = n
const { nodeName } = n
// Text Node
if (nodeType === NodeType.TEXT_NODE) return true

if (isDOMText(n)) { return true }

// Element Node
if (nodeType === NodeType.ELEMENT_NODE) {
if (isDOMElement(n)) {
// 过滤掉忽略的 tag
if (IGNORE_TAGS.has(nodeName.toLowerCase())) return false
else return true
if (IGNORE_TAGS.has(nodeName.toLowerCase())) { return false }
return true
}
return false
})
if (domNodes.length === 0) return
if (domNodes.length === 0) { return }

// ------------- 把 DOM nodes 转换为 slate nodes ,并插入到编辑器 -------------

const { selection } = e
if (selection == null) return

if (selection == null) { return }
let curEmptyParagraphPath: Path | null = null

// 是否当前选中了一个空 p (如果是,后面会删掉)
// 递归调用时不判断
if (DomEditor.isSelectedEmptyParagraph(e) && !isRecursive) {
const { focus } = selection

curEmptyParagraphPath = [focus.path[0]] // 只记录顶级 path 即可
}

div.setAttribute('hidden', 'true')
document.body.appendChild(div)

let insertedElemNum = 0 // 记录插入 elem 的数量 ( textNode 不算 )

domNodes.forEach((n, index) => {
const { nodeType, nodeName, textContent = '' } = n
const { nodeName, textContent = '' } = n

// ------ Text node ------
if (nodeType === NodeType.TEXT_NODE) {
if (!textContent || !textContent.trim()) return // 无内容的 Text
if (isDOMText(n)) {
if (!textContent || !textContent.trim()) { return } // 无内容的 Text

// 插入文本
//【注意】insertNode 和 insertText 有区别:后者会继承光标处的文本样式(如加粗);前者会加入纯文本,无样式;
// 【注意】insertNode 和 insertText 有区别:后者会继承光标处的文本样式(如加粗);前者会加入纯文本,无样式;
e.insertNode({ text: textContent })
return
}
Expand All @@ -314,11 +338,12 @@ export const withContent = <T extends Editor>(editor: T) => {
// 判断当前的 el 是否是可识别的 tag
const el = n as DOMElement
let isParseMatch = false

if (TEXT_TAGS.includes(nodeName.toLowerCase())) {
// text elem,如 <span>
isParseMatch = true
} else {
for (let selector in PARSE_ELEM_HTML_CONF) {
for (const selector in PARSE_ELEM_HTML_CONF) {
if (el.matches(selector)) {
// 普通 elem,如 <p> <a> 等(非 text elem)
isParseMatch = true
Expand All @@ -334,31 +359,30 @@ export const withContent = <T extends Editor>(editor: T) => {
const parsedRes = parseElemHtml($el, e) as Element

if (Array.isArray(parsedRes)) {
parsedRes.forEach(el => insertElemToEditor(e, el))
insertedElemNum++ // 记录数量
parsedRes.forEach(parsedEl => insertElemToEditor(e, parsedEl))
insertedElemNum += 1 // 记录数量
} else {
insertElemToEditor(e, parsedRes)
insertedElemNum++ // 记录数量
insertedElemNum += 1 // 记录数量
}

// 如果当前选中 void node ,则选区移动一下
if (DomEditor.isSelectedVoidNode(e)) e.move(1)
if (DomEditor.isSelectedVoidNode(e)) { e.move(1) }

return
}

// 没有匹配上(如 div )
const display = window.getComputedStyle(el).display

if (!DomEditor.isSelectedEmptyParagraph(e)) {
// 当前不是空行,且 非 inline - 则换行
if (display.indexOf('inline') < 0) {
if (index >= 1) {
const prevEl = domNodes[index - 1] as DOMElement
// 如果是 list 列表需要多插入一个回车,模拟双回车删除空 list
if (
'matches' in prevEl &&
prevEl.matches('ul:not([data-w-e-type]),ol:not([data-w-e-type])')
) {

if (isUnprocessedListElement(prevEl)) {
e.insertBreak()
}
}
Expand Down Expand Up @@ -397,6 +421,7 @@ export const withContent = <T extends Editor>(editor: T) => {
e.clear()
// 设置新内容
const newContent = htmlToContent(e, html == null ? '' : html)

Transforms.insertFragment(e, newContent)

// 恢复编辑器状态和选区
Expand Down
Loading

0 comments on commit f1848d5

Please sign in to comment.