Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

253 图片无法回显行内元素包含行内元素无法解析 #283

7 changes: 7 additions & 0 deletions .changeset/dry-zebras-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@wangeditor-next/video-module': patch
'@wangeditor-next/list-module': patch
'@wangeditor-next/core': patch
---

253 图片无法回显行内元素包含行内元素无法解析
36 changes: 35 additions & 1 deletion packages/core/src/config/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

import { ImageElement } from 'packages/basic-modules/src/modules/image/custom-types'
import { VideoElement } from 'packages/video-module/src/module/custom-types'
import { Node, NodeEntry, Range } from 'slate'
import {
Descendant, Node, NodeEntry, Range,
} from 'slate'

import { IDomEditor } from '../editor/interface'
import { IMenuGroup } from '../menus/interface'
import { IUploadConfig } from '../upload'
import { DOMElement } from '../utils/dom'

interface IHoverbarConf {
// key 即 element type
Expand All @@ -21,6 +24,27 @@ interface IHoverbarConf {

export type AlertType = 'success' | 'info' | 'warning' | 'error'

/**
* EditorEvents 包含所有编辑器的生命周期事件。
*
* @property {string} CREATED - 编辑器创建后触发,用于初始化操作。
* @property {string} DESTROYED - 编辑器销毁时触发,用于清理操作。
* @property {string} CHANGE - 编辑器内容发生变化时触发,通常用于监听输入或变动。
* @property {string} SCROLL - 编辑器滚动时触发,用于同步滚动状态或执行相关操作。
* @property {string} FULLSCREEN - 编辑器进入全屏时触发,通常用于调整布局或容器尺寸。
* @property {string} UNFULLSCREEN - 编辑器退出全屏时触发,恢复原始布局状态。
*/
export const EditorEvents = {
CREATED: 'created',
DESTROYED: 'destroyed',
CHANGE: 'change',
SCROLL: 'scroll',
FULLSCREEN: 'fullscreen',
UNFULLSCREEN: 'unFullScreen',
} as const

export type EditorEventType = typeof EditorEvents[keyof typeof EditorEvents]

export interface ISingleMenuConfig {
[key: string]: any;
iconSvg?: string;
Expand Down Expand Up @@ -219,3 +243,13 @@ export interface IToolbarConfig {
excludeKeys: Array<string> // 排除哪些菜单
modalAppendToBody: boolean // modal append 到 body ,而非 $textAreaContainer 内
}

type PluginFnType = <T extends IDomEditor>(editor: T) => T

export interface ICreateOption {
selector: string | DOMElement
config: Partial<IEditorConfig>
content?: Descendant[]
html?: string
plugins: PluginFnType[]
}
113 changes: 63 additions & 50 deletions packages/core/src/create/create-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,69 @@
* @author wangfupeng
*/

import { createEditor, Descendant } from 'slate'
import { createEditor } from 'slate'
import { withHistory } from 'slate-history'
import { withDOM } from '../editor/plugins/with-dom'

import { genEditorConfig } from '../config/index'
import { EditorEvents, ICreateOption } from '../config/interface'
import { IDomEditor } from '../editor/interface'
import { withConfig } from '../editor/plugins/with-config'
import { withContent } from '../editor/plugins/with-content'
import { withEventData } from '../editor/plugins/with-event-data'
import { withDOM } from '../editor/plugins/with-dom'
import { withEmitter } from '../editor/plugins/with-emitter'
import { withSelection } from '../editor/plugins/with-selection'
import { withEventData } from '../editor/plugins/with-event-data'
import { withMaxLength } from '../editor/plugins/with-max-length'
import TextArea from '../text-area/TextArea'
import { withSelection } from '../editor/plugins/with-selection'
import HoverBar from '../menus/bar/HoverBar'
import { genEditorConfig } from '../config/index'
import { IDomEditor } from '../editor/interface'
import { DomEditor } from '../editor/dom-editor'
import { IEditorConfig } from '../config/interface'
import TextArea from '../text-area/TextArea'
import { promiseResolveThen } from '../utils/util'
import { isRepeatedCreateTextarea, genDefaultContent, htmlToContent } from './helper'
import type { DOMElement } from '../utils/dom'
import {
EDITOR_TO_TEXTAREA,
TEXTAREA_TO_EDITOR,
EDITOR_TO_CONFIG,
HOVER_BAR_TO_EDITOR,
EDITOR_TO_HOVER_BAR,
EDITOR_TO_TEXTAREA,
HOVER_BAR_TO_EDITOR,
TEXTAREA_TO_EDITOR,
} from '../utils/weak-maps'
import bindNodeRelation from './bind-node-relation'
import $ from '../utils/dom'

type PluginFnType = <T extends IDomEditor>(editor: T) => T
import {
initializeContent, isRepeatedCreateTextarea,
} from './helper'

interface ICreateOption {
selector: string | DOMElement
config: Partial<IEditorConfig>
content?: Descendant[]
html?: string
plugins: PluginFnType[]
const MIN_TEXTAREA_HEIGHT = 300
const MESSAGES = {
heightWarning: {
en: 'Textarea height < 300px. This may cause modal and hoverbar position error',
zh: '编辑区域高度 < 300px 这可能会导致 modal hoverbar 定位异常',
},
}

/**
* 创建编辑器
*/
export default function (option: Partial<ICreateOption>) {
const { selector = '', config = {}, content, html, plugins = [] } = option
const {
selector = '', config = {}, content, html, plugins = [],
} = option

// 创建实例 - 使用插件
let editor = withHistory(
withMaxLength(
withEmitter(withSelection(withContent(withConfig(withDOM(withEventData(createEditor()))))))
)
)

const createBaseEditor = () => createEditor() as IDomEditor

const applyPlugins = (editor: IDomEditor) => {
return [
withEventData,
withDOM,
withConfig,
withContent,
withSelection,
withEmitter,
withMaxLength,
withHistory,
].reduce((ed, plugin) => plugin(ed), editor)
}

let editor = applyPlugins(createBaseEditor())

if (selector) {
// 检查是否对同一个 DOM 重复创建
if (isRepeatedCreateTextarea(editor, selector)) {
Expand All @@ -62,6 +75,7 @@ export default function (option: Partial<ICreateOption>) {

// 处理配置
const editorConfig = genEditorConfig(config)

EDITOR_TO_CONFIG.set(editor, editorConfig)
const { hoverbarKeys = {} } = editorConfig

Expand All @@ -70,50 +84,48 @@ export default function (option: Partial<ICreateOption>) {
editor = plugin(editor)
})

// 初始化内容(要在 config 和 plugins 后面)
if (html != null) {
// 传入 html ,转换为 JSON content
editor.children = htmlToContent(editor, html)
}
if (content && content.length) {
editor.children = content // 传入 JSON content
}
if (editor.children.length === 0) {
editor.children = genDefaultContent() // 默认内容
}
DomEditor.normalizeContent(editor) // 格式化,用户输入的 content 可能不规范(如两个相连的 text 没有合并)
editor.children = initializeContent(editor, { html, content })
// 兼容了更多格式,normalizeContent 以不在适合于初始化 content
// Content normalization is disabled to support more formats.
// Note: This may result in non-normalized content (e.g., adjacent text nodes won't be merged).
// TODO: Document specific formats that would break with normalization
// DomEditor.normalizeContent(editor)

if (selector) {
// 传入了 selector ,则创建 textarea DOM
const textarea = new TextArea(selector)

cycleccc marked this conversation as resolved.
Show resolved Hide resolved
EDITOR_TO_TEXTAREA.set(editor, textarea)
TEXTAREA_TO_EDITOR.set(textarea, editor)
textarea.changeViewState() // 初始化时触发一次,以便能初始化 textarea DOM 和 selection

// 判断 textarea 最小高度,并给出提示
promiseResolveThen(() => {
const $scroll = textarea.$scroll
if ($scroll == null) return
if ($scroll.height() < 300) {
let info = '编辑区域高度 < 300px 这可能会导致 modal hoverbar 定位异常'
info += '\nTextarea height < 300px . This may be cause modal and hoverbar position error'
console.warn(info, $scroll)

if ($scroll == null) { return }
if ($scroll.height() < MIN_TEXTAREA_HEIGHT) {
console.warn(
`${MESSAGES.heightWarning.zh}\n${MESSAGES.heightWarning.en}`,
{ element: $scroll, height: $scroll.height() },
)
}
})

// 创建 hoverbar DOM
let hoverbar: HoverBar | null

if (Object.keys(hoverbarKeys).length > 0) {
hoverbar = new HoverBar()
HOVER_BAR_TO_EDITOR.set(hoverbar, editor)
EDITOR_TO_HOVER_BAR.set(editor, hoverbar)
}

// 隐藏 panel and modal
editor.on('change', () => {
editor.on(EditorEvents.CHANGE, () => {
editor.hidePanelOrModal()
})
editor.on('scroll', () => {
editor.on(EditorEvents.SCROLL, () => {
editor.hidePanelOrModal()
})
} else {
Expand All @@ -123,11 +135,12 @@ export default function (option: Partial<ICreateOption>) {

// 触发生命周期
const { onCreated, onDestroyed } = editorConfig

cycleccc marked this conversation as resolved.
Show resolved Hide resolved
if (onCreated) {
editor.on('created', () => onCreated(editor))
editor.on(EditorEvents.CREATED, () => onCreated(editor))
}
if (onDestroyed) {
editor.on('destroyed', () => onDestroyed(editor))
editor.on(EditorEvents.DESTROYED, () => onDestroyed(editor))
}

// 创建完毕,异步触发 created
Expand Down
30 changes: 25 additions & 5 deletions packages/core/src/create/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@
*/

import { Descendant } from 'slate'

import { EditorEvents } from '../config/interface'
import { IDomEditor } from '../editor/interface'
import parseElemHtml from '../parse-html/parse-elem-html'
import $, { DOMElement } from '../utils/dom'

function isRepeatedCreate(
editor: IDomEditor,
attrKey: string,
selector: string | DOMElement
selector: string | DOMElement,
): boolean {
// @ts-ignore
const $elem = $(selector)

if ($elem.attr(attrKey)) {
return true // 有属性,说明已经创建过
}
Expand All @@ -23,7 +26,7 @@ function isRepeatedCreate(
$elem.attr(attrKey, 'true')

// 销毁时删除属性
editor.on('destroyed', () => {
editor.on(EditorEvents.DESTROYED, () => {
$elem.removeAttr(attrKey)
})

Expand All @@ -35,7 +38,7 @@ function isRepeatedCreate(
*/
export function isRepeatedCreateTextarea(
editor: IDomEditor,
selector: string | DOMElement
selector: string | DOMElement,
): boolean {
return isRepeatedCreate(editor, 'data-w-e-textarea', selector)
}
Expand All @@ -45,7 +48,7 @@ export function isRepeatedCreateTextarea(
*/
export function isRepeatedCreateToolbar(
editor: IDomEditor,
selector: string | DOMElement
selector: string | DOMElement,
): boolean {
return isRepeatedCreate(editor, 'data-w-e-toolbar', selector)
}
Expand All @@ -71,7 +74,7 @@ export function htmlToContent(editor: IDomEditor, html: string = ''): Descendant
const res: Descendant[] = []

// 空白内容
if (html === '') html = '<p><br></p>'
if (html === '') { html = '<p><br></p>' }

// 非 HTML 格式,文本格式,用 <p> 包裹
if (html.indexOf('<') !== 0) {
Expand All @@ -83,6 +86,7 @@ export function htmlToContent(editor: IDomEditor, html: string = ''): Descendant

const $content = $(`<div>${html}</div>`)
const list = Array.from($content.children())

list.forEach(child => {
const $child = $(child)
const parsedRes = parseElemHtml($child, editor)
Expand All @@ -96,3 +100,19 @@ export function htmlToContent(editor: IDomEditor, html: string = ''): Descendant

return res
}

/**
* 初始化内容(要在 config 和 plugins 后面)
*/
export function initializeContent(editor: IDomEditor, options: { html?: string, content?: Descendant[] }) {
// 传入 html ,转换为 JSON content
if (options.html != null) {
return htmlToContent(editor, options.html)
}
// 传入 JSON content
if (options.content?.length) {
return options.content
}
// 默认内容
return genDefaultContent()
}
9 changes: 5 additions & 4 deletions packages/core/src/menus/bar/HoverBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from 'slate'

import { CustomElement } from '../../../../custom-types'
import { EditorEvents } from '../../config/interface'
import { DomEditor } from '../../editor/dom-editor'
import { IDomEditor } from '../../editor/interface'
import { i18nListenLanguage } from '../../i18n'
Expand Down Expand Up @@ -80,16 +81,16 @@ class HoverBar {
textarea.$textAreaContainer.append($elem)

// 绑定 editor onchange
editor.on('change', this.changeHoverbarState)
editor.on(EditorEvents.CHANGE, this.changeHoverbarState)

// 滚动时隐藏
const hideAndClean = this.hideAndClean.bind(this)

editor.on('scroll', hideAndClean)
editor.on(EditorEvents.SCROLL, hideAndClean)

// fullScreen 时隐藏
editor.on('fullScreen', hideAndClean)
editor.on('unFullScreen', hideAndClean)
editor.on(EditorEvents.FULLSCREEN, hideAndClean)
editor.on(EditorEvents.UNFULLSCREEN, hideAndClean)
})

// 监听语言变更
Expand Down
Loading