document.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. CKEDITOR.define( [
  7. 'treemodel/element',
  8. 'treemodel/rootelement',
  9. 'treemodel/batch',
  10. 'emittermixin',
  11. 'utils',
  12. 'ckeditorerror'
  13. ], ( Element, RootElement, Batch, EmitterMixin, utils, CKEditorError ) => {
  14. const graveyardSymbol = Symbol( 'graveyard' );
  15. /**
  16. * Document tree model describes all editable data in the editor. It may contain multiple {@link #roots root elements},
  17. * for example if the editor have multiple editable areas, each area will be represented by the separate root.
  18. *
  19. * All changes in the document are done by {@link treeModel.operation.Operation operations}. To create operations in
  20. * the simple way use use the {@link treeModel.Batch} API, for example:
  21. *
  22. * doc.batch().insert( position, nodes ).split( otherPosition );
  23. *
  24. * @see #batch
  25. *
  26. * @class treeModel.Document
  27. */
  28. class Document {
  29. /**
  30. * Creates an empty document instance with no {@link #roots}.
  31. *
  32. * @constructor
  33. */
  34. constructor() {
  35. /**
  36. * List of roots that are owned and managed by this document.
  37. *
  38. * @readonly
  39. * @property {Map} roots
  40. */
  41. this.roots = new Map();
  42. // Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
  43. this.createRoot( graveyardSymbol );
  44. /**
  45. * Document version. It starts from `0` and every operation increases the version number. It is used to ensure that
  46. * operations are applied on the proper document version. If the {@link treeModel.operation.Operation#baseVersion} will
  47. * not match document version the {@link document-applyOperation-wrong-version} error is thrown.
  48. *
  49. * @readonly
  50. * @property {Number} version
  51. */
  52. this.version = 0;
  53. /**
  54. * Array of pending changes. See: {@link #enqueueChanges}.
  55. *
  56. * @private
  57. * @property {Array.<Function>}
  58. */
  59. this._pendingChanges = [];
  60. }
  61. /**
  62. * Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
  63. *
  64. * @readonly
  65. * @property {treeModel.RootElement} graveyard
  66. */
  67. get graveyard() {
  68. return this.getRoot( graveyardSymbol );
  69. }
  70. /**
  71. * This is the entry point for all document changes. All changes on the document are done using
  72. * {@link treeModel.operation.Operation operations}. To create operations in the simple way use the
  73. * {@link treeModel.Batch} API available via {@link #batch} method.
  74. *
  75. * This method calls {@link #change} event.
  76. *
  77. * @param {treeModel.operation.Operation} operation Operation to be applied.
  78. */
  79. applyOperation( operation ) {
  80. if ( operation.baseVersion !== this.version ) {
  81. /**
  82. * Only operations with matching versions can be applied.
  83. *
  84. * @error document-applyOperation-wrong-version
  85. * @param {treeModel.operation.Operation} operation
  86. */
  87. throw new CKEditorError(
  88. 'document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
  89. { operation: operation } );
  90. }
  91. let changes = operation._execute();
  92. this.version++;
  93. const batch = operation.delta && operation.delta.batch;
  94. this.fire( 'change', operation.type, changes, batch );
  95. }
  96. /**
  97. * Creates a {@link treeModel.Batch} instance which allows to change the document.
  98. *
  99. * @returns {treeModel.Batch} Batch instance.
  100. */
  101. batch() {
  102. return new Batch( this );
  103. }
  104. /**
  105. * Creates a new top-level root.
  106. *
  107. * @param {String|Symbol} name Unique root name.
  108. * @returns {treeModel.RootElement} Created root.
  109. */
  110. createRoot( name ) {
  111. if ( this.roots.has( name ) ) {
  112. /**
  113. * Root with specified name already exists.
  114. *
  115. * @error document-createRoot-name-exists
  116. * @param {treeModel.Document} doc
  117. * @param {String} name
  118. */
  119. throw new CKEditorError(
  120. 'document-createRoot-name-exists: Root with specified name already exists.',
  121. { name: name }
  122. );
  123. }
  124. const root = new RootElement( this );
  125. this.roots.set( name, root );
  126. return root;
  127. }
  128. /**
  129. * Enqueue a callback with document changes. Any changes to be done on document (mostly using {@link #batch} should
  130. * be placed in the queued callback. If no other plugin is changing document at the moment, the callback will be
  131. * called immediately. Otherwise it will wait for all previously queued changes to finish happening. This way
  132. * queued callback will not interrupt other callbacks.
  133. *
  134. * When all queued changes are done {@link #changesDone} event is fired.
  135. *
  136. * @param {Function} callback Callback to enqueue.
  137. */
  138. enqueueChanges( callback ) {
  139. this._pendingChanges.push( callback );
  140. if ( this._pendingChanges.length == 1 ) {
  141. while ( this._pendingChanges.length ) {
  142. this._pendingChanges[ 0 ]();
  143. this._pendingChanges.shift();
  144. }
  145. this.fire( 'changesDone' );
  146. }
  147. }
  148. /**
  149. * Returns top-level root by it's name.
  150. *
  151. * @param {String|Symbol} name Name of the root to get.
  152. * @returns {treeModel.RootElement} Root registered under given name.
  153. */
  154. getRoot( name ) {
  155. if ( !this.roots.has( name ) ) {
  156. /**
  157. * Root with specified name does not exist.
  158. *
  159. * @error document-createRoot-root-not-exist
  160. * @param {String} name
  161. */
  162. throw new CKEditorError(
  163. 'document-createRoot-root-not-exist: Root with specified name does not exist.',
  164. { name: name }
  165. );
  166. }
  167. return this.roots.get( name );
  168. }
  169. /**
  170. * Fired when document changes by applying an operation.
  171. *
  172. * There are 5 types of change:
  173. *
  174. * * 'insert' when nodes are inserted,
  175. * * 'remove' when nodes are removed,
  176. * * 'reinsert' when remove is undone,
  177. * * 'move' when nodes are moved,
  178. * * 'attr' when attributes change.
  179. *
  180. * Change event is fired after the change is done. This means that any ranges or positions passed in
  181. * `changeInfo` are referencing nodes and paths in updated tree model.
  182. *
  183. * @event change
  184. * @param {String} type Change type, possible option: 'insert', 'remove', 'reinsert', 'move', 'attr'.
  185. * @param {Object} changeInfo Additional information about the change.
  186. * @param {treeModel.Range} changeInfo.range Range containing changed nodes. Note that for 'remove' the range will be in the
  187. * {@link #graveyard graveyard root}.
  188. * @param {treeModel.Position} [changeInfo.sourcePosition] Change source position. Exists for 'remove', 'reinsert' and 'move'.
  189. * Note that for 'reinsert' the source position will be in the {@link #graveyard graveyard root}.
  190. * @param {treeModel.Attribute} [changeInfo.oldAttr] Only for 'attr' type. If the type is 'attr' and `oldAttr`
  191. * is `undefined` it means that new attribute was inserted. Otherwise it contains changed or removed attribute.
  192. * @param {treeModel.Attribute} [changeInfo.newAttr] Only for 'attr' type. If the type is 'attr' and `newAttr`
  193. * is `undefined` it means that attribute was removed. Otherwise it contains changed or inserted attribute.
  194. * @param {treeModel.Batch} {@link treeModel.Batch} of changes which this change is a part of.
  195. */
  196. /**
  197. * Fired when all queued document changes are done. See {@link #enqueueChanges}.
  198. *
  199. * @event changesDone
  200. */
  201. }
  202. utils.extend( Document.prototype, EmitterMixin );
  203. return Document;
  204. } );