document.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  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 '../emittermixin.js';
  14. import CKEditorError from '../ckeditorerror.js';
  15. import utils from '../utils.js';
  16. import CharacterProxy from './characterproxy.js';
  17. import Schema from '../schema.js';
  18. const graveyardSymbol = Symbol( 'graveyard' );
  19. /**
  20. * Document tree model describes all editable data in the editor. It may contain multiple
  21. * {@link core.treeModel.Document#roots root elements}, for example if the editor have multiple editable areas, each area will be
  22. * represented by the separate root.
  23. *
  24. * All changes in the document are done by {@link core.treeModel.operation.Operation operations}. To create operations in
  25. * the simple way use use the {@link core.treeModel.Batch} API, for example:
  26. *
  27. * doc.batch().insert( position, nodes ).split( otherPosition );
  28. *
  29. * @see core.treeModel.Document#batch
  30. *
  31. * @memberOf core.treeModel
  32. */
  33. export default class Document {
  34. /**
  35. * Creates an empty document instance with no {@link core.treeModel.Document#roots} (other than graveyard).
  36. */
  37. constructor() {
  38. /**
  39. * List of roots that are owned and managed by this document.
  40. *
  41. * @readonly
  42. * @member {Map} core.treeModel.Document#roots
  43. */
  44. this.roots = new Map();
  45. /**
  46. * Document version. It starts from `0` and every operation increases the version number. It is used to ensure that
  47. * operations are applied on the proper document version. If the {@link core.treeModel.operation.Operation#baseVersion} will
  48. * not match document version the {@link document-applyOperation-wrong-version} error is thrown.
  49. *
  50. * @readonly
  51. * @member {Number} core.treeModel.Document#version
  52. */
  53. this.version = 0;
  54. // Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
  55. this.createRoot( graveyardSymbol );
  56. /**
  57. * Array of pending changes. See: {@link core.treeModel.Document#enqueueChanges}.
  58. *
  59. * @private
  60. * @member {Array.<Function>} core.treeModel.Document#_pendingChanges
  61. */
  62. this._pendingChanges = [];
  63. /**
  64. * Selection done on this document.
  65. *
  66. * @readonly
  67. * @member {core.treeModel.Selection} core.treeModel.Document#selection
  68. */
  69. this.selection = new Selection();
  70. this.selection.on( 'update', () => {
  71. this._setSelectionAttributes();
  72. } );
  73. this.on( 'changesDone', () => {
  74. this._setSelectionAttributes();
  75. } );
  76. this.schema = new Schema();
  77. }
  78. /**
  79. * Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
  80. *
  81. * @readonly
  82. * @type {core.treeModel.RootElement}
  83. */
  84. get graveyard() {
  85. return this.getRoot( graveyardSymbol );
  86. }
  87. /**
  88. * This is the entry point for all document changes. All changes on the document are done using
  89. * {@link core.treeModel.operation.Operation operations}. To create operations in the simple way use the
  90. * {@link core.treeModel.Batch} API available via {@link core.treeModel.Document#batch} method.
  91. *
  92. * This method calls {@link core.treeModel.Document#change} event.
  93. *
  94. * @param {core.treeModel.operation.Operation} operation Operation to be applied.
  95. */
  96. applyOperation( operation ) {
  97. if ( operation.baseVersion !== this.version ) {
  98. /**
  99. * Only operations with matching versions can be applied.
  100. *
  101. * @error document-applyOperation-wrong-version
  102. * @param {core.treeModel.operation.Operation} operation
  103. */
  104. throw new CKEditorError(
  105. 'document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
  106. { operation: operation } );
  107. }
  108. let changes = operation._execute();
  109. this.version++;
  110. const batch = operation.delta && operation.delta.batch;
  111. this.fire( 'change', operation.type, changes, batch );
  112. }
  113. /**
  114. * Creates a {@link core.treeModel.Batch} instance which allows to change the document.
  115. *
  116. * @returns {core.treeModel.Batch} Batch instance.
  117. */
  118. batch() {
  119. return new Batch( this );
  120. }
  121. /**
  122. * Creates a new top-level root.
  123. *
  124. * @param {String|Symbol} name Unique root name.
  125. * @returns {core.treeModel.RootElement} Created root.
  126. */
  127. createRoot( name ) {
  128. if ( this.roots.has( name ) ) {
  129. /**
  130. * Root with specified name already exists.
  131. *
  132. * @error document-createRoot-name-exists
  133. * @param {core.treeModel.Document} doc
  134. * @param {String} name
  135. */
  136. throw new CKEditorError(
  137. 'document-createRoot-name-exists: Root with specified name already exists.',
  138. { name: name }
  139. );
  140. }
  141. const root = new RootElement( this );
  142. this.roots.set( name, root );
  143. return root;
  144. }
  145. /**
  146. * Enqueue a callback with document changes. Any changes to be done on document (mostly using {@link core.treeModel.Document#batch} should
  147. * be placed in the queued callback. If no other plugin is changing document at the moment, the callback will be
  148. * called immediately. Otherwise it will wait for all previously queued changes to finish happening. This way
  149. * queued callback will not interrupt other callbacks.
  150. *
  151. * When all queued changes are done {@link core.treeModel.Document#changesDone} event is fired.
  152. *
  153. * @param {Function} callback Callback to enqueue.
  154. */
  155. enqueueChanges( callback ) {
  156. this._pendingChanges.push( callback );
  157. if ( this._pendingChanges.length == 1 ) {
  158. while ( this._pendingChanges.length ) {
  159. this._pendingChanges[ 0 ]();
  160. this._pendingChanges.shift();
  161. }
  162. this.fire( 'changesDone' );
  163. }
  164. }
  165. /**
  166. * Returns top-level root by it's name.
  167. *
  168. * @param {String|Symbol} name Name of the root to get.
  169. * @returns {core.treeModel.RootElement} Root registered under given name.
  170. */
  171. getRoot( name ) {
  172. if ( !this.roots.has( name ) ) {
  173. /**
  174. * Root with specified name does not exist.
  175. *
  176. * @error document-createRoot-root-not-exist
  177. * @param {String} name
  178. */
  179. throw new CKEditorError(
  180. 'document-createRoot-root-not-exist: Root with specified name does not exist.',
  181. { name: name }
  182. );
  183. }
  184. return this.roots.get( name );
  185. }
  186. _setSelectionAttributes() {
  187. if ( !this.selection.hasAnyRange ) {
  188. this.selection.clearAttributes();
  189. } else {
  190. let position = this.selection.getFirstPosition();
  191. let positionParent = position.parent;
  192. let attrs = null;
  193. if ( this.selection.isCollapsed === false ) {
  194. // 1. If selection is a range...
  195. let range = this.selection.getFirstRange();
  196. // ...look for a first character node in that range and take attributes from it.
  197. for ( let item of range ) {
  198. if ( item.type == 'CHARACTER' ) {
  199. attrs = item.item.getAttributes();
  200. break;
  201. }
  202. }
  203. }
  204. // 2. If the selection is a caret or the range does not contain a character node...
  205. if ( !attrs && this.selection.isCollapsed === true ) {
  206. let nodeBefore = positionParent.getChild( position.offset - 1 );
  207. let nodeAfter = positionParent.getChild( position.offset );
  208. // ...look at the node before caret and take attributes from it if it is a character node.
  209. attrs = getAttrsIfCharacter( nodeBefore );
  210. // 3. If not, look at the node after caret...
  211. if ( !attrs ) {
  212. attrs = getAttrsIfCharacter( nodeAfter );
  213. }
  214. // 4. If not, try to find the first character on the left, that is in the same node.
  215. if ( !attrs ) {
  216. let node = nodeBefore;
  217. while ( node && !attrs ) {
  218. node = node.previousSibling;
  219. attrs = getAttrsIfCharacter( node );
  220. }
  221. }
  222. // 5. If not found, try to find the first character on the right, that is in the same node.
  223. if ( !attrs ) {
  224. let node = nodeAfter;
  225. while ( node && !attrs ) {
  226. node = node.nextSibling;
  227. attrs = getAttrsIfCharacter( node );
  228. }
  229. }
  230. // 6. If not found, selection won't get any attributes.
  231. }
  232. if ( attrs ) {
  233. this.selection.setAttributesTo( attrs );
  234. } else {
  235. this.selection.clearAttributes();
  236. }
  237. }
  238. function getAttrsIfCharacter( node ) {
  239. if ( node instanceof CharacterProxy ) {
  240. return node.getAttributes();
  241. }
  242. return null;
  243. }
  244. }
  245. /**
  246. * Fired when document changes by applying an operation.
  247. *
  248. * There are 5 types of change:
  249. *
  250. * * `'insert'` when nodes are inserted,
  251. * * `'remove'` when nodes are removed,
  252. * * `'reinsert'` when remove is undone,
  253. * * `'move'` when nodes are moved,
  254. * * `'attribute'` when attributes change. TODO attribute
  255. *
  256. * Change event is fired after the change is done. This means that any ranges or positions passed in
  257. * `changeInfo` are referencing nodes and paths in updated tree model.
  258. *
  259. * @event core.treeModel.Document#change
  260. * @param {String} type Change type, possible option: `'insert'`, `'remove'`, `'reinsert'`, `'move'`, `'attribute'`.
  261. * @param {Object} changeInfo Additional information about the change.
  262. * @param {core.treeModel.Range} changeInfo.range Range containing changed nodes. Note that for `'remove'` the range will be in the
  263. * {@link core.treeModel.Document#graveyard graveyard root}.
  264. * @param {core.treeModel.Position} [changeInfo.sourcePosition] Change source position. Exists for `'remove'`, `'reinsert'` and `'move'`.
  265. * Note that for 'reinsert' the source position will be in the {@link core.treeModel.Document#graveyard graveyard root}.
  266. * @param {String} [changeInfo.key] Only for `'attribute'` type. Key of changed / inserted / removed attribute.
  267. * @param {*} [changeInfo.oldValue] Only for `'attribute'` type. If the type is `'attribute'` and `oldValue`
  268. * is `undefined` it means that new attribute was inserted. Otherwise it contains changed or removed attribute value.
  269. * @param {*} [changeInfo.newValue] Only for `'attribute'` type. If the type is `'attribute'` and `newValue`
  270. * is `undefined` it means that attribute was removed. Otherwise it contains changed or inserted attribute value.
  271. * @param {core.treeModel.Batch} batch A {@link core.treeModel.Batch batch} of changes which this change is a part of.
  272. */
  273. /**
  274. * Fired when all queued document changes are done. See {@link core.treeModel.Document#enqueueChanges}.
  275. *
  276. * @event core.treeModel.Document#changesDone
  277. */
  278. }
  279. utils.mix( Document, EmitterMixin );