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
相关产品推荐
相关产品推荐

