/** * @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, schema: model.schema } ); 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 );