You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CKEditor5+React:如何更新行内小部件对应的模型节点?

CKEditor5行内React组件小部件模型更新问题

我们正在CKEditor5中实现一个行内React组件小部件,已完成渲染,但无法正确更新模型节点。参考官方React组件教程并修改为行内小部件后,遇到了模型属性更新不生效的问题。

我们的BracketOption组件是一个带状态的按钮,用户点击时希望更新bracketOption模型元素的optedState属性。为此给组件传入了回调函数,但**回调内如何通过model.change()正确更新模型节点?**目前尝试在文档中查找节点并修改,虽然能找到节点并更新其属性,但模型检查器中看不到任何变化。

bracketOption初始状态为UNDECIDED,点击后切换为OPTED_IN(绿色)或OPTED_OUT,我们需要同步更新对应的模型属性。


相关代码

utils.ts

export function getChildNodeByAttribute(node: Element, attributeName: string, attributeValue: string) : Element | null{
    for (let i = 0; i < node.childCount; i++) {
        const child = node.getChild(i)
        if (child instanceof Element && child.getAttribute(attributeName) === attributeValue) {
            return child
        }

        const result = getChildNodeByAttribute(child as Element, attributeName, attributeValue)
        if (result) {
            return result
        }
    }
    
    return null
}

bracketOptionEditing.js

import { Plugin } from '@ckeditor/ckeditor5-core'

import { getChildNodeByAttribute } from './utils';
import { Widget, toWidget, toWidgetEditable, viewToModelPositionOutsideModelElement } from '@ckeditor/ckeditor5-widget';

export default class BracketOptionEditing extends Plugin {
    static get requires() {
        return [Widget];
    }

    init() {
        console.log('BracketOptionEditing was initialized')

        this._defineSchema()
        this._defineConverters()

        this.editor.editing.mapper.on(
            'viewToModelPosition',
            viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.hasClass('bracket-option'))
        );
    }

    _defineSchema() {
        const schema = this.editor.model.schema

        schema.register('bracketOption', {
            // 表现为独立的行内对象(如行内图片)
            // 允许在$text允许的位置(如段落中)使用
            inheritAllFrom: '$inlineObject',

            allowAttributes: [
                'id',
                'value', // 括号选项内容(暂时为文本;后续支持包含单位的复合内容)
                'optedState' // 'undecided', 'optedIn', 或 'optedOut'
            ]
        })
    }

    _defineConverters() {
        const editor = this.editor
        const model = editor.model
        const conversion = editor.conversion
        const renderBracketOption = editor.config.get('bracketOption').bracketOptionRenderer

        // **注:省略其他转换器**

        // <bracketOption> 模型转编辑视图
        conversion.for('editingDowncast').elementToElement({
            model: 'bracketOption',
            view: (modelElement, { writer: viewWriter }) => {
                // 在编辑视图中,模型 <bracketOption> 对应:
                //
                // <span class="bracket-option" data-id="...">
                //     <span class="bracket-option__react-wrapper">
                //         <BracketOption /> (React组件)
                //     </span>
                // </span>
                const id = modelElement.getAttribute('id')
                const value = modelElement.getAttribute('value')
                const optedState = modelElement.getAttribute('optedState')

                // 外层 <span class="bracket-option" data-id="..."></span> 元素
                const span = viewWriter.createContainerElement('span', {
                    class: 'bracket-option',
                    'data-id': id
                })

                // 内层 <span class="bracket-option__react-wrapper"></span> 元素
                // 该元素将承载React <BracketOption />组件
                const reactWrapper = viewWriter.createRawElement('span', {
                    class: 'bracket-option__react-wrapper'
                }, function (domElement) {
                    // 此处React渲染实际的括号选项预览,由UIElement承载在视图中
                    // 使用编辑器配置中传入的renderer函数
                    renderBracketOption(id, value, optedState, (newState) => {
                        var root = model.document.getRoot()
                        var node = getChildNodeByAttribute(root, 'id', id)
                        if (node) {
                            // 注:此处能找到匹配节点并更新属性,但检查器中的模型状态未反映变化
                            var root = model.document.getRoot()
                            var node = getChildNodeByAttribute(root, 'id', id)
                            if (node) {
                                writer.setAttribute('optedState', newState, node)
                            }
                        }
                        console.log(newState)
                    }, domElement);
                })

                viewWriter.insert(viewWriter.createPositionAt(span, 0), reactWrapper)

                return toWidget(span, viewWriter, { label: '括号选项小部件' })
            }
        });
    }
}

BracketOption.tsx

import React from 'react';
import { OptedState } from '../model/optionItemState';

interface BracketOptionProps {
    id: string;
    value: string;
    initialOptedState: OptedState;
    onOptedStateChanged: (newState: OptedState) => void;
}

const BracketOption: React.FC<BracketOptionProps> = ({ id, value, initialOptedState, onOptedStateChanged }) => {
    const [optedState, setOptedState] = React.useState<OptedState>(initialOptedState);
    const handleClick = React.useCallback(() => {
        const newState = optedState === OptedState.OptedIn ? OptedState.OptedOut : OptedState.OptedIn;
        setOptedState(newState);
        onOptedStateChanged?.(newState);

    }, [onOptedStateChanged, optedState]);

    let buttonStyle = {};
    if (optedState === OptedState.OptedIn) {
        buttonStyle = { backgroundColor: 'green' };
    } else if (optedState === OptedState.OptedOut) {
        buttonStyle = { backgroundColor: 'white' };
    } else {
        buttonStyle = { backgroundColor: 'lightgray' };
    }

    return (
        <span data-id={id} data-opted-state={optedState}>
            <button id={id} onClick={handleClick} style={buttonStyle}>{value}</button>
        </span>
    );
};

export default BracketOption;

App.tsx

import React, { Component } from 'react';

import { CKEditor } from '@ckeditor/ckeditor5-react';
import CKEditorInspector from '@ckeditor/ckeditor5-inspector';

// 注意:使用源码编辑器(而非构建版本)!
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';

import { Essentials } from '@ckeditor/ckeditor5-essentials';
import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import UnitsOfMeasure from './ckeditor/unitsOfMeasure';
import { default as BracketOptionPlugin } from './ckeditor/bracketOption';
import { OptedState } from './model/optionItemState';
import { createRoot } from 'react-dom/client';
import BracketOption from './react/BracketOption';

const editorConfiguration = {
    plugins: [Essentials, Bold, Italic, Paragraph, UnitsOfMeasure, BracketOptionPlugin],
    bracketOption: {
        bracketOptionRenderer: (
            id: string,
            value: string,
            optedState: OptedState,
            onOptedStateChanged: (newState: OptedState) => void,
            domElement: HTMLElement,
        ) => {
            const root = createRoot(domElement);

            root.render(
                <BracketOption id={id} value={value} initialOptedState={optedState} onOptedStateChanged={onOptedStateChanged} />
            );
        }
    }
};

const intialData = "<p>After construction ends, prior to occupancy and with all interior finishes <span class='bracket-option' data-id='123' data-opted-state='UNDECIDED'>installed</span>, perform a building flush-out by supplying a total volume of <span class='units-of-measure'>{14,000 cu. ft. (4 300 000 L)}</span> of outdoor air per <span class='units-of-measure'>{sq. ft. (sq. m)}</span> of floor area while maintaining an internal temperature of at least <span class='units-of-measure'>{60 deg F (16 deg C)}</span> and a relative humidity no higher than 60 percent.</p>";

class App extends Component {
    render() {
        return (
            <div className="App">
                <h2>在React中使用CKEditor 5源码版本</h2>
                <CKEditor
                    editor={ClassicEditor}
                    config={editorConfiguration}
                    data={intialData}
                    onReady={editor => {
                        // 可存储editor实例供后续使用
                        console.log('编辑器已就绪!', editor);
                        CKEditorInspector.attach(editor);
                    }}
                    onChange={(event) => {
                        console.log(event);
                    }}
                    onBlur={(event, editor) => {
                        console.log('失去焦点.', editor);
                    }}
                    onFocus={(event, editor) => {
                        console.log('获得焦点.', editor);
                    }}
                />
            </div>
        );
    }
}

export default App;

内容的提问来源于stack exchange,提问作者System.Cats.Lol

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 22:13:09