editingcontroller.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module engine/controller/editingcontroller
  7. */
  8. import RootEditableElement from '../view/rooteditableelement';
  9. import View from '../view/view';
  10. import Mapper from '../conversion/mapper';
  11. import DowncastDispatcher from '../conversion/downcastdispatcher';
  12. import { clearAttributes, convertCollapsedSelection, convertRangeSelection, insertText, remove } from '../conversion/downcasthelpers';
  13. import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
  14. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  15. import { convertSelectionChange } from '../conversion/upcasthelpers';
  16. // @if CK_DEBUG_ENGINE // const { dumpTrees, initDocumentDumping } = require( '../dev-utils/utils' );
  17. /**
  18. * Controller for the editing pipeline. The editing pipeline controls {@link ~EditingController#model model} rendering,
  19. * including selection handling. It also creates the {@link ~EditingController#view view} which builds a
  20. * browser-independent virtualization over the DOM elements. The editing controller also attaches default converters.
  21. *
  22. * @mixes module:utils/observablemixin~ObservableMixin
  23. */
  24. export default class EditingController {
  25. /**
  26. * Creates an editing controller instance.
  27. *
  28. * @param {module:engine/model/model~Model} model Editing model.
  29. * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor The styles processor instance.
  30. */
  31. constructor( model, stylesProcessor ) {
  32. /**
  33. * Editor model.
  34. *
  35. * @readonly
  36. * @member {module:engine/model/model~Model}
  37. */
  38. this.model = model;
  39. /**
  40. * Editing view controller.
  41. *
  42. * @readonly
  43. * @member {module:engine/view/view~View}
  44. */
  45. this.view = new View( stylesProcessor );
  46. /**
  47. * Mapper which describes the model-view binding.
  48. *
  49. * @readonly
  50. * @member {module:engine/conversion/mapper~Mapper}
  51. */
  52. this.mapper = new Mapper();
  53. /**
  54. * Downcast dispatcher that converts changes from the model to {@link #view the editing view}.
  55. *
  56. * @readonly
  57. * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #downcastDispatcher
  58. */
  59. this.downcastDispatcher = new DowncastDispatcher( {
  60. mapper: this.mapper,
  61. schema: model.schema
  62. } );
  63. const doc = this.model.document;
  64. const selection = doc.selection;
  65. const markers = this.model.markers;
  66. // When plugins listen on model changes (on selection change, post fixers, etc.) and change the view as a result of
  67. // model's change, they might trigger view rendering before the conversion is completed (e.g. before the selection
  68. // is converted). We disable rendering for the length of the outermost model change() block to prevent that.
  69. //
  70. // See https://github.com/ckeditor/ckeditor5-engine/issues/1528
  71. this.listenTo( this.model, '_beforeChanges', () => {
  72. this.view._disableRendering( true );
  73. }, { priority: 'highest' } );
  74. this.listenTo( this.model, '_afterChanges', () => {
  75. this.view._disableRendering( false );
  76. }, { priority: 'lowest' } );
  77. // Whenever model document is changed, convert those changes to the view (using model.Document#differ).
  78. // Do it on 'low' priority, so changes are converted after other listeners did their job.
  79. // Also convert model selection.
  80. this.listenTo( doc, 'change', () => {
  81. this.view.change( writer => {
  82. this.downcastDispatcher.convertChanges( doc.differ, markers, writer );
  83. this.downcastDispatcher.convertSelection( selection, markers, writer );
  84. } );
  85. }, { priority: 'low' } );
  86. // Convert selection from the view to the model when it changes in the view.
  87. this.listenTo( this.view.document, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );
  88. // Attach default model converters.
  89. this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
  90. this.downcastDispatcher.on( 'remove', remove(), { priority: 'low' } );
  91. // Attach default model selection converters.
  92. this.downcastDispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
  93. this.downcastDispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } );
  94. this.downcastDispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
  95. // Binds {@link module:engine/view/document~Document#roots view roots collection} to
  96. // {@link module:engine/model/document~Document#roots model roots collection} so creating
  97. // model root automatically creates corresponding view root.
  98. this.view.document.roots.bindTo( this.model.document.roots ).using( root => {
  99. // $graveyard is a special root that has no reflection in the view.
  100. if ( root.rootName == '$graveyard' ) {
  101. return null;
  102. }
  103. const viewRoot = new RootEditableElement( this.view.document, root.name );
  104. viewRoot.rootName = root.rootName;
  105. this.mapper.bindElements( root, viewRoot );
  106. return viewRoot;
  107. } );
  108. // @if CK_DEBUG_ENGINE // initDocumentDumping( this.model.document );
  109. // @if CK_DEBUG_ENGINE // initDocumentDumping( this.view.document );
  110. // @if CK_DEBUG_ENGINE // dumpTrees( this.model.document, this.model.document.version );
  111. // @if CK_DEBUG_ENGINE // dumpTrees( this.view.document, this.model.document.version );
  112. // @if CK_DEBUG_ENGINE // this.model.document.on( 'change', () => {
  113. // @if CK_DEBUG_ENGINE // dumpTrees( this.view.document, this.model.document.version );
  114. // @if CK_DEBUG_ENGINE // }, { priority: 'lowest' } );
  115. }
  116. /**
  117. * Removes all event listeners attached to the `EditingController`. Destroys all objects created
  118. * by `EditingController` that need to be destroyed.
  119. */
  120. destroy() {
  121. this.view.destroy();
  122. this.stopListening();
  123. }
  124. }
  125. mix( EditingController, ObservableMixin );