editingcontroller.js 4.9 KB

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