ckeditor/ckeditor5-editor-inline

View on GitHub
src/inlineeditorui.js

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module editor-inline/inlineeditorui
 */

import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
import enableToolbarKeyboardFocus from '@ckeditor/ckeditor5-ui/src/toolbar/enabletoolbarkeyboardfocus';
import normalizeToolbarConfig from '@ckeditor/ckeditor5-ui/src/toolbar/normalizetoolbarconfig';
import { enablePlaceholder } from '@ckeditor/ckeditor5-engine/src/view/placeholder';

/**
 * The inline editor UI class.
 *
 * @extends module:core/editor/editorui~EditorUI
 */
export default class InlineEditorUI extends EditorUI {
    /**
     * Creates an instance of the inline 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;

        /**
         * A normalized `config.toolbar` object.
         *
         * @type {Object}
         * @private
         */
        this._toolbarConfig = normalizeToolbarConfig( editor.config.get( 'toolbar' ) );
    }

    /**
     * @inheritDoc
     */
    get element() {
        return this.view.editable.element;
    }

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

        // The editable UI and editing root should share the same name. Then name is used
        // to recognize the particular editable, for instance in ARIA attributes.
        editable.name = editingRoot.rootName;

        view.render();

        // 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 InlineEditor.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 inline editor has only one.
        this.setEditableElement( editable.name, editableElement );

        // Let the global focus tracker know that the editable UI element is focusable and
        // belongs to the editor. From now on, the focus tracker will sustain the editor focus
        // as long as the editable is focused (e.g. the user is typing).
        this.focusTracker.add( 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 );

        // 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 );

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

    /**
     * @inheritDoc
     */
    destroy() {
        const view = this.view;
        const editingView = this.editor.editing.view;

        editingView.detachDomRoot( view.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 editableElement = view.editable.element;
        const editingView = editor.editing.view;
        const toolbar = view.toolbar;

        // Set–up the view#panel.
        view.panel.bind( 'isVisible' ).to( this.focusTracker, 'isFocused' );

        if ( this._toolbarConfig.viewportTopOffset ) {
            view.viewportTopOffset = this._toolbarConfig.viewportTopOffset;
        }

        // https://github.com/ckeditor/ckeditor5-editor-inline/issues/4
        view.listenTo( editor.ui, 'update', () => {
            // Don't pin if the panel is not already visible. It prevents the panel
            // showing up when there's no focus in the UI.
            if ( view.panel.isVisible ) {
                view.panel.pin( {
                    target: editableElement,
                    positions: view.panelPositions
                } );
            }
        } );

        toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory );

        enableToolbarKeyboardFocus( {
            origin: editingView,
            originFocusTracker: this.focusTracker,
            originKeystrokeHandler: editor.keystrokes,
            toolbar
        } );
    }

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

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

        if ( placeholderText ) {
            enablePlaceholder( {
                view: editingView,
                element: editingRoot,
                text: placeholderText,
                isDirectHost: false
            } );
        }
    }
}