ckeditor / ckeditor5

Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.
https://ckeditor.com/ckeditor-5
Other
9.35k stars 3.68k forks source link

Docs issue report from "framework/guides/deep-dive/ui/custom-editor-creator.html" -- TypeError Cannot read properties of null #12582

Closed hero100era closed 10 months ago

hero100era commented 1 year ago

📝 Provide detailed reproduction steps (if any)

  1. I'm planning to integrated the multiroot editor with flask-admin, here is my ckeditor.js content:
 import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
 import DataApiMixin from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin';
 import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
 import setDataInElement from '@ckeditor/ckeditor5-utils/src/dom/setdatainelement';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
 import { enablePlaceholder } from '@ckeditor/ckeditor5-engine/src/view/placeholder';
 import EditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/editoruiview';
 import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import Template from '@ckeditor/ckeditor5-ui/src/template';

import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment.js';
import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat.js';
import AutoImage from '@ckeditor/ckeditor5-image/src/autoimage.js';
import AutoLink from '@ckeditor/ckeditor5-link/src/autolink.js';
import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote.js';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js';
import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock.js';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials.js';
import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace.js';
import FontBackgroundColor from '@ckeditor/ckeditor5-font/src/fontbackgroundcolor.js';
import FontColor from '@ckeditor/ckeditor5-font/src/fontcolor.js';
import FontFamily from '@ckeditor/ckeditor5-font/src/fontfamily.js';
import FontSize from '@ckeditor/ckeditor5-font/src/fontsize.js';
import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport.js';
import Heading from '@ckeditor/ckeditor5-heading/src/heading.js';
import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight.js';
import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline.js';
import Image from '@ckeditor/ckeditor5-image/src/image.js';
import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption.js';
import ImageInsert from '@ckeditor/ckeditor5-image/src/imageinsert.js';
import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize.js';
import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle.js';
import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar.js';
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload.js';
import Indent from '@ckeditor/ckeditor5-indent/src/indent.js';
import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock.js';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic.js';
import Link from '@ckeditor/ckeditor5-link/src/link.js';
import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage.js';
import List from '@ckeditor/ckeditor5-list/src/list.js';
import ListProperties from '@ckeditor/ckeditor5-list/src/listproperties.js';
import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed.js';
import MediaEmbedToolbar from '@ckeditor/ckeditor5-media-embed/src/mediaembedtoolbar.js';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice.js';
import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js';
import SimpleUploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/simpleuploadadapter.js';
import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js';
import SpecialCharacters from '@ckeditor/ckeditor5-special-characters/src/specialcharacters.js';
import SpecialCharactersArrows from '@ckeditor/ckeditor5-special-characters/src/specialcharactersarrows.js';
import SpecialCharactersCurrency from '@ckeditor/ckeditor5-special-characters/src/specialcharacterscurrency.js';
import SpecialCharactersEssentials from '@ckeditor/ckeditor5-special-characters/src/specialcharactersessentials.js';
import SpecialCharactersLatin from '@ckeditor/ckeditor5-special-characters/src/specialcharacterslatin.js';
import SpecialCharactersMathematical from '@ckeditor/ckeditor5-special-characters/src/specialcharactersmathematical.js';
import SpecialCharactersText from '@ckeditor/ckeditor5-special-characters/src/specialcharacterstext.js';
import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough.js';
import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript.js';
import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript.js';
import Table from '@ckeditor/ckeditor5-table/src/table.js';
import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
import TableColumnResize from '@ckeditor/ckeditor5-table/src/tablecolumnresize.js';
import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar.js';
import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation.js';
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline.js';

class MultirootEditor extends Editor {
    /**
     * Creates an instance of the multiroot 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 multiroot 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 multiroot 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 )
            );
        } );
    }
}

mix( MultirootEditor, DataApiMixin );

/**
 * The multiroot editor UI class.
 *
 * @extends module:core/editor/editorui~EditorUI
 */
class MultirootEditorUI extends EditorUI {
    /**
     * Creates an instance of the multiroot 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 DecoupledEditor.create().
            const editableElement = editable.element;

            // Register the editable UI view in the editor. A single editor instance can aggregate multiple
            // editable areas (roots) but the decoupled editor has only one.
            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() {
        const view = this.view;
        const editingView = this.editor.editing.view;

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

        view.destroy();

        super.destroy();
    }

    /**
     * Initializes the inline editor 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 ) {
                enablePlaceholder( {
                    view: editingView,
                    element: editingRoot,
                    text: placeholderText,
                    isDirectHost: false,
                    keepOnFocus: true
                } );
            }
        }
    }
}

/**
 * The multiroot 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 multiroot 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 );

        const t = locale.t;

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

        /**
         * The editables of the multiroot 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 ], {
                label: editableView => {
                    return t( 'Rich Text Editor. Editing area: %0', editableView.name );
                }
            } );

            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 ] );
    }
}

// Plugins to include in the build.
MultirootEditor.builtinPlugins = [
    Alignment,
    Autoformat,
    AutoImage,
    AutoLink,
    BlockQuote,
    Bold,
    CodeBlock,
    Essentials,
    FindAndReplace,
    FontBackgroundColor,
    FontColor,
    FontFamily,
    FontSize,
    GeneralHtmlSupport,
    Heading,
    Highlight,
    HorizontalLine,
    Image,
    ImageCaption,
    ImageInsert,
    ImageResize,
    ImageStyle,
    ImageToolbar,
    ImageUpload,
    Indent,
    IndentBlock,
    Italic,
    Link,
    LinkImage,
    List,
    ListProperties,
    MediaEmbed,
    MediaEmbedToolbar,
    Paragraph,
    PasteFromOffice,
    RemoveFormat,
    SimpleUploadAdapter,
    SourceEditing,
    SpecialCharacters,
    SpecialCharactersArrows,
    SpecialCharactersCurrency,
    SpecialCharactersEssentials,
    SpecialCharactersLatin,
    SpecialCharactersMathematical,
    SpecialCharactersText,
    Strikethrough,
    Subscript,
    Superscript,
    Table,
    TableCellProperties,
    TableColumnResize,
    TableProperties,
    TableToolbar,
    TextTransformation,
    Underline
];

// Editor configuration.
MultirootEditor.defaultConfig = {
    toolbar: {
        items: [
            'heading',
            '|',
            'bold',
            'italic',
            'link',
            'bulletedList',
            'numberedList',
            '|',
            'outdent',
            'indent',
            '|',
            'imageUpload',
            'blockQuote',
            'insertTable',
            'mediaEmbed',
            'undo',
            'redo',
            'alignment',
            'findAndReplace',
            'codeBlock',
            'fontBackgroundColor',
            'fontColor',
            'fontFamily',
            'fontSize',
            'highlight',
            'horizontalLine',
            'imageInsert',
            'removeFormat',
            'sourceEditing',
            'specialCharacters',
            'strikethrough',
            'subscript',
            'superscript',
            'underline'
        ]
    },
    language: 'en',
    image: {
        toolbar: [
            'imageTextAlternative',
            'imageStyle:inline',
            'imageStyle:block',
            'imageStyle:side',
            'linkImage'
        ]
    },
    table: {
        contentToolbar: [
            'tableColumn',
            'tableRow',
            'mergeTableCells',
            'tableCellProperties',
            'tableProperties'
        ]
    }
};

export default MultirootEditor;
  1. the packages.json:

    {
    "name": "ckeditor5-custom-build",
    "author": "CKSource",
    "description": "A custom CKEditor 5 build made by the CKEditor 5 online builder.",
    "version": "0.0.1",
    "license": "SEE LICENSE IN LICENSE.md",
    "private": true,
    "main": "./build/ckeditor.js",
    "devDependencies": {
    "@ckeditor/ckeditor5-alignment": "^35.2.0",
    "@ckeditor/ckeditor5-autoformat": "^35.2.0",
    "@ckeditor/ckeditor5-basic-styles": "^35.2.0",
    "@ckeditor/ckeditor5-block-quote": "^35.2.0",
    "@ckeditor/ckeditor5-code-block": "^35.2.0",
    "@ckeditor/ckeditor5-dev-utils": "^30.5.0",
    "@ckeditor/ckeditor5-dev-webpack-plugin": "^30.5.0",
    "@ckeditor/ckeditor5-editor-classic": "^35.2.0",
    "@ckeditor/ckeditor5-essentials": "^35.2.0",
    "@ckeditor/ckeditor5-find-and-replace": "^35.2.0",
    "@ckeditor/ckeditor5-font": "^35.2.0",
    "@ckeditor/ckeditor5-heading": "^35.2.0",
    "@ckeditor/ckeditor5-highlight": "^35.2.0",
    "@ckeditor/ckeditor5-horizontal-line": "^35.2.0",
    "@ckeditor/ckeditor5-html-support": "^35.2.0",
    "@ckeditor/ckeditor5-image": "^35.2.0",
    "@ckeditor/ckeditor5-indent": "^35.2.0",
    "@ckeditor/ckeditor5-link": "^35.2.0",
    "@ckeditor/ckeditor5-list": "^35.2.0",
    "@ckeditor/ckeditor5-media-embed": "^35.2.0",
    "@ckeditor/ckeditor5-paragraph": "^35.2.0",
    "@ckeditor/ckeditor5-paste-from-office": "^35.2.0",
    "@ckeditor/ckeditor5-remove-format": "^35.2.0",
    "@ckeditor/ckeditor5-source-editing": "^35.2.0",
    "@ckeditor/ckeditor5-special-characters": "^35.2.0",
    "@ckeditor/ckeditor5-table": "^35.2.0",
    "@ckeditor/ckeditor5-theme-lark": "^35.2.0",
    "@ckeditor/ckeditor5-typing": "^35.2.0",
    "@ckeditor/ckeditor5-upload": "^35.2.0",
    "css-loader": "^5.2.7",
    "postcss": "^8.4.17",
    "postcss-loader": "^4.3.0",
    "raw-loader": "^4.0.2",
    "style-loader": "^2.0.0",
    "terser-webpack-plugin": "^4.2.3",
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0"
    },
    "scripts": {
    "build": "webpack --mode production"
    }
    }
  2. the flask-admin template I'm using:

{% extends 'admin/model/create.html' %}

{% block body %}
{{ super() }}
<div id="toolbar"></div>
{% endblock body %}

{% block tail %}
  {{ super() }}
  <script src="{{ url_for('static', filename='js/ckeditor.js') }}"></script>
  <script>
    const upload_url = "{{ url_for('web.upload') }}";
    const csrf_token = "{{ csrf_token() }}";
    MultirootEditor
    .create( {
        header: document.querySelector( '#body' ),
        content: document.querySelector( '#body_cn' ),
    }, {
        placeholder: {
            header: 'Header text goes here',
            content: 'Type content here',
        },
        simpleUpload: {
            uploadUrl: upload_url,
            // withCredentials: true,
            headers: {
                'X-CSRF-TOKEN': csrf_token,
                Authorization: 'Bearer ' + csrf_token,
            },
        },
    } )
    .then( newEditor => {
        document.querySelector( '#toolbar' ).appendChild( newEditor.ui.view.toolbar.element );

        window.editor = newEditor;
    } )
    .catch( err => {
        console.error( err.stack );
    } );
  </script>  
{% endblock %}

✔️ Expected result

the editor shall run as expected.

❌ Actual result

I got error at "console.error( err.stack ); " and the stack as below:

?url=%2Fadmin%2Fcontent%2F:343 TypeError: Cannot read properties of null (reading '_addClass')
    at kr.addClass (downcastwriter.js:359:17)
    at tablecolumnresizeediting.js:116:37
    at La.change (view.js:427:36)
    at WD.<anonymous> (tablecolumnresizeediting.js:115:24)
    at WD.fire (emittermixin.js:154:47)
    at WD.set [as _isResizingAllowed] (observablemixin.js:79:30)
    at Io (observablemixin.js:572:20)
    at observablemixin.js:389:9
    at Array.forEach (<anonymous>)
    at Object.Do [as to] (observablemixin.js:388:26)

📃 Other details


If you'd like to see this fixed sooner, add a 👍 reaction to this post.

CKEditorBot commented 11 months ago

There has been no activity on this issue for the past year. We've marked it as stale and will close it in 30 days. We understand it may still be relevant, so if you're interested in the solution, leave a comment or reaction under this issue.

CKEditorBot commented 10 months ago

We've closed your issue due to inactivity over the last year. We understand that the issue may still be relevant. If so, feel free to open a new one (and link this issue to it).