CKEditor5+React:如何更新内联小部件表示的节点?

问题描述 投票:0回答:1

我们正在尝试在 CKEditor5 中构建内联 React 组件小部件。我们已经让它渲染,但现在不确定如何更新模型节点。我们遵循了 React 组件教程,但将其修改为内联小部件。

我们有一个名为

BracketOption
的组件,它本质上是一个带有状态的按钮;当用户单击按钮时,我们要更新
optedState
模型元素的
bracketOption
属性。为了实现这一点,我们将回调传递到我们的组件中。 在回调中,我们如何更新模型中的节点?我们尝试在
document
中搜索节点(如下)。我们尝试使用
model.change()
修改它,我们找到的节点会更新,但更改不会反映在模型检查器中。

bracketOption
以“UNDECIDED”状态开始。单击将其设置为 OPTED_IN(绿色)后,我们要相应地更新模型属性(见下文)。

// 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', {
            // Behaves like a self-contained inline object (e.g. an inline image)
            // allowed in places where $text is allowed (e.g. in paragraphs).
            inheritAllFrom: '$inlineObject',

            allowAttributes: [
                'id',
                'value', // content of bracket option (text for now; TODO allow composite content including units-of-measure)
                'optedState' // 'undecided', 'optedIn', or 'optedOut'
            ]
        })
    }

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

        // **NOTE: Other converters omitted for brevity**

        // <bracketOption> convert model to editing view
        conversion.for('editingDowncast').elementToElement({
            model: 'bracketOption',
            view: (modelElement, { writer: viewWriter }) => {
                // In the editing view, the model <bracketOption> corresponds to:
                //
                // <span class="bracket-option" data-id="...">
                //     <span class="bracket-option__react-wrapper">
                //         <BracketOption /> (React component)
                //     </span>
                // </span>
                const id = modelElement.getAttribute('id')
                const value = modelElement.getAttribute('value')
                const optedState = modelElement.getAttribute('optedState')

                // The outermost <span class="bracket-option" data-id="..."></span> element.
                const span = viewWriter.createContainerElement('span', {
                    class: 'bracket-option',
                    'data-id': id
                })

                // The inner <span class="bracket-option__react-wrapper"></span> element.
                // This element will host a React <BracketOption /> component.
                const reactWrapper = viewWriter.createRawElement('span', {
                    class: 'bracket-option__react-wrapper'
                }, function (domElement) {
                    // This is the place where React renders the actual bracket-option preview hosted
                    // by a UIElement in the view. You are using a function (renderer) passed as
                    // editor.config.bracket-options#bracketOptionRenderer.
                    renderBracketOption(id, value, optedState, (newState) => {
                        var root = model.document.getRoot()
                        var node = getChildNodeByAttribute(root, 'id', id)
                        if (node) {
                            // NOTE: This finds a match and updates its attributes, but the inspector's Model state does not reflect the change.
                            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: 'bracket option widget' })
            }
        });
    }
}
// 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';

// NOTE: Use the editor from source (not a build)!
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>Using CKEditor&nbsp;5 from source in React</h2>
                <CKEditor
                    editor={ClassicEditor}
                    config={editorConfiguration}
                    data={intialData}
                    onReady={editor => {
                        // You can store the "editor" and use when it is needed.
                        console.log('Editor is ready to use!', editor);
                        CKEditorInspector.attach(editor);
                    }}
                    onChange={(event) => {
                        console.log(event);
                    }}
                    onBlur={(event, editor) => {
                        console.log('Blur.', editor);
                    }}
                    onFocus={(event, editor) => {
                        console.log('Focus.', editor);
                    }}
                />
            </div>
        );
    }
}

export default App;
reactjs ckeditor5
1个回答
0
投票

原来我检查了错误的编辑器实例!由于某种原因,

<App>
调用了编辑器的
onReady()
两次,从CKEditor5检查器的角度来看,导致了“第二个编辑器”。一旦我选择了第二个实例,更改就会反映出来。

© www.soinside.com 2019 - 2024. All rights reserved.