ckeditor/ckeditor5-engine

View on GitHub
src/controller/editingcontroller.js

Summary

Maintainability
A
1 hr
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 engine/controller/editingcontroller
 */

import RootEditableElement from '../view/rooteditableelement';
import View from '../view/view';
import Mapper from '../conversion/mapper';
import DowncastDispatcher from '../conversion/downcastdispatcher';
import { clearAttributes, convertCollapsedSelection, convertRangeSelection, insertText, remove } from '../conversion/downcasthelpers';

import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import { convertSelectionChange } from '../conversion/upcasthelpers';

// @if CK_DEBUG_ENGINE // const { dumpTrees, initDocumentDumping } = require( '../dev-utils/utils' );

/**
 * Controller for the editing pipeline. The editing pipeline controls {@link ~EditingController#model model} rendering,
 * including selection handling. It also creates the {@link ~EditingController#view view} which builds a
 * browser-independent virtualization over the DOM elements. The editing controller also attaches default converters.
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
export default class EditingController {
    /**
     * Creates an editing controller instance.
     *
     * @param {module:engine/model/model~Model} model Editing model.
     * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor The styles processor instance.
     */
    constructor( model, stylesProcessor ) {
        /**
         * Editor model.
         *
         * @readonly
         * @member {module:engine/model/model~Model}
         */
        this.model = model;

        /**
         * Editing view controller.
         *
         * @readonly
         * @member {module:engine/view/view~View}
         */
        this.view = new View( stylesProcessor );

        /**
         * Mapper which describes the model-view binding.
         *
         * @readonly
         * @member {module:engine/conversion/mapper~Mapper}
         */
        this.mapper = new Mapper();

        /**
         * Downcast dispatcher that converts changes from the model to {@link #view the editing view}.
         *
         * @readonly
         * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #downcastDispatcher
         */
        this.downcastDispatcher = new DowncastDispatcher( {
            mapper: this.mapper
        } );

        const doc = this.model.document;
        const selection = doc.selection;
        const markers = this.model.markers;

        // When plugins listen on model changes (on selection change, post fixers, etc) and change the view as a result of
        // model's change, they might trigger view rendering before the conversion is completed (e.g. before the selection
        // is converted). We disable rendering for the length of the outermost model change() block to prevent that.
        //
        // See https://github.com/ckeditor/ckeditor5-engine/issues/1528
        this.listenTo( this.model, '_beforeChanges', () => {
            this.view._disableRendering( true );
        }, { priority: 'highest' } );

        this.listenTo( this.model, '_afterChanges', () => {
            this.view._disableRendering( false );
        }, { priority: 'lowest' } );

        // Whenever model document is changed, convert those changes to the view (using model.Document#differ).
        // Do it on 'low' priority, so changes are converted after other listeners did their job.
        // Also convert model selection.
        this.listenTo( doc, 'change', () => {
            this.view.change( writer => {
                this.downcastDispatcher.convertChanges( doc.differ, markers, writer );
                this.downcastDispatcher.convertSelection( selection, markers, writer );
            } );
        }, { priority: 'low' } );

        // Convert selection from the view to the model when it changes in the view.
        this.listenTo( this.view.document, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );

        // Attach default model converters.
        this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
        this.downcastDispatcher.on( 'remove', remove(), { priority: 'low' } );

        // Attach default model selection converters.
        this.downcastDispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
        this.downcastDispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } );
        this.downcastDispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );

        // Binds {@link module:engine/view/document~Document#roots view roots collection} to
        // {@link module:engine/model/document~Document#roots model roots collection} so creating
        // model root automatically creates corresponding view root.
        this.view.document.roots.bindTo( this.model.document.roots ).using( root => {
            // $graveyard is a special root that has no reflection in the view.
            if ( root.rootName == '$graveyard' ) {
                return null;
            }

            const viewRoot = new RootEditableElement( this.view.document, root.name );

            viewRoot.rootName = root.rootName;
            this.mapper.bindElements( root, viewRoot );

            return viewRoot;
        } );

        // @if CK_DEBUG_ENGINE // initDocumentDumping( this.model.document );
        // @if CK_DEBUG_ENGINE // initDocumentDumping( this.view.document );

        // @if CK_DEBUG_ENGINE // dumpTrees( this.model.document, this.model.document.version );
        // @if CK_DEBUG_ENGINE // dumpTrees( this.view.document, this.model.document.version );

        // @if CK_DEBUG_ENGINE // this.model.document.on( 'change', () => {
        // @if CK_DEBUG_ENGINE //    dumpTrees( this.view.document, this.model.document.version );
        // @if CK_DEBUG_ENGINE // }, { priority: 'lowest' } );
    }

    /**
     * Removes all event listeners attached to the `EditingController`. Destroys all objects created
     * by `EditingController` that need to be destroyed.
     */
    destroy() {
        this.view.destroy();
        this.stopListening();
    }
}

mix( EditingController, ObservableMixin );