document.js 8.1 KB

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