8
0

model.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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/model
  7. */
  8. import Batch from './batch';
  9. import Writer from './writer';
  10. import Schema from './schema';
  11. import Document from './document';
  12. import MarkerCollection from './markercollection';
  13. import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
  14. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  15. import ModelElement from './element';
  16. import ModelRange from './range';
  17. import insertContent from './utils/insertcontent';
  18. import deleteContent from './utils/deletecontent';
  19. import modifySelection from './utils/modifyselection';
  20. import getSelectedContent from './utils/getselectedcontent';
  21. import { injectSelectionPostFixer } from './utils/selection-post-fixer';
  22. /**
  23. * Editor's data model. Read about the model in the
  24. * {@glink framework/guides/architecture/editing-engine engine architecture guide}.
  25. *
  26. * @mixes module:utils/observablemixin~ObservableMixin
  27. */
  28. export default class Model {
  29. constructor() {
  30. /**
  31. * Model's marker collection.
  32. *
  33. * @readonly
  34. * @member {module:engine/model/markercollection~MarkerCollection}
  35. */
  36. this.markers = new MarkerCollection();
  37. /**
  38. * Model's document.
  39. *
  40. * @readonly
  41. * @member {module:engine/model/document~Document}
  42. */
  43. this.document = new Document( this );
  44. /**
  45. * Model's schema.
  46. *
  47. * @readonly
  48. * @member {module:engine/model/schema~Schema}
  49. */
  50. this.schema = new Schema();
  51. /**
  52. * All callbacks added by {@link module:engine/model/model~Model#change} or
  53. * {@link module:engine/model/model~Model#enqueueChange} methods waiting to be executed.
  54. *
  55. * @private
  56. * @type {Array.<Function>}
  57. */
  58. this._pendingChanges = [];
  59. /**
  60. * The last created and currently used writer instance.
  61. *
  62. * @private
  63. * @member {module:engine/model/writer~Writer}
  64. */
  65. this._currentWriter = null;
  66. [ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation' ]
  67. .forEach( methodName => this.decorate( methodName ) );
  68. // Adding operation validation with `highest` priority, so it is called before any other feature would like
  69. // to do anything with the operation. If the operation has incorrect parameters it should throw on the earliest occasion.
  70. this.on( 'applyOperation', ( evt, args ) => {
  71. const operation = args[ 0 ];
  72. operation._validate();
  73. }, { priority: 'highest' } );
  74. // Register some default abstract entities.
  75. this.schema.register( '$root', {
  76. isLimit: true
  77. } );
  78. this.schema.register( '$block', {
  79. allowIn: '$root',
  80. isBlock: true
  81. } );
  82. this.schema.register( '$text', {
  83. allowIn: '$block'
  84. } );
  85. this.schema.register( '$clipboardHolder', {
  86. allowContentOf: '$root',
  87. isLimit: true
  88. } );
  89. this.schema.extend( '$text', { allowIn: '$clipboardHolder' } );
  90. // Element needed by `upcastElementToMarker` converter.
  91. // This element temporarily represents marker bound during conversion process and is removed
  92. // at the end of conversion. `UpcastDispatcher` or at least `Conversion` class looks like a better for this
  93. // registration but both know nothing about Schema.
  94. this.schema.register( '$marker', {
  95. allowIn: [ '$root', '$block' ]
  96. } );
  97. injectSelectionPostFixer( this );
  98. }
  99. /**
  100. * The `change()` method is the primary way of changing the model. You should use it to modify all document nodes
  101. * (including detached nodes – i.e. nodes not added to the {@link module:engine/model/model~Model#document model document}),
  102. * the {@link module:engine/model/document~Document#selection document's selection}, and
  103. * {@link module:engine/model/model~Model#markers model markers}.
  104. *
  105. * model.change( writer => {
  106. * writer.insertText( 'foo', paragraph, 'end' );
  107. * } );
  108. *
  109. * All changes inside the change block use the same {@link module:engine/model/batch~Batch} so they are combined
  110. * into a single undo step.
  111. *
  112. * model.change( writer => {
  113. * writer.insertText( 'foo', paragraph, 'end' ); // foo.
  114. *
  115. * model.change( writer => {
  116. * writer.insertText( 'bar', paragraph, 'end' ); // foobar.
  117. * } );
  118. *
  119. * writer.insertText( 'bom', paragraph, 'end' ); // foobarbom.
  120. * } );
  121. *
  122. * The callback of the `change()` block is executed synchronously.
  123. *
  124. * You can also return a value from the change block.
  125. *
  126. * const img = model.change( writer => {
  127. * return writer.createElement( 'img' );
  128. * } );
  129. *
  130. * @see #enqueueChange
  131. * @param {Function} callback Callback function which may modify the model.
  132. * @returns {*} Value returned by the callback.
  133. */
  134. change( callback ) {
  135. if ( this._pendingChanges.length === 0 ) {
  136. // If this is the outermost block, create a new batch and start `_runPendingChanges` execution flow.
  137. this._pendingChanges.push( { batch: new Batch(), callback } );
  138. return this._runPendingChanges()[ 0 ];
  139. } else {
  140. // If this is not the outermost block, just execute the callback.
  141. return callback( this._currentWriter );
  142. }
  143. }
  144. /**
  145. * The `enqueueChange()` method performs similar task as the {@link #change `change()` method}, with two major differences.
  146. *
  147. * First, the callback of `enqueueChange()` is executed when all other enqueued changes are done. It might be executed
  148. * immediately if it is not nested in any other change block, but if it is nested in another (enqueue)change block,
  149. * it will be delayed and executed after the outermost block.
  150. *
  151. * model.change( writer => {
  152. * console.log( 1 );
  153. *
  154. * model.enqueueChange( writer => {
  155. * console.log( 2 );
  156. * } );
  157. *
  158. * console.log( 3 );
  159. * } ); // Will log: 1, 3, 2.
  160. *
  161. * Second, it lets you define the {@link module:engine/model/batch~Batch} into which you want to add your changes.
  162. * By default, a new batch is created. In the sample above, `change` and `enqueueChange` blocks use a different
  163. * batch (and different {@link module:engine/model/writer~Writer} since each of them operates on the separate batch).
  164. *
  165. * When using the `enqueueChange()` block you can also add some changes to the batch you used before.
  166. *
  167. * model.enqueueChange( batch, writer => {
  168. * writer.insertText( 'foo', paragraph, 'end' );
  169. * } );
  170. *
  171. * The batch instance can be obtained from {@link module:engine/model/writer~Writer#batch the writer}.
  172. *
  173. * @param {module:engine/model/batch~Batch|'transparent'|'default'} batchOrType Batch or batch type should be used in the callback.
  174. * If not defined, a new batch will be created.
  175. * @param {Function} callback Callback function which may modify the model.
  176. */
  177. enqueueChange( batchOrType, callback ) {
  178. if ( typeof batchOrType === 'string' ) {
  179. batchOrType = new Batch( batchOrType );
  180. } else if ( typeof batchOrType == 'function' ) {
  181. callback = batchOrType;
  182. batchOrType = new Batch();
  183. }
  184. this._pendingChanges.push( { batch: batchOrType, callback } );
  185. if ( this._pendingChanges.length == 1 ) {
  186. this._runPendingChanges();
  187. }
  188. }
  189. /**
  190. * {@link module:utils/observablemixin~ObservableMixin#decorate Decorated} function to apply
  191. * {@link module:engine/model/operation/operation~Operation operations} on the model.
  192. *
  193. * @param {module:engine/model/operation/operation~Operation} operation Operation to apply
  194. */
  195. applyOperation( operation ) {
  196. operation._execute();
  197. }
  198. /**
  199. * Inserts content into the editor (specified selection) as one would expect the paste
  200. * functionality to work.
  201. *
  202. * @fires insertContent
  203. * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
  204. * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
  205. * Selection into which the content should be inserted.
  206. */
  207. insertContent( content, selection ) {
  208. insertContent( this, content, selection );
  209. }
  210. /**
  211. * Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
  212. *
  213. * **Note:** For the sake of predictability, the resulting selection should always be collapsed.
  214. * In cases where a feature wants to modify deleting behavior so selection isn't collapsed
  215. * (e.g. a table feature may want to keep row selection after pressing <kbd>Backspace</kbd>),
  216. * then that behavior should be implemented in the view's listener. At the same time, the table feature
  217. * will need to modify this method's behavior too, e.g. to "delete contents and then collapse
  218. * the selection inside the last selected cell" or "delete the row and collapse selection somewhere near".
  219. * That needs to be done in order to ensure that other features which use `deleteContent()` will work well with tables.
  220. *
  221. * @fires deleteContent
  222. * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
  223. * Selection of which the content should be deleted.
  224. * @param {module:engine/model/batch~Batch} batch Batch to which the operations will be added.
  225. * @param {Object} [options]
  226. * @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection.
  227. *
  228. * For example `<heading>x[x</heading><paragraph>y]y</paragraph>` will become:
  229. *
  230. * * `<heading>x^y</heading>` with the option disabled (`leaveUnmerged == false`)
  231. * * `<heading>x^</heading><paragraph>y</paragraph>` with enabled (`leaveUnmerged == true`).
  232. *
  233. * Note: {@link module:engine/model/schema~Schema#isObject object} and {@link module:engine/model/schema~Schema#isLimit limit}
  234. * elements will not be merged.
  235. *
  236. * @param {Boolean} [options.doNotResetEntireContent=false] Whether to skip replacing the entire content with a
  237. * paragraph when the entire content was selected.
  238. *
  239. * For example `<heading>[x</heading><paragraph>y]</paragraph>` will become:
  240. *
  241. * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
  242. * * `<heading>^</heading>` with enabled (`doNotResetEntireContent == true`)
  243. */
  244. deleteContent( selection, options ) {
  245. deleteContent( this, selection, options );
  246. }
  247. /**
  248. * Modifies the selection. Currently, the supported modifications are:
  249. *
  250. * * Extending. The selection focus is moved in the specified `options.direction` with a step specified in `options.unit`.
  251. * Possible values for `unit` are:
  252. * * `'character'` (default) - moves selection by one user-perceived character. In most cases this means moving by one
  253. * character in `String` sense. However, unicode also defines "combing marks". These are special symbols, that combines
  254. * with a symbol before it ("base character") to create one user-perceived character. For example, `q̣̇` is a normal
  255. * letter `q` with two "combining marks": upper dot (`Ux0307`) and lower dot (`Ux0323`). For most actions, i.e. extending
  256. * selection by one position, it is correct to include both "base character" and all of it's "combining marks". That is
  257. * why `'character'` value is most natural and common method of modifying selection.
  258. * * `'codePoint'` - moves selection by one unicode code point. In contrary to, `'character'` unit, this will insert
  259. * selection between "base character" and "combining mark", because "combining marks" have their own unicode code points.
  260. * However, for technical reasons, unicode code points with values above `UxFFFF` are represented in native `String` by
  261. * two characters, called "surrogate pairs". Halves of "surrogate pairs" have a meaning only when placed next to each other.
  262. * For example `𨭎` is represented in `String` by `\uD862\uDF4E`. Both `\uD862` and `\uDF4E` do not have any meaning
  263. * outside the pair (are rendered as ? when alone). Position between them would be incorrect. In this case, selection
  264. * extension will include whole "surrogate pair".
  265. * * `'word'` - moves selection by a whole word.
  266. *
  267. * **Note:** if you extend a forward selection in a backward direction you will in fact shrink it.
  268. *
  269. * @fires modifySelection
  270. * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
  271. * The selection to modify.
  272. * @param {Object} [options]
  273. * @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
  274. * @param {'character'|'codePoint'|'word'} [options.unit='character'] The unit by which selection should be modified.
  275. */
  276. modifySelection( selection, options ) {
  277. modifySelection( this, selection, options );
  278. }
  279. /**
  280. * Gets a clone of the selected content.
  281. *
  282. * For example, for the following selection:
  283. *
  284. * ```html
  285. * <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
  286. * ```
  287. *
  288. * It will return a document fragment with such a content:
  289. *
  290. * ```html
  291. * <quote><h>st</h></quote><p>se</p>
  292. * ```
  293. *
  294. * @fires getSelectedContent
  295. * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
  296. * The selection of which content will be returned.
  297. * @returns {module:engine/model/documentfragment~DocumentFragment}
  298. */
  299. getSelectedContent( selection ) {
  300. return getSelectedContent( this, selection );
  301. }
  302. /**
  303. * Checks whether given {@link module:engine/model/range~Range range} or {@link module:engine/model/element~Element element}
  304. * has any content.
  305. *
  306. * Content is any text node or element which is registered in {@link module:engine/model/schema~Schema schema}.
  307. *
  308. * @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
  309. * @returns {Boolean}
  310. */
  311. hasContent( rangeOrElement ) {
  312. if ( rangeOrElement instanceof ModelElement ) {
  313. rangeOrElement = ModelRange.createIn( rangeOrElement );
  314. }
  315. if ( rangeOrElement.isCollapsed ) {
  316. return false;
  317. }
  318. for ( const item of rangeOrElement.getItems() ) {
  319. // Remember, `TreeWalker` returns always `textProxy` nodes.
  320. if ( item.is( 'textProxy' ) || this.schema.isObject( item ) ) {
  321. return true;
  322. }
  323. }
  324. return false;
  325. }
  326. /**
  327. * Removes all events listeners set by model instance and destroys {@link module:engine/model/document~Document}.
  328. */
  329. destroy() {
  330. this.document.destroy();
  331. this.stopListening();
  332. }
  333. /**
  334. * Common part of {@link module:engine/model/model~Model#change} and {@link module:engine/model/model~Model#enqueueChange}
  335. * which calls callbacks and returns array of values returned by these callbacks.
  336. *
  337. * @private
  338. * @returns {Array.<*>} Array of values returned by callbacks.
  339. */
  340. _runPendingChanges() {
  341. const ret = [];
  342. this.fire( '_beforeChanges' );
  343. while ( this._pendingChanges.length ) {
  344. // Create a new writer using batch instance created for this chain of changes.
  345. const currentBatch = this._pendingChanges[ 0 ].batch;
  346. this._currentWriter = new Writer( this, currentBatch );
  347. // Execute changes callback and gather the returned value.
  348. const callbackReturnValue = this._pendingChanges[ 0 ].callback( this._currentWriter );
  349. ret.push( callbackReturnValue );
  350. // Fire internal `_change` event.
  351. this.fire( '_change', this._currentWriter );
  352. this._pendingChanges.shift();
  353. this._currentWriter = null;
  354. }
  355. this.fire( '_afterChanges' );
  356. return ret;
  357. }
  358. /**
  359. * Fired after leaving each {@link module:engine/model/model~Model#enqueueChange} block or outermost
  360. * {@link module:engine/model/model~Model#change} block.
  361. *
  362. * **Note:** This is an internal event! Use {@link module:engine/model/document~Document#event:change} instead.
  363. *
  364. * @protected
  365. * @event _change
  366. * @param {module:engine/model/writer~Writer} writer `Writer` instance that has been used in the change block.
  367. */
  368. /**
  369. * Fired when enter the first {@link module:engine/model/model~Model#enqueueChange} or
  370. * {@link module:engine/model/model~Model#change} block of the pending changes.
  371. *
  372. * @protected
  373. * @event _beforeChanges
  374. */
  375. /**
  376. * Fired when leave the last {@link module:engine/model/model~Model#enqueueChange} or
  377. * {@link module:engine/model/model~Model#change} block of the pending changes.
  378. *
  379. * @protected
  380. * @event _afterChanges
  381. */
  382. /**
  383. * Fired every time any {@link module:engine/model/operation/operation~Operation operation} is applied on the model
  384. * using {@link #applyOperation}.
  385. *
  386. * Note that this event is suitable only for very specific use-cases. Use it if you need to listen to every single operation
  387. * applied on the document. However, in most cases {@link module:engine/model/document~Document#event:change} should
  388. * be used.
  389. *
  390. * A few callbacks are already added to this event by engine internal classes:
  391. *
  392. * * with `highest` priority operation is validated,
  393. * * with `normal` priority operation is executed,
  394. * * with `low` priority the {@link module:engine/model/document~Document} updates its version,
  395. * * with `low` priority {@link module:engine/model/liveposition~LivePosition} and {@link module:engine/model/liverange~LiveRange}
  396. * update themselves.
  397. *
  398. * @event applyOperation
  399. * @param {Array} args Arguments of the `applyOperation` which is an array with a single element - applied
  400. * {@link module:engine/model/operation/operation~Operation operation}.
  401. */
  402. /**
  403. * Event fired when {@link #insertContent} method is called.
  404. *
  405. * The {@link #insertContent default action of that method} is implemented as a
  406. * listener to this event so it can be fully customized by the features.
  407. *
  408. * @event insertContent
  409. * @param {Array} args The arguments passed to the original method.
  410. */
  411. /**
  412. * Event fired when {@link #deleteContent} method is called.
  413. *
  414. * The {@link #deleteContent default action of that method} is implemented as a
  415. * listener to this event so it can be fully customized by the features.
  416. *
  417. * @event deleteContent
  418. * @param {Array} args The arguments passed to the original method.
  419. */
  420. /**
  421. * Event fired when {@link #modifySelection} method is called.
  422. *
  423. * The {@link #modifySelection default action of that method} is implemented as a
  424. * listener to this event so it can be fully customized by the features.
  425. *
  426. * @event modifySelection
  427. * @param {Array} args The arguments passed to the original method.
  428. */
  429. /**
  430. * Event fired when {@link #getSelectedContent} method is called.
  431. *
  432. * The {@link #getSelectedContent default action of that method} is implemented as a
  433. * listener to this event so it can be fully customized by the features.
  434. *
  435. * @event getSelectedContent
  436. * @param {Array} args The arguments passed to the original method.
  437. */
  438. }
  439. mix( Model, ObservableMixin );