document.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/document
  7. */
  8. import Differ from './differ';
  9. import Range from './range';
  10. import Position from './position';
  11. import RootElement from './rootelement';
  12. import History from './history';
  13. import DocumentSelection from './documentselection';
  14. import TreeWalker from './treewalker';
  15. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  16. import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
  17. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  18. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  19. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  20. import { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckeditor5-utils/src/unicode';
  21. const graveyardName = '$graveyard';
  22. /**
  23. * Document tree model describes all editable data in the editor. It may contain multiple
  24. * {@link module:engine/model/document~Document#roots root elements}, for example if the editor have multiple editable areas,
  25. * each area will be represented by the separate root.
  26. *
  27. * @mixes module:utils/emittermixin~EmitterMixin
  28. */
  29. export default class Document {
  30. /**
  31. * Creates an empty document instance with no {@link #roots} (other than
  32. * the {@link #graveyard graveyard root}).
  33. */
  34. constructor( model ) {
  35. /**
  36. * {@link module:engine/model/model~Model} the document is part of.
  37. *
  38. * @readonly
  39. * @member {module:engine/model/model~Model}
  40. */
  41. this.model = model;
  42. /**
  43. * Document version. It starts from `0` and every operation increases the version number. It is used to ensure that
  44. * operations are applied on the proper document version.
  45. * If the {@link module:engine/model/operation/operation~Operation#baseVersion} will not match document version the
  46. * {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown.
  47. *
  48. * @readonly
  49. * @member {Number}
  50. */
  51. this.version = 0;
  52. /**
  53. * Document's history.
  54. *
  55. * **Note:** Be aware that deltas applied to the document might get removed or changed.
  56. *
  57. * @readonly
  58. * @member {module:engine/model/history~History}
  59. */
  60. this.history = new History( this );
  61. /**
  62. * Selection done on this document.
  63. *
  64. * @readonly
  65. * @member {module:engine/model/documentselection~DocumentSelection}
  66. */
  67. this.selection = new DocumentSelection( this );
  68. /**
  69. * List of roots that are owned and managed by this document. Use {@link #createRoot} and
  70. * {@link #getRoot} to manipulate it.
  71. *
  72. * @readonly
  73. * @member {module:utils/collection~Collection}
  74. */
  75. this.roots = new Collection( { idProperty: 'rootName' } );
  76. /**
  77. * Model differ object. Its role is to buffer changes done on model document and then calculate a diff of those changes.
  78. *
  79. * @readonly
  80. * @member {module:engine/model/differ~Differ}
  81. */
  82. this.differ = new Differ();
  83. /**
  84. * Post-fixer callbacks registered to the model.
  85. *
  86. * @private
  87. * @member {Set}
  88. */
  89. this._postFixers = new Set();
  90. // Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
  91. this.createRoot( '$root', graveyardName );
  92. // First, if the operation is a document operation check if it's base version is correct.
  93. this.listenTo( model, 'applyOperation', ( evt, args ) => {
  94. const operation = args[ 0 ];
  95. if ( operation.isDocumentOperation && operation.baseVersion !== this.version ) {
  96. /**
  97. * Only operations with matching versions can be applied.
  98. *
  99. * @error document-applyOperation-wrong-version
  100. * @param {module:engine/model/operation/operation~Operation} operation
  101. */
  102. throw new CKEditorError(
  103. 'model-document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
  104. { operation }
  105. );
  106. }
  107. }, { priority: 'highest' } );
  108. // Then, still before an operation is applied on model, buffer the change in differ.
  109. this.listenTo( model, 'applyOperation', ( evt, args ) => {
  110. const operation = args[ 0 ];
  111. if ( operation.isDocumentOperation ) {
  112. this.differ.bufferOperation( operation );
  113. }
  114. }, { priority: 'high' } );
  115. // After the operation is applied, bump document's version and add the operation to the history.
  116. this.listenTo( model, 'applyOperation', ( evt, args ) => {
  117. const operation = args[ 0 ];
  118. if ( operation.isDocumentOperation ) {
  119. this.version++;
  120. this.history.addDelta( operation.delta );
  121. }
  122. }, { priority: 'low' } );
  123. // Listen to selection changes. If selection changed, mark it.
  124. let hasSelectionChanged = false;
  125. this.listenTo( this.selection, 'change', () => {
  126. hasSelectionChanged = true;
  127. } );
  128. // Wait for `_change` event from model, which signalizes that outermost change block has finished.
  129. // When this happens, check if there were any changes done on document, and if so, call post fixers,
  130. // fire `change` event for features and conversion and then reset the differ.
  131. this.listenTo( model, '_change', ( evt, writer ) => {
  132. if ( !this.differ.isEmpty || hasSelectionChanged ) {
  133. this._callPostFixers( writer );
  134. this.fire( 'change' );
  135. this.differ.reset();
  136. hasSelectionChanged = false;
  137. }
  138. } );
  139. // Buffer marker changes.
  140. // This is not covered in buffering operations because markers may change outside of them (when they
  141. // are modified using `model.markers` collection, not through `MarkerOperation`).
  142. this.listenTo( model.markers, 'add', ( evt, marker ) => {
  143. // TODO: Should filter out changes of markers that are not in document.
  144. // Whenever a new marker is added, buffer that change.
  145. this.differ.bufferMarkerChange( marker.name, null, marker.getRange() );
  146. // Whenever marker changes, buffer that.
  147. marker.on( 'change', ( evt, oldRange ) => {
  148. this.differ.bufferMarkerChange( marker.name, oldRange, marker.getRange() );
  149. } );
  150. } );
  151. this.listenTo( model.markers, 'remove', ( evt, marker ) => {
  152. // TODO: Should filter out changes of markers that are not in document.
  153. // Whenever marker is removed, buffer that change.
  154. this.differ.bufferMarkerChange( marker.name, marker.getRange(), null );
  155. } );
  156. }
  157. /**
  158. * Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
  159. *
  160. * @readonly
  161. * @member {module:engine/model/rootelement~RootElement}
  162. */
  163. get graveyard() {
  164. return this.getRoot( graveyardName );
  165. }
  166. /**
  167. * Creates a new top-level root.
  168. *
  169. * @param {String} [elementName='$root'] Element name. Defaults to `'$root'` which also have
  170. * some basic schema defined (`$block`s are allowed inside the `$root`). Make sure to define a proper
  171. * schema if you use a different name.
  172. * @param {String} [rootName='main'] Unique root name.
  173. * @returns {module:engine/model/rootelement~RootElement} Created root.
  174. */
  175. createRoot( elementName = '$root', rootName = 'main' ) {
  176. if ( this.roots.get( rootName ) ) {
  177. /**
  178. * Root with specified name already exists.
  179. *
  180. * @error model-document-createRoot-name-exists
  181. * @param {module:engine/model/document~Document} doc
  182. * @param {String} name
  183. */
  184. throw new CKEditorError(
  185. 'model-document-createRoot-name-exists: Root with specified name already exists.',
  186. { name: rootName }
  187. );
  188. }
  189. const root = new RootElement( this, elementName, rootName );
  190. this.roots.add( root );
  191. return root;
  192. }
  193. /**
  194. * Removes all events listeners set by document instance.
  195. */
  196. destroy() {
  197. this.selection.destroy();
  198. this.stopListening();
  199. }
  200. /**
  201. * Returns top-level root by its name.
  202. *
  203. * @param {String} [name='main'] Unique root name.
  204. * @returns {module:engine/model/rootelement~RootElement|null} Root registered under given name or null when
  205. * there is no root of given name.
  206. */
  207. getRoot( name = 'main' ) {
  208. return this.roots.get( name );
  209. }
  210. /**
  211. * Returns array with names of all roots (without the {@link #graveyard}) added to the document.
  212. *
  213. * @returns {Array.<String>} Roots names.
  214. */
  215. getRootNames() {
  216. return Array.from( this.roots, root => root.rootName ).filter( name => name != graveyardName );
  217. }
  218. /**
  219. * Basing on given `position`, finds and returns a {@link module:engine/model/range~Range Range} instance that is
  220. * nearest to that `position` and is a correct range for selection.
  221. *
  222. * Correct selection range might be collapsed - when it's located in position where text node can be placed.
  223. * Non-collapsed range is returned when selection can be placed around element marked as "object" in
  224. * {@link module:engine/model/schema~Schema schema}.
  225. *
  226. * Direction of searching for nearest correct selection range can be specified as:
  227. * * `both` - searching will be performed in both ways,
  228. * * `forward` - searching will be performed only forward,
  229. * * `backward` - searching will be performed only backward.
  230. *
  231. * When valid selection range cannot be found, `null` is returned.
  232. *
  233. * @param {module:engine/model/position~Position} position Reference position where new selection range should be looked for.
  234. * @param {'both'|'forward'|'backward'} [direction='both'] Search direction.
  235. * @returns {module:engine/model/range~Range|null} Nearest selection range or `null` if one cannot be found.
  236. */
  237. getNearestSelectionRange( position, direction = 'both' ) {
  238. const schema = this.model.schema;
  239. // Return collapsed range if provided position is valid.
  240. if ( schema.checkChild( position, '$text' ) ) {
  241. return new Range( position );
  242. }
  243. let backwardWalker, forwardWalker;
  244. if ( direction == 'both' || direction == 'backward' ) {
  245. backwardWalker = new TreeWalker( { startPosition: position, direction: 'backward' } );
  246. }
  247. if ( direction == 'both' || direction == 'forward' ) {
  248. forwardWalker = new TreeWalker( { startPosition: position } );
  249. }
  250. for ( const data of combineWalkers( backwardWalker, forwardWalker ) ) {
  251. const type = ( data.walker == backwardWalker ? 'elementEnd' : 'elementStart' );
  252. const value = data.value;
  253. if ( value.type == type && schema.isObject( value.item ) ) {
  254. return Range.createOn( value.item );
  255. }
  256. if ( schema.checkChild( value.nextPosition, '$text' ) ) {
  257. return new Range( value.nextPosition );
  258. }
  259. }
  260. return null;
  261. }
  262. /**
  263. * Used to register a post-fixer callback. Post-fixers mechanism guarantees that the features that listen to
  264. * {@link module:engine/model/model~Model#event:_change model's change event} will operate on a correct model state.
  265. *
  266. * Execution of a feature may lead to an incorrect document tree state. The callbacks are used to fix document tree after
  267. * it has changed. Post-fixers are fired just after all changes from the outermost change block were applied but
  268. * before {@link module:engine/model/document~Document#event:change} is fired. If a post-fixer callback made a change,
  269. * it should return `true`. When this happens, all post-fixers are fired again to check if something else should
  270. * not be fixed in the new document tree state.
  271. *
  272. * As a parameter, a post-fixer callback receives {@link module:engine/model/writer~Writer} instance connected with the executed
  273. * changes block. Thanks to that, all changes done by the callback will be added to the same {@link module:engine/model/batch~Batch}
  274. * (and undo step) as the original changes. This makes post-fixer changes transparent for the user.
  275. *
  276. * An example of a post-fixer is a callback that checks if all the data was removed from the editor. If so, the
  277. * callback should add an empty paragraph, so that the editor is never empty:
  278. *
  279. * document.registerPostFixer( writer => {
  280. * const changes = document.differ.getChanges();
  281. *
  282. * // Check if the changes lead to an empty root in an editor.
  283. * let applied = false;
  284. *
  285. * for ( const entry of changes ) {
  286. * if ( entry.type == 'remove' && entry.position.root.isEmpty ) {
  287. * writer.insertElement( 'paragraph', ModelPosition.createAt( entry.position.root, 0 ) );
  288. *
  289. * applied = true;
  290. * }
  291. * }
  292. *
  293. * return applied;
  294. * } );
  295. *
  296. * @param {Function} postFixer
  297. */
  298. registerPostFixer( postFixer ) {
  299. this._postFixers.add( postFixer );
  300. }
  301. /**
  302. * Custom toJSON method to solve child-parent circular dependencies.
  303. *
  304. * @returns {Object} Clone of this object with the document property changed to string.
  305. */
  306. toJSON() {
  307. const json = clone( this );
  308. // Due to circular references we need to remove parent reference.
  309. json.selection = '[engine.model.DocumentSelection]';
  310. json.model = '[engine.model.Model]';
  311. return json;
  312. }
  313. /**
  314. * Returns default root for this document which is either the first root that was added to the the document using
  315. * {@link #createRoot} or the {@link #graveyard graveyard root} if no other roots were created.
  316. *
  317. * @protected
  318. * @returns {module:engine/model/rootelement~RootElement} The default root for this document.
  319. */
  320. _getDefaultRoot() {
  321. for ( const root of this.roots ) {
  322. if ( root !== this.graveyard ) {
  323. return root;
  324. }
  325. }
  326. return this.graveyard;
  327. }
  328. /**
  329. * Returns a default range for this selection. The default range is a collapsed range that starts and ends
  330. * at the beginning of this selection's document's {@link #_getDefaultRoot default root}.
  331. *
  332. * @protected
  333. * @returns {module:engine/model/range~Range}
  334. */
  335. _getDefaultRange() {
  336. const defaultRoot = this._getDefaultRoot();
  337. // Find the first position where the selection can be put.
  338. const position = new Position( defaultRoot, [ 0 ] );
  339. const nearestRange = this.getNearestSelectionRange( position );
  340. // If valid selection range is not found - return range collapsed at the beginning of the root.
  341. return nearestRange || new Range( position );
  342. }
  343. /**
  344. * Checks whether given {@link module:engine/model/range~Range range} is a valid range for
  345. * {@link #selection document's selection}.
  346. *
  347. * @private
  348. * @param {module:engine/model/range~Range} range Range to check.
  349. * @returns {Boolean} `true` if `range` is valid, `false` otherwise.
  350. */
  351. _validateSelectionRange( range ) {
  352. return validateTextNodePosition( range.start ) && validateTextNodePosition( range.end );
  353. }
  354. /**
  355. * Performs post-fixer loops. Executes post-fixer callbacks as long as neither of them has done any changes to model.
  356. *
  357. * @private
  358. */
  359. _callPostFixers( writer ) {
  360. let wasFixed = false;
  361. do {
  362. for ( const callback of this._postFixers ) {
  363. wasFixed = callback( writer );
  364. if ( wasFixed ) {
  365. break;
  366. }
  367. }
  368. } while ( wasFixed );
  369. }
  370. /**
  371. * Fired after outermost {@link module:engine/model/model~Model#change change} or
  372. * {@link module:engine/model/model~Model#enqueueChange enqueueChange} block has been executed and
  373. * document model tree was changed during its execution.
  374. *
  375. * @event change
  376. */
  377. }
  378. mix( Document, EmitterMixin );
  379. // Checks whether given range boundary position is valid for document selection, meaning that is not between
  380. // unicode surrogate pairs or base character and combining marks.
  381. function validateTextNodePosition( rangeBoundary ) {
  382. const textNode = rangeBoundary.textNode;
  383. if ( textNode ) {
  384. const data = textNode.data;
  385. const offset = rangeBoundary.offset - textNode.startOffset;
  386. return !isInsideSurrogatePair( data, offset ) && !isInsideCombinedSymbol( data, offset );
  387. }
  388. return true;
  389. }
  390. // Generator function returning values from provided walkers, switching between them at each iteration. If only one walker
  391. // is provided it will return data only from that walker.
  392. //
  393. // @param {module:engine/module/treewalker~TreeWalker} [backward] Walker iterating in backward direction.
  394. // @param {module:engine/module/treewalker~TreeWalker} [forward] Walker iterating in forward direction.
  395. // @returns {Iterable.<Object>} Object returned at each iteration contains `value` and `walker` (informing which walker returned
  396. // given value) fields.
  397. function* combineWalkers( backward, forward ) {
  398. let done = false;
  399. while ( !done ) {
  400. done = true;
  401. if ( backward ) {
  402. const step = backward.next();
  403. if ( !step.done ) {
  404. done = false;
  405. yield {
  406. walker: backward,
  407. value: step.value
  408. };
  409. }
  410. }
  411. if ( forward ) {
  412. const step = forward.next();
  413. if ( !step.done ) {
  414. done = false;
  415. yield {
  416. walker: forward,
  417. value: step.value
  418. };
  419. }
  420. }
  421. }
  422. }