model.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  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/model
  7. */
  8. // Load all basic deltas and transformations, they register themselves.
  9. import './delta/basic-deltas';
  10. import './delta/basic-transformations';
  11. import Batch from './batch';
  12. import Writer from './writer';
  13. import Schema from './schema';
  14. import Document from './document';
  15. import MarkerCollection from './markercollection';
  16. import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
  17. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  18. import deltaTransform from './delta/transform';
  19. import ModelElement from './element';
  20. import ModelRange from './range';
  21. import insertContent from './utils/insertcontent';
  22. import deleteContent from './utils/deletecontent';
  23. import modifySelection from './utils/modifyselection';
  24. import getSelectedContent from './utils/getselectedcontent';
  25. /**
  26. * Editors data model class. Model defines all data: either nodes users see in editable roots, grouped as the
  27. * {@link module:engine/model/model~Model#document}, and all detached nodes, used to data manipulation. All of them are
  28. * created and modified by the {@link module:engine/model/writer~Writer}, which can be get using
  29. * {@link module:engine/model/model~Model#change} or {@link module:engine/model/model~Model#enqueueChange} methods.
  30. */
  31. export default class Model {
  32. constructor() {
  33. /**
  34. * All callbacks added by {@link module:engine/model/model~Model#change} or
  35. * {@link module:engine/model/model~Model#enqueueChange} methods waiting to be executed.
  36. *
  37. * @private
  38. * @type {Array.<Function>}
  39. */
  40. this._pendingChanges = [];
  41. /**
  42. * Editors document model.
  43. *
  44. * @member {module:engine/model/document~Document}
  45. */
  46. this.document = new Document( this );
  47. /**
  48. * Schema for editors model.
  49. *
  50. * @member {module:engine/model/schema~Schema}
  51. */
  52. this.schema = new Schema();
  53. /**
  54. * Models markers' collection.
  55. *
  56. * @readonly
  57. * @member {module:engine/model/markercollection~MarkerCollection}
  58. */
  59. this.markers = new MarkerCollection();
  60. [ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation' ]
  61. .forEach( methodName => this.decorate( methodName ) );
  62. // Register some default abstract entities.
  63. this.schema.register( '$root', { isLimit: true } );
  64. this.schema.register( '$block', { allowIn: '$root' } );
  65. this.schema.register( '$text', { allowIn: '$block' } );
  66. // TODO review
  67. // Create an "all allowed" context in the schema for processing the pasted content.
  68. // Read: https://github.com/ckeditor/ckeditor5-engine/issues/638#issuecomment-255086588
  69. this.schema.register( '$clipboardHolder', {
  70. allowContentOf: '$root'
  71. } );
  72. this.schema.extend( '$text', { allowIn: '$clipboardHolder' } );
  73. }
  74. /**
  75. * Change method is the primary way of changing the model. You should use it to modify any node, including detached
  76. * nodes, not added to the {@link module:engine/model/model~Model#document}.
  77. *
  78. * model.change( writer => {
  79. * writer.insertText( 'foo', paragraph, 'end' );
  80. * } );
  81. *
  82. * All changes inside the change block use the same {@link module:engine/model/batch~Batch} so share the same
  83. * undo step.
  84. *
  85. * model.change( writer => {
  86. * writer.insertText( 'foo', paragraph, 'end' ); // foo
  87. *
  88. * model.change( writer => {
  89. * writer.insertText( 'bar', paragraph, 'end' ); // foobar
  90. * } );
  91. *
  92. * writer.insertText( 'bom', paragraph, 'end' ); // foobarbom
  93. * } );
  94. *
  95. * Change block is executed imminently.
  96. *
  97. * You can also return a value from the change block.
  98. *
  99. * const img = model.change( writer => {
  100. * return writer.createElement( 'img' );
  101. * } );
  102. *
  103. * When the outermost block is done the {@link #event:change} event is fired.
  104. *
  105. * @see #enqueueChange
  106. * @fires event:change
  107. * @fires event:changesDone
  108. * @param {Function} callback Callback function which may modify the model.
  109. * @returns {*} Value returned by the callback
  110. */
  111. change( callback ) {
  112. if ( this._pendingChanges.length === 0 ) {
  113. this._pendingChanges.push( { batch: new Batch(), callback } );
  114. return this._runPendingChanges()[ 0 ];
  115. } else {
  116. return callback( this._currentWriter );
  117. }
  118. }
  119. /**
  120. * `enqueueChange` method is very similar to the {@link #change change method}, with two major differences.
  121. *
  122. * First, the callback of the `enqueueChange` is executed when all other changes are done. It might be executed
  123. * imminently if it is not nested in any other change block, but if it is nested in another change it will be delayed
  124. * and executed after the outermost block. If will be also executed after all previous `enqueueChange` blocks.
  125. *
  126. * model.change( writer => {
  127. * console.log( 1 );
  128. *
  129. * model.enqueueChange( writer => {
  130. * console.log( 3 );
  131. * } );
  132. *
  133. * console.log( 2 );
  134. * } );
  135. *
  136. * Second, it let you define the {@link module:engine/model/batch~Batch} to which you want to add your changes.
  137. * By default it creates a new batch, note that in the sample above `change` and `enqueueChange` blocks use a different
  138. * batch (and different {@link module:engine/model/writer~Writer} since each of them operates on the separate batch).
  139. *
  140. * Using `enqueueChange` block you can also add some changes to the batch you used before.
  141. *
  142. * model.enqueueChange( batch, writer => {
  143. * writer.insertText( 'foo', paragraph, 'end' );
  144. * } );
  145. *
  146. * @fires event:change
  147. * @fires event:changesDone
  148. * @param {module:engine/model/batch~Batch|String} batchOrType Batch or batch type should be used in the callback.
  149. * If not defined new batch will be created.
  150. * @param {Function} callback Callback function which may modify the model.
  151. */
  152. enqueueChange( batchOrType, callback ) {
  153. if ( typeof batchOrType === 'string' ) {
  154. batchOrType = new Batch( batchOrType );
  155. } else if ( typeof batchOrType == 'function' ) {
  156. callback = batchOrType;
  157. batchOrType = new Batch();
  158. }
  159. this._pendingChanges.push( { batch: batchOrType, callback } );
  160. if ( this._pendingChanges.length == 1 ) {
  161. this._runPendingChanges();
  162. }
  163. }
  164. /**
  165. * Common part of {@link module:engine/model/model~Model#change} and {@link module:engine/model/model~Model#enqueueChange}
  166. * which calls callbacks and returns array of values returned by these callbacks.
  167. *
  168. * @private
  169. * @returns {Array.<*>} Array of values returned by callbacks.
  170. */
  171. _runPendingChanges() {
  172. const ret = [];
  173. while ( this._pendingChanges.length ) {
  174. this._currentWriter = new Writer( this, this._pendingChanges[ 0 ].batch );
  175. ret.push( this._pendingChanges[ 0 ].callback( this._currentWriter ) );
  176. this.fire( 'change' );
  177. this._pendingChanges.shift();
  178. this._currentWriter = null;
  179. }
  180. this.fire( 'changesDone' );
  181. return ret;
  182. }
  183. /**
  184. * {@link module:utils/observablemixin~ObservableMixin#decorate Decorated} function to apply
  185. * {@link module:engine/model/operation/operation~Operation operations} on the model.
  186. *
  187. * @param {module:engine/model/operation/operation~Operation} operation Operation to apply
  188. * @returns {Object} Object with additional information about the applied changes. It properties depends on the
  189. * operation type.
  190. */
  191. applyOperation( operation ) {
  192. return operation._execute();
  193. }
  194. /**
  195. * Transforms two sets of deltas by themselves. Returns both transformed sets.
  196. *
  197. * @param {Array.<module:engine/model/delta/delta~Delta>} deltasA Array with the first set of deltas to transform. These
  198. * deltas are considered more important (than `deltasB`) when resolving conflicts.
  199. * @param {Array.<module:engine/model/delta/delta~Delta>} deltasB Array with the second set of deltas to transform. These
  200. * deltas are considered less important (than `deltasA`) when resolving conflicts.
  201. * @param {Boolean} [useContext=false] When set to `true`, transformation will store and use additional context
  202. * information to guarantee more expected results. Should be used whenever deltas related to already applied
  203. * deltas are transformed (for example when undoing changes).
  204. * @returns {Object}
  205. * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasA The first set of deltas transformed
  206. * by the second set of deltas.
  207. * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasB The second set of deltas transformed
  208. * by the first set of deltas.
  209. */
  210. transformDeltas( deltasA, deltasB, useContext = false ) {
  211. return deltaTransform.transformDeltaSets( deltasA, deltasB, useContext ? this.document : null );
  212. }
  213. /**
  214. * See {@link module:engine/model/utils/insertcontent~insertContent}.
  215. *
  216. * @fires insertContent
  217. * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
  218. * @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
  219. */
  220. insertContent( content, selection ) {
  221. insertContent( this, content, selection );
  222. }
  223. /**
  224. * See {@link module:engine/model/utils/deletecontent.deleteContent}.
  225. *
  226. * Note: For the sake of predictability, the resulting selection should always be collapsed.
  227. * In cases where a feature wants to modify deleting behavior so selection isn't collapsed
  228. * (e.g. a table feature may want to keep row selection after pressing <kbd>Backspace</kbd>),
  229. * then that behavior should be implemented in the view's listener. At the same time, the table feature
  230. * will need to modify this method's behavior too, e.g. to "delete contents and then collapse
  231. * the selection inside the last selected cell" or "delete the row and collapse selection somewhere near".
  232. * That needs to be done in order to ensure that other features which use `deleteContent()` will work well with tables.
  233. *
  234. * @fires deleteContent
  235. * @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted.
  236. * @param {Object} options See {@link module:engine/model/utils/deletecontent~deleteContent}'s options.
  237. */
  238. deleteContent( selection, options ) {
  239. deleteContent( this, selection, options );
  240. }
  241. /**
  242. * See {@link module:engine/model/utils/modifyselection~modifySelection}.
  243. *
  244. * @fires modifySelection
  245. * @param {module:engine/model/selection~Selection} selection The selection to modify.
  246. * @param {Object} options See {@link module:engine/model/utils/modifyselection.modifySelection}'s options.
  247. */
  248. modifySelection( selection, options ) {
  249. modifySelection( this, selection, options );
  250. }
  251. /**
  252. * See {@link module:engine/model/utils/getselectedcontent~getSelectedContent}.
  253. *
  254. * @fires getSelectedContent
  255. * @param {module:engine/model/selection~Selection} selection The selection of which content will be retrieved.
  256. * @returns {module:engine/model/documentfragment~DocumentFragment} Document fragment holding the clone of the selected content.
  257. */
  258. getSelectedContent( selection ) {
  259. return getSelectedContent( this, selection );
  260. }
  261. /**
  262. * Checks whether given {@link module:engine/model/range~Range range} or {@link module:engine/model/element~Element element}
  263. * has any content.
  264. *
  265. * Content is any text node or element which is registered in {@link module:engine/model/schema~Schema schema}.
  266. *
  267. * @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
  268. * @returns {Boolean}
  269. */
  270. hasContent( rangeOrElement ) {
  271. if ( rangeOrElement instanceof ModelElement ) {
  272. rangeOrElement = ModelRange.createIn( rangeOrElement );
  273. }
  274. if ( rangeOrElement.isCollapsed ) {
  275. return false;
  276. }
  277. for ( const item of rangeOrElement.getItems() ) {
  278. // Remember, `TreeWalker` returns always `textProxy` nodes.
  279. if ( item.is( 'textProxy' ) || this.schema.isObject( item.name ) ) {
  280. return true;
  281. }
  282. }
  283. return false;
  284. }
  285. /**
  286. * Removes all events listeners set by model instance and destroy Document.
  287. */
  288. destroy() {
  289. this.document.destroy();
  290. this.stopListening();
  291. }
  292. }
  293. mix( Model, ObservableMixin );
  294. /**
  295. * Fired after leaving each {@link module:engine/model/model~Model#enqueueChange} block or outermost
  296. * {@link module:engine/model/model~Model#change} block.
  297. * Have the same parameters as {@link module:engine/model/model~Model#change}.
  298. *
  299. * @event change
  300. */
  301. /**
  302. * Fired when all queued model changes are done.
  303. *
  304. * @see #change
  305. * @see #enqueueChange
  306. * @event changesDone
  307. */
  308. /**
  309. * Fired every time any {@link module:engine/model/operation/operation~Operation operation} is applied on the model
  310. * using {@link #applyOperation}.
  311. *
  312. * Note that this is an internal event for the specific use-cases. You can use it if you need to know about each operation
  313. * applied on the document, but in most cases {@link #change} event which is fired when all changes in a
  314. * {@link module:engine/model/batch~Batch} are applied, is a better choice.
  315. *
  316. * With the high priority operation is validated.
  317. *
  318. * With the normal priority operation is executed. After that priority you will be able to get additional
  319. * information about the applied changes returned by {@link module:engine/model/operation/operation~Operation#_execute}
  320. * as `evt.return`.
  321. *
  322. * With the low priority the {@link module:engine/model/document~Document} listen on this event and updates its version.
  323. *
  324. * @event applyOperation
  325. * @param {Array} args Arguments of the `applyOperation` which are an array with a single element:
  326. * {@link module:engine/model/operation/operation~Operation operation}.
  327. */
  328. /**
  329. * Event fired when {@link #insertContent} method is called.
  330. *
  331. * The {@link #insertContent default action of that method} is implemented as a
  332. * listener to this event so it can be fully customized by the features.
  333. *
  334. * @event insertContent
  335. * @param {Array} args The arguments passed to the original method.
  336. */
  337. /**
  338. * Event fired when {@link #deleteContent} method is called.
  339. *
  340. * The {@link #deleteContent default action of that method} is implemented as a
  341. * listener to this event so it can be fully customized by the features.
  342. *
  343. * @event deleteContent
  344. * @param {Array} args The arguments passed to the original method.
  345. */
  346. /**
  347. * Event fired when {@link #modifySelection} method is called.
  348. *
  349. * The {@link #modifySelection default action of that method} is implemented as a
  350. * listener to this event so it can be fully customized by the features.
  351. *
  352. * @event modifySelection
  353. * @param {Array} args The arguments passed to the original method.
  354. */
  355. /**
  356. * Event fired when {@link #getSelectedContent} method is called.
  357. *
  358. * The {@link #getSelectedContent default action of that method} is implemented as a
  359. * listener to this event so it can be fully customized by the features.
  360. *
  361. * @event getSelectedContent
  362. * @param {Array} args The arguments passed to the original method.
  363. */