Contribute to this guide

guide实现自定义编辑器创建器

CKEditor 5 的灵活架构允许创建自定义编辑器。不仅可以更改 主题样式重新设计 UI,还可以修改整个编辑器初始化过程,从而创建新的编辑器类型。因此,除了标准编辑器(如 经典内联气球文档)之外,还可以创建 多根编辑器 等自定义类型。

本指南介绍了实现自定义多根编辑器的过程。您也可以查看 多根编辑器示例页面 的演示。

# Editor 类

*Editor 类是每种编辑器类型的主类。它初始化整个编辑器及其 UI 部分。自定义创建器类应扩展 基础 Editor。对于多根编辑器,它可能如下所示

import { Editor, getDataFromElement, setDataInElement } from 'ckeditor5';

/**
 * The multi-root editor implementation. It provides inline editables and a single toolbar.
 *
 * Unlike other editors, the toolbar is not rendered automatically and needs to be attached to the DOM manually.
 *
 * This type of an editor is dedicated to integrations which require a customized UI with an open
 * structure, allowing developers to specify the exact location of the interface.
 *
 * @implements module:core/editor/editorwithui~EditorWithUI
 * @extends module:core/editor/editor~Editor
 */
class MultirootEditor extends Editor {
    /**
    * Creates an instance of the multi-root editor.
    *
    * **Note:** Do not use the constructor to create editor instances. Use the static `MultirootEditor.create()` method instead.
    *
    * @protected
    * @param {Object.<String,HTMLElement>} sourceElements The list of DOM elements that will be the source
    * for the created editor (on which the editor will be initialized).
    * @param {module:core/editor/editorconfig~EditorConfig} config The editor configuration.
    */
    constructor( sourceElements, config ) {
        super( config );

        if ( this.config.get( 'initialData' ) === undefined ) {
            // Create initial data object containing data from all roots.
            const initialData = {};

            for ( const rootName of Object.keys( sourceElements ) ) {
                initialData[ rootName ] = getDataFromElement( sourceElements[ rootName ] );
            }

            this.config.set( 'initialData', initialData );
        }

        // Create root and UIView element for each editable container.
        for ( const rootName of Object.keys( sourceElements ) ) {
            this.model.document.createRoot( '$root', rootName );
        }

        this.ui = new MultirootEditorUI( this, new MultirootEditorUIView( this.locale, this.editing.view, sourceElements ) );
    }

    /**
    * @inheritDoc
    */
    destroy() {
        // Cache the data and editable DOM elements, then destroy.
        // It's safe to assume that the model->view conversion will not work after super.destroy(),
        // same as `ui.getEditableElement()` method will not return editables.
        const data = {};
        const editables = {};
        const editablesNames = Array.from( this.ui.getEditableElementsNames() );

        for ( const rootName of editablesNames ) {
            data[ rootName ] = this.getData( { rootName } );
            editables[ rootName ] = this.ui.getEditableElement( rootName );
        }

        this.ui.destroy();

        return super.destroy()
            .then( () => {
                for ( const rootName of editablesNames ) {
                    setDataInElement( editables[ rootName ], data[ rootName ] );
                }
            } );
    }

    /**
    * Creates a multi-root editor instance.
    *
    * @param {Object.<String,HTMLElement>} sourceElements The list of DOM elements that will be the source
    * for the created editor (on which the editor will be initialized).
    * @param {module:core/editor/editorconfig~EditorConfig} config The editor configuration.
    * @returns {Promise} A promise resolved once the editor is ready. The promise returns the created multi-root editor instance.
    */
    static create( sourceElements, config ) {
        return new Promise( resolve => {
            const editor = new this( sourceElements, config );

            resolve(
                editor.initPlugins()
                    .then( () => editor.ui.init() )
                    .then( () => editor.data.init( editor.config.get( 'initialData' ) ) )
                    .then( () => editor.fire( 'ready' ) )
                    .then( () => editor )
            );
        } );
    }
}

# EditorUI 类

*EditorUI 类是主 UI 类,它初始化 UI 组件(主视图和工具栏)并设置 焦点跟踪器 或占位符管理等机制。自定义 *EditorUI 类应扩展 基础 EditorUI,如下所示

import { EditorUI, enablePlaceholder } from 'ckeditor5';

/**
 * The multi-root editor UI class.
 *
 * @extends module:ui/editorui/editorui~EditorUI
 */
class MultirootEditorUI extends EditorUI {
    /**
    * Creates an instance of the multi-root editor UI class.
    *
    * @param {module:core/editor/editor~Editor} editor The editor instance.
    * @param {module:ui/editorui/editoruiview~EditorUIView} view The view of the UI.
    */
    constructor( editor, view ) {
        super( editor );

        /**
        * The main (top–most) view of the editor UI.
        *
        * @readonly
        * @member {module:ui/editorui/editoruiview~EditorUIView} #view
        */
        this.view = view;
    }

    /**
    * Initializes the UI.
    */
    init() {
        const view = this.view;
        const editor = this.editor;
        const editingView = editor.editing.view;

        let lastFocusedEditableElement;

        view.render();

        // Keep track of the last focused editable element. Knowing which one was focused
        // is useful when the focus moves from editable to other UI components like balloons
        // (especially inputs) but the editable remains the "focus context" (e.g. link balloon
        // attached to a link in an editable). In this case, the editable should preserve visual
        // focus styles.
        this.focusTracker.on( 'change:focusedElement', ( evt, name, focusedElement ) => {
            for ( const editable of this.view.editables ) {
                if ( editable.element === focusedElement ) {
                    lastFocusedEditableElement = editable.element;
                }
            }
        } );

        // If the focus tracker loses focus, stop tracking the last focused editable element.
        // Wherever the focus is restored, it will no longer be in the context of that editable
        // because the focus "came from the outside", as opposed to the focus moving from one element
        // to another within the editor UI.
        this.focusTracker.on( 'change:isFocused', ( evt, name, isFocused ) => {
            if ( !isFocused ) {
                lastFocusedEditableElement = null;
            }
        } );

        for ( const editable of this.view.editables ) {
            // The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
            // But it can be available earlier if a DOM element has been passed to MultirootEditor.create().
            const editableElement = editable.element;

            // Register each editable UI view in the editor.
            this.setEditableElement( editable.name, editableElement );

            // Let the editable UI element respond to the changes in the global editor focus
            // tracker. It has been added to the same tracker a few lines above but, in reality, there are
            // many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
            // as they have focus, the editable should act like it is focused too (although technically
            // it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
            // Doing otherwise will result in editable focus styles disappearing, once e.g. the
            // toolbar gets focused.
            editable.bind( 'isFocused' ).to( this.focusTracker, 'isFocused', this.focusTracker, 'focusedElement',
                ( isFocused, focusedElement ) => {
                    // When the focus tracker is blurred, it means the focus moved out of the editor UI.
                    // No editable will maintain focus then.
                    if ( !isFocused ) {
                        return false;
                    }

                    // If the focus tracker says the editor UI is focused and currently focused element
                    // is the editable, then the editable should be visually marked as focused too.
                    if ( focusedElement === editableElement ) {
                        return true;
                    }
                    // If the focus tracker says the editor UI is focused but the focused element is
                    // not an editable, it is possible that the editable is still (context–)focused.
                    // For instance, the focused element could be an input inside of a balloon attached
                    // to the content in the editable. In such case, the editable should remain _visually_
                    // focused even though technically the focus is somewhere else. The focus moved from
                    // the editable to the input but the focus context remained the same.
                    else {
                        return lastFocusedEditableElement === editableElement;
                    }
                } );

            // Bind the editable UI element to the editing view, making it an end– and entry–point
            // of the editor's engine. This is where the engine meets the UI.
            editingView.attachDomRoot( editableElement, editable.name );
        }

        this._initPlaceholder();
        this._initToolbar();
        this.fire( 'ready' );
    }

    /**
    * @inheritDoc
    */
    destroy() {
        super.destroy();

        const view = this.view;
        const editingView = this.editor.editing.view;

        for ( const editable of this.view.editables ) {
            editingView.detachDomRoot( editable.name );
        }

        view.destroy();
    }

    /**
    * Initializes the editor main toolbar and its panel.
    *
    * @private
    */
    _initToolbar() {
        const editor = this.editor;
        const view = this.view;
        const toolbar = view.toolbar;

        toolbar.fillFromConfig( editor.config.get( 'toolbar' ), this.componentFactory );

        // Register the toolbar so it becomes available for Alt+F10 and Esc navigation.
        this.addToolbar( view.toolbar );
    }

    /**
    * Enable the placeholder text on the editing root, if any was configured.
    *
    * @private
    */
    _initPlaceholder() {
        const editor = this.editor;
        const editingView = editor.editing.view;

        for ( const editable of this.view.editables ) {
            const editingRoot = editingView.document.getRoot( editable.name );
            const sourceElement = this.getEditableElement( editable.name );

            const placeholderText = editor.config.get( 'placeholder' )[ editable.name ] ||
                sourceElement && sourceElement.tagName.toLowerCase() === 'textarea' && sourceElement.getAttribute( 'placeholder' );

            if ( placeholderText ) {
                editingRoot.placeholder = placeholderText;

                enablePlaceholder( {
                    view: editingView,
                    element: editingRoot,
                    isDirectHost: false,
                    keepOnFocus: true
                } );
            }
        }
    }
}

# EditorUIView 类

最后,*EditorUIView 类负责注册和处理所有可编辑元素,并创建编辑器工具栏。自定义 *EditorUIView 类应扩展 基础 EditorUIView

import { EditorUIView, InlineEditableUIView, Template, ToolbarView } from 'ckeditor5';

/**
 * The multi-root editor UI view. It is a virtual view providing an inline editable, but without
 * any specific arrangement of the components in the DOM.
 *
 * @extends module:ui/editorui/editoruiview~EditorUIView
 */
class MultirootEditorUIView extends EditorUIView {
    /**
    * Creates an instance of the multi-root editor UI view.
    *
    * @param {module:utils/locale~Locale} locale The locale instance.
    * @param {module:engine/view/view~View} editingView The editing view instance this view is related to.
    * @param {Object.<String,HTMLElement>} editableElements The list of editable elements, containing name and html element
    * for each editable.
    */
    constructor( locale, editingView, editableElements ) {
        super( locale );

        /**
        * The main toolbar of the multi-root editor UI.
        *
        * @readonly
        * @member {module:ui/toolbar/toolbarview~ToolbarView}
        */
        this.toolbar = new ToolbarView( locale );

        /**
        * The editables of the multi-root editor UI.
        *
        * @readonly
        * @member {Array.<module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView>}
        */
        this.editables = [];

        // Create InlineEditableUIView instance for each editable.
        for ( const editableName of Object.keys( editableElements ) ) {
            const editable = new InlineEditableUIView( locale, editingView, editableElements[ editableName ] );

            editable.name = editableName;
            this.editables.push( editable );
        }

        // This toolbar may be placed anywhere in the page so things like font size need to be reset in it.
        // Because of the above, make sure the toolbar supports rounded corners.
        // Also, make sure the toolbar has the proper dir attribute because its ancestor may not have one
        // and some toolbar item styles depend on this attribute.
        Template.extend( this.toolbar.template, {
            attributes: {
                class: [
                    'ck-reset_all',
                    'ck-rounded-corners'
                ],
                dir: locale.uiLanguageDirection
            }
        } );
    }

    /**
    * @inheritDoc
    */
    render() {
        super.render();

        this.registerChild( this.editables );
        this.registerChild( [ this.toolbar ] );
    }
}

# 初始化自定义编辑器实例

现在多根编辑器创建器已经准备就绪,您可以创建完全可用的编辑器实例。

使用以下 HTML:

<div id="toolbar"></div>

<header id="header">
    <h2>Gone traveling</h2>
    <h3>Monthly travel news and inspiration</h3>
</header>

<div id="content">
    <h3>Destination of the Month</h3>

    <h4>Valletta</h4>

    <figure class="image image-style-align-right">
        <img alt="Picture of a sunlit facade of a Maltan building." src="../../../assets/img/malta.jpg">
        <figcaption>It's siesta time in Valletta.</figcaption>
    </figure>

    <p>The capital city of <a href="https://en.wikipedia.org/wiki/Malta" target="_blank" rel="external">Malta</a> is the top destination this summer. It’s home to a cutting-edge contemporary architecture, baroque masterpieces, delicious local cuisine, and at least 8 months of sun. It’s also a top destination for filmmakers, so you can take a tour through locations familiar to you from Game of Thrones, Gladiator, Troy and many more.</p>
</div>

<div class="demo-row">
    <div class="demo-row__half">
        <div id="footer-left">
            <h3>The three greatest things you learn from traveling</h3>
            <p><a href="#">Find out more</a></p>
        </div>
    </div>

    <div class="demo-row__half">
        <div id="footer-right">
            <h3>Walking the capitals of Europe: Warsaw</h3>
            <p><a href="#">Find out more</a></p>
        </div>
    </div>
</div>

您可以使用以下代码初始化编辑器

MultirootEditor
    .create( {
        header: document.querySelector( '#header' ),
        content: document.querySelector( '#content' ),
        footerleft: document.querySelector( '#footer-left' ),
        footerright: document.querySelector( '#footer-right' )
    }, {
        plugins: [ Essentials, Paragraph, Heading, Bold, Italic, List, Link, BlockQuote, Image, ImageCaption,
            ImageStyle, ImageToolbar, ImageUpload, Table, TableToolbar, MediaEmbed, EasyImage ],
        toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'uploadImage', 'blockQuote',
            'insertTable', 'mediaEmbed', 'undo', 'redo' ],
        image: {
            toolbar: [ 'toggleImageCaption', 'imageTextAlternative', '|', 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText' ],
        },
        table: {
            contentToolbar: [
                'tableColumn',
                'tableRow',
                'mergeTableCells'
            ]
        },
        placeholder: {
            header: 'Header text goes here',
            content: 'Type content here',
            footerleft: 'Left footer content',
            footerright: 'Right footer content'
        },
    } )
    .then( newEditor => {
        document.querySelector( '#toolbar' ).appendChild( newEditor.ui.view.toolbar.element );

        window.editor = newEditor;
    } )
    .catch( err => {
        console.error( err.stack );
    } );

这将创建与 多根编辑器示例页面 上使用的编辑器完全相同的编辑器。