document.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/document
  7. */
  8. import Range from './range';
  9. import Position from './position';
  10. import RootElement from './rootelement';
  11. import History from './history';
  12. import DocumentSelection from './documentselection';
  13. import TreeWalker from './treewalker';
  14. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  15. import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
  16. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  17. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  18. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  19. import { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckeditor5-utils/src/unicode';
  20. const graveyardName = '$graveyard';
  21. /**
  22. * Document tree model describes all editable data in the editor. It may contain multiple
  23. * {@link module:engine/model/document~Document#roots root elements}, for example if the editor have multiple editable areas,
  24. * each area will be represented by the separate root.
  25. *
  26. * @mixes module:utils/emittermixin~EmitterMixin
  27. */
  28. export default class Document {
  29. /**
  30. * Creates an empty document instance with no {@link #roots} (other than
  31. * the {@link #graveyard graveyard root}).
  32. */
  33. constructor( model ) {
  34. /**
  35. * {@link module:engine/model/model~Model} the document is part of.
  36. *
  37. * @readonly
  38. * @member {module:engine/model/model~Model}
  39. */
  40. this.model = model;
  41. /**
  42. * Document version. It starts from `0` and every operation increases the version number. It is used to ensure that
  43. * operations are applied on the proper document version.
  44. * If the {@link module:engine/model/operation/operation~Operation#baseVersion} will not match document version the
  45. * {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown.
  46. *
  47. * @readonly
  48. * @member {Number}
  49. */
  50. this.version = 0;
  51. /**
  52. * Document's history.
  53. *
  54. * **Note:** Be aware that deltas applied to the document might get removed or changed.
  55. *
  56. * @readonly
  57. * @member {module:engine/model/history~History}
  58. */
  59. this.history = new History( this );
  60. /**
  61. * Selection done on this document.
  62. *
  63. * @readonly
  64. * @member {module:engine/model/documentselection~DocumentSelection}
  65. */
  66. this.selection = new DocumentSelection( this );
  67. /**
  68. * List of roots that are owned and managed by this document. Use {@link #createRoot} and
  69. * {@link #getRoot} to manipulate it.
  70. *
  71. * @readonly
  72. * @member {module:utils/collection~Collection}
  73. */
  74. this.roots = new Collection( { idProperty: 'rootName' } );
  75. // Add events that will ensure selection correctness.
  76. this.selection.on( 'change:range', () => {
  77. for ( const range of this.selection.getRanges() ) {
  78. if ( !this._validateSelectionRange( range ) ) {
  79. /**
  80. * Range from {@link module:engine/model/documentselection~DocumentSelection document selection}
  81. * starts or ends at incorrect position.
  82. *
  83. * @error document-selection-wrong-position
  84. * @param {module:engine/model/range~Range} range
  85. */
  86. throw new CKEditorError(
  87. 'document-selection-wrong-position: Range from document selection starts or ends at incorrect position.',
  88. { range }
  89. );
  90. }
  91. }
  92. } );
  93. // Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
  94. this.createRoot( '$root', graveyardName );
  95. this.listenTo( model, 'applyOperation', ( evt, args ) => {
  96. const operation = args[ 0 ];
  97. if ( operation.isDocumentOperation && operation.baseVersion !== this.version ) {
  98. /**
  99. * Only operations with matching versions can be applied.
  100. *
  101. * @error document-applyOperation-wrong-version
  102. * @param {module:engine/model/operation/operation~Operation} operation
  103. */
  104. throw new CKEditorError(
  105. 'model-document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
  106. { operation }
  107. );
  108. }
  109. operation._validate();
  110. }, { priority: 'highest' } );
  111. this.listenTo( model, 'applyOperation', ( evt, args ) => {
  112. const operation = args[ 0 ];
  113. if ( operation.isDocumentOperation ) {
  114. this.version++;
  115. this.history.addDelta( operation.delta );
  116. this.fire( 'change', operation.type, evt.return, operation.delta.batch, operation.delta.type );
  117. }
  118. }, { priority: 'low' } );
  119. // Temporary compatibility.
  120. model.delegate( 'changesDone' ).to( this );
  121. }
  122. /**
  123. * Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
  124. *
  125. * @readonly
  126. * @member {module:engine/model/rootelement~RootElement}
  127. */
  128. get graveyard() {
  129. return this.getRoot( graveyardName );
  130. }
  131. /**
  132. * Creates a new top-level root.
  133. *
  134. * @param {String} [elementName='$root'] Element name. Defaults to `'$root'` which also have
  135. * some basic schema defined (`$block`s are allowed inside the `$root`). Make sure to define a proper
  136. * schema if you use a different name.
  137. * @param {String} [rootName='main'] Unique root name.
  138. * @returns {module:engine/model/rootelement~RootElement} Created root.
  139. */
  140. createRoot( elementName = '$root', rootName = 'main' ) {
  141. if ( this.roots.get( rootName ) ) {
  142. /**
  143. * Root with specified name already exists.
  144. *
  145. * @error model-document-createRoot-name-exists
  146. * @param {module:engine/model/document~Document} doc
  147. * @param {String} name
  148. */
  149. throw new CKEditorError(
  150. 'model-document-createRoot-name-exists: Root with specified name already exists.',
  151. { name: rootName }
  152. );
  153. }
  154. const root = new RootElement( this, elementName, rootName );
  155. this.roots.add( root );
  156. return root;
  157. }
  158. /**
  159. * Removes all events listeners set by document instance.
  160. */
  161. destroy() {
  162. this.selection.destroy();
  163. this.stopListening();
  164. }
  165. /**
  166. * Returns top-level root by its name.
  167. *
  168. * @param {String} [name='main'] Unique root name.
  169. * @returns {module:engine/model/rootelement~RootElement} Root registered under given name.
  170. */
  171. getRoot( name = 'main' ) {
  172. if ( !this.hasRoot( name ) ) {
  173. /**
  174. * Root with specified name does not exist.
  175. *
  176. * @error model-document-getRoot-root-not-exist
  177. * @param {String} name
  178. */
  179. throw new CKEditorError(
  180. 'model-document-getRoot-root-not-exist: Root with specified name does not exist.',
  181. { name }
  182. );
  183. }
  184. return this.roots.get( name );
  185. }
  186. /**
  187. * Checks if root with given name is defined.
  188. *
  189. * @param {String} name Name of root to check.
  190. * @returns {Boolean}
  191. */
  192. hasRoot( name ) {
  193. return !!this.roots.get( name );
  194. }
  195. /**
  196. * Returns array with names of all roots (without the {@link #graveyard}) added to the document.
  197. *
  198. * @returns {Array.<String>} Roots names.
  199. */
  200. getRootNames() {
  201. return Array.from( this.roots, root => root.rootName ).filter( name => name != graveyardName );
  202. }
  203. /**
  204. * Basing on given `position`, finds and returns a {@link module:engine/model/range~Range Range} instance that is
  205. * nearest to that `position` and is a correct range for selection.
  206. *
  207. * Correct selection range might be collapsed - when it's located in position where text node can be placed.
  208. * Non-collapsed range is returned when selection can be placed around element marked as "object" in
  209. * {@link module:engine/model/schema~Schema schema}.
  210. *
  211. * Direction of searching for nearest correct selection range can be specified as:
  212. * * `both` - searching will be performed in both ways,
  213. * * `forward` - searching will be performed only forward,
  214. * * `backward` - searching will be performed only backward.
  215. *
  216. * When valid selection range cannot be found, `null` is returned.
  217. *
  218. * @param {module:engine/model/position~Position} position Reference position where new selection range should be looked for.
  219. * @param {'both'|'forward'|'backward'} [direction='both'] Search direction.
  220. * @returns {module:engine/model/range~Range|null} Nearest selection range or `null` if one cannot be found.
  221. */
  222. getNearestSelectionRange( position, direction = 'both' ) {
  223. const schema = this.model.schema;
  224. // Return collapsed range if provided position is valid.
  225. if ( schema.checkChild( position, '$text' ) ) {
  226. return new Range( position );
  227. }
  228. let backwardWalker, forwardWalker;
  229. if ( direction == 'both' || direction == 'backward' ) {
  230. backwardWalker = new TreeWalker( { startPosition: position, direction: 'backward' } );
  231. }
  232. if ( direction == 'both' || direction == 'forward' ) {
  233. forwardWalker = new TreeWalker( { startPosition: position } );
  234. }
  235. for ( const data of combineWalkers( backwardWalker, forwardWalker ) ) {
  236. const type = ( data.walker == backwardWalker ? 'elementEnd' : 'elementStart' );
  237. const value = data.value;
  238. if ( value.type == type && schema.isObject( value.item ) ) {
  239. return Range.createOn( value.item );
  240. }
  241. if ( schema.checkChild( value.nextPosition, '$text' ) ) {
  242. return new Range( value.nextPosition );
  243. }
  244. }
  245. return null;
  246. }
  247. /**
  248. * Custom toJSON method to solve child-parent circular dependencies.
  249. *
  250. * @returns {Object} Clone of this object with the document property changed to string.
  251. */
  252. toJSON() {
  253. const json = clone( this );
  254. // Due to circular references we need to remove parent reference.
  255. json.selection = '[engine.model.DocumentSelection]';
  256. json.model = '[engine.model.Model]';
  257. return json;
  258. }
  259. /**
  260. * Returns default root for this document which is either the first root that was added to the the document using
  261. * {@link #createRoot} or the {@link #graveyard graveyard root} if no other roots were created.
  262. *
  263. * @protected
  264. * @returns {module:engine/model/rootelement~RootElement} The default root for this document.
  265. */
  266. _getDefaultRoot() {
  267. for ( const root of this.roots ) {
  268. if ( root !== this.graveyard ) {
  269. return root;
  270. }
  271. }
  272. return this.graveyard;
  273. }
  274. /**
  275. * Returns a default range for this selection. The default range is a collapsed range that starts and ends
  276. * at the beginning of this selection's document's {@link #_getDefaultRoot default root}.
  277. *
  278. * @protected
  279. * @returns {module:engine/model/range~Range}
  280. */
  281. _getDefaultRange() {
  282. const defaultRoot = this._getDefaultRoot();
  283. // Find the first position where the selection can be put.
  284. const position = new Position( defaultRoot, [ 0 ] );
  285. const nearestRange = this.getNearestSelectionRange( position );
  286. // If valid selection range is not found - return range collapsed at the beginning of the root.
  287. return nearestRange || new Range( position );
  288. }
  289. /**
  290. * Checks whether given {@link module:engine/model/range~Range range} is a valid range for
  291. * {@link #selection document's selection}.
  292. *
  293. * @private
  294. * @param {module:engine/model/range~Range} range Range to check.
  295. * @returns {Boolean} `true` if `range` is valid, `false` otherwise.
  296. */
  297. _validateSelectionRange( range ) {
  298. return validateTextNodePosition( range.start ) && validateTextNodePosition( range.end );
  299. }
  300. /**
  301. * Fired when document changes by applying an operation.
  302. *
  303. * There are several types of change:
  304. *
  305. * * 'insert' when nodes are inserted,
  306. * * 'remove' when nodes are removed,
  307. * * 'reinsert' when remove is undone,
  308. * * 'move' when nodes are moved,
  309. * * 'rename' when element is renamed,
  310. * * 'marker' when a marker changes (added, removed or its range is changed),
  311. * * 'addAttribute' when attributes are added,
  312. * * 'removeAttribute' when attributes are removed,
  313. * * 'changeAttribute' when attributes change,
  314. * * 'addRootAttribute' when attribute for root is added,
  315. * * 'removeRootAttribute' when attribute for root is removed,
  316. * * 'changeRootAttribute' when attribute for root changes.
  317. *
  318. * @event change
  319. * @param {String} type Change type.
  320. * @param {Object} data Additional information about the change.
  321. * @param {module:engine/model/range~Range} [data.range] Range in model containing changed nodes. Note that the range state is
  322. * after changes has been done, i.e. for 'remove' the range will be in the {@link #graveyard graveyard root}.
  323. * The range is not defined for root, rename and marker types.
  324. * @param {module:engine/model/position~Position} [data.sourcePosition] Change source position.
  325. * Exists for 'remove', 'reinsert' and 'move'.
  326. * Note that this position state is before changes has been done, i.e. for 'reinsert' the source position will be in the
  327. * {@link #graveyard graveyard root}.
  328. * @param {String} [data.key] Only for attribute types. Key of changed / inserted / removed attribute.
  329. * @param {*} [data.oldValue] Only for 'removeAttribute', 'removeRootAttribute', 'changeAttribute' or
  330. * 'changeRootAttribute' type.
  331. * @param {*} [data.newValue] Only for 'addAttribute', 'addRootAttribute', 'changeAttribute' or
  332. * 'changeRootAttribute' type.
  333. * @param {module:engine/model/rootelement~RootElement} [data.root] Root element which attributes got changed. This is defined
  334. * only for root types.
  335. * @param {module:engine/model/batch~Batch} batch A {@link module:engine/model/batch~Batch batch}
  336. * of changes which this change is a part of.
  337. */
  338. /**
  339. * Fired when all queued document changes are done. See {@link module:engine/model/model~Model#change}.
  340. *
  341. * @event changesDone
  342. */
  343. }
  344. mix( Document, EmitterMixin );
  345. // Checks whether given range boundary position is valid for document selection, meaning that is not between
  346. // unicode surrogate pairs or base character and combining marks.
  347. function validateTextNodePosition( rangeBoundary ) {
  348. const textNode = rangeBoundary.textNode;
  349. if ( textNode ) {
  350. const data = textNode.data;
  351. const offset = rangeBoundary.offset - textNode.startOffset;
  352. return !isInsideSurrogatePair( data, offset ) && !isInsideCombinedSymbol( data, offset );
  353. }
  354. return true;
  355. }
  356. // Generator function returning values from provided walkers, switching between them at each iteration. If only one walker
  357. // is provided it will return data only from that walker.
  358. //
  359. // @param {module:engine/module/treewalker~TreeWalker} [backward] Walker iterating in backward direction.
  360. // @param {module:engine/module/treewalker~TreeWalker} [forward] Walker iterating in forward direction.
  361. // @returns {Iterable.<Object>} Object returned at each iteration contains `value` and `walker` (informing which walker returned
  362. // given value) fields.
  363. function* combineWalkers( backward, forward ) {
  364. let done = false;
  365. while ( !done ) {
  366. done = true;
  367. if ( backward ) {
  368. const step = backward.next();
  369. if ( !step.done ) {
  370. done = false;
  371. yield {
  372. walker: backward,
  373. value: step.value
  374. };
  375. }
  376. }
  377. if ( forward ) {
  378. const step = forward.next();
  379. if ( !step.done ) {
  380. done = false;
  381. yield {
  382. walker: forward,
  383. value: step.value
  384. };
  385. }
  386. }
  387. }
  388. }