8
0

model.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  1. /**
  2. * @license Copyright (c) 2003-2019, 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 ModelPosition from './position';
  18. import ModelSelection from './selection';
  19. import insertContent from './utils/insertcontent';
  20. import deleteContent from './utils/deletecontent';
  21. import modifySelection from './utils/modifyselection';
  22. import getSelectedContent from './utils/getselectedcontent';
  23. import { injectSelectionPostFixer } from './utils/selection-post-fixer';
  24. /**
  25. * Editor's data model. Read about the model in the
  26. * {@glink framework/guides/architecture/editing-engine engine architecture guide}.
  27. *
  28. * @mixes module:utils/observablemixin~ObservableMixin
  29. */
  30. export default class Model {
  31. constructor() {
  32. /**
  33. * Model's marker collection.
  34. *
  35. * @readonly
  36. * @member {module:engine/model/markercollection~MarkerCollection}
  37. */
  38. this.markers = new MarkerCollection();
  39. /**
  40. * Model's document.
  41. *
  42. * @readonly
  43. * @member {module:engine/model/document~Document}
  44. */
  45. this.document = new Document( this );
  46. /**
  47. * Model's schema.
  48. *
  49. * @readonly
  50. * @member {module:engine/model/schema~Schema}
  51. */
  52. this.schema = new Schema();
  53. /**
  54. * All callbacks added by {@link module:engine/model/model~Model#change} or
  55. * {@link module:engine/model/model~Model#enqueueChange} methods waiting to be executed.
  56. *
  57. * @private
  58. * @type {Array.<Function>}
  59. */
  60. this._pendingChanges = [];
  61. /**
  62. * The last created and currently used writer instance.
  63. *
  64. * @private
  65. * @member {module:engine/model/writer~Writer}
  66. */
  67. this._currentWriter = null;
  68. [ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation' ]
  69. .forEach( methodName => this.decorate( methodName ) );
  70. // Adding operation validation with `highest` priority, so it is called before any other feature would like
  71. // to do anything with the operation. If the operation has incorrect parameters it should throw on the earliest occasion.
  72. this.on( 'applyOperation', ( evt, args ) => {
  73. const operation = args[ 0 ];
  74. operation._validate();
  75. }, { priority: 'highest' } );
  76. // Register some default abstract entities.
  77. this.schema.register( '$root', {
  78. isLimit: true
  79. } );
  80. this.schema.register( '$block', {
  81. allowIn: '$root',
  82. isBlock: true
  83. } );
  84. this.schema.register( '$text', {
  85. allowIn: '$block'
  86. } );
  87. this.schema.register( '$clipboardHolder', {
  88. allowContentOf: '$root',
  89. isLimit: true
  90. } );
  91. this.schema.extend( '$text', { allowIn: '$clipboardHolder' } );
  92. // Element needed by `upcastElementToMarker` converter.
  93. // This element temporarily represents marker bound during conversion process and is removed
  94. // at the end of conversion. `UpcastDispatcher` or at least `Conversion` class looks like a better for this
  95. // registration but both know nothing about Schema.
  96. this.schema.register( '$marker', {
  97. allowIn: [ '$root', '$block' ]
  98. } );
  99. injectSelectionPostFixer( this );
  100. }
  101. /**
  102. * The `change()` method is the primary way of changing the model. You should use it to modify all document nodes
  103. * (including detached nodes – i.e. nodes not added to the {@link module:engine/model/model~Model#document model document}),
  104. * the {@link module:engine/model/document~Document#selection document's selection}, and
  105. * {@link module:engine/model/model~Model#markers model markers}.
  106. *
  107. * model.change( writer => {
  108. * writer.insertText( 'foo', paragraph, 'end' );
  109. * } );
  110. *
  111. * All changes inside the change block use the same {@link module:engine/model/batch~Batch} so they are combined
  112. * into a single undo step.
  113. *
  114. * model.change( writer => {
  115. * writer.insertText( 'foo', paragraph, 'end' ); // foo.
  116. *
  117. * model.change( writer => {
  118. * writer.insertText( 'bar', paragraph, 'end' ); // foobar.
  119. * } );
  120. *
  121. * writer.insertText( 'bom', paragraph, 'end' ); // foobarbom.
  122. * } );
  123. *
  124. * The callback of the `change()` block is executed synchronously.
  125. *
  126. * You can also return a value from the change block.
  127. *
  128. * const img = model.change( writer => {
  129. * return writer.createElement( 'img' );
  130. * } );
  131. *
  132. * @see #enqueueChange
  133. * @param {Function} callback Callback function which may modify the model.
  134. * @returns {*} Value returned by the callback.
  135. */
  136. change( callback ) {
  137. if ( this._pendingChanges.length === 0 ) {
  138. // If this is the outermost block, create a new batch and start `_runPendingChanges` execution flow.
  139. this._pendingChanges.push( { batch: new Batch(), callback } );
  140. return this._runPendingChanges()[ 0 ];
  141. } else {
  142. // If this is not the outermost block, just execute the callback.
  143. return callback( this._currentWriter );
  144. }
  145. }
  146. /**
  147. * The `enqueueChange()` method performs similar task as the {@link #change `change()` method}, with two major differences.
  148. *
  149. * First, the callback of `enqueueChange()` is executed when all other enqueued changes are done. It might be executed
  150. * immediately if it is not nested in any other change block, but if it is nested in another (enqueue)change block,
  151. * it will be delayed and executed after the outermost block.
  152. *
  153. * model.change( writer => {
  154. * console.log( 1 );
  155. *
  156. * model.enqueueChange( writer => {
  157. * console.log( 2 );
  158. * } );
  159. *
  160. * console.log( 3 );
  161. * } ); // Will log: 1, 3, 2.
  162. *
  163. * Second, it lets you define the {@link module:engine/model/batch~Batch} into which you want to add your changes.
  164. * By default, a new batch is created. In the sample above, `change` and `enqueueChange` blocks use a different
  165. * batch (and different {@link module:engine/model/writer~Writer} since each of them operates on the separate batch).
  166. *
  167. * When using the `enqueueChange()` block you can also add some changes to the batch you used before.
  168. *
  169. * model.enqueueChange( batch, writer => {
  170. * writer.insertText( 'foo', paragraph, 'end' );
  171. * } );
  172. *
  173. * The batch instance can be obtained from {@link module:engine/model/writer~Writer#batch the writer}.
  174. *
  175. * @param {module:engine/model/batch~Batch|'transparent'|'default'} batchOrType Batch or batch type should be used in the callback.
  176. * If not defined, a new batch will be created.
  177. * @param {Function} callback Callback function which may modify the model.
  178. */
  179. enqueueChange( batchOrType, callback ) {
  180. if ( typeof batchOrType === 'string' ) {
  181. batchOrType = new Batch( batchOrType );
  182. } else if ( typeof batchOrType == 'function' ) {
  183. callback = batchOrType;
  184. batchOrType = new Batch();
  185. }
  186. this._pendingChanges.push( { batch: batchOrType, callback } );
  187. if ( this._pendingChanges.length == 1 ) {
  188. this._runPendingChanges();
  189. }
  190. }
  191. /**
  192. * {@link module:utils/observablemixin~ObservableMixin#decorate Decorated} function for applying
  193. * {@link module:engine/model/operation/operation~Operation operations} to the model.
  194. *
  195. * This is a low-level way of changing the model. It is exposed for very specific use cases (like the undo feature).
  196. * Normally, to modify the model, you will want to use {@link module:engine/model/writer~Writer `Writer`}.
  197. * See also {@glink framework/guides/architecture/editing-engine#changing-the-model Changing the model} section
  198. * of the {@glink framework/guides/architecture/editing-engine Editing architecture} guide.
  199. *
  200. * @param {module:engine/model/operation/operation~Operation} operation The operation to apply.
  201. */
  202. applyOperation( operation ) {
  203. operation._execute();
  204. }
  205. /**
  206. * Inserts content at the position in the editor specified by the selection, as one would expect the paste
  207. * functionality to work.
  208. *
  209. * This is a high-level method. It takes the {@link #schema schema} into consideration when inserting
  210. * the content, clears the given selection's content before inserting nodes and moves the selection
  211. * to its target position at the end of the process.
  212. * It can split elements, merge them, wrap bare text nodes with paragraphs, etc. &mdash; just like the
  213. * pasting feature should do.
  214. *
  215. * For lower-level methods see {@link module:engine/model/writer~Writer `Writer`}.
  216. *
  217. * This method, unlike {@link module:engine/model/writer~Writer `Writer`}'s methods, does not have to be used
  218. * inside a {@link #change `change()` block}.
  219. *
  220. * # Conversion and schema
  221. *
  222. * Inserting elements and text nodes into the model is not enough to make CKEditor 5 render that content
  223. * to the user. CKEditor 5 implements a model-view-controller architecture and what `model.insertContent()` does
  224. * is only adding nodes to the model. Additionally, you need to define
  225. * {@glink framework/guides/architecture/editing-engine#conversion converters} between the model and view
  226. * and define those nodes in the {@glink framework/guides/architecture/editing-engine#schema schema}.
  227. *
  228. * So, while this method may seem similar to CKEditor 4 `editor.insertHtml()` (in fact, both methods
  229. * are used for paste-like content insertion), the CKEditor 5 method cannot be use to insert arbitrary HTML
  230. * unless converters are defined for all elements and attributes in that HTML.
  231. *
  232. * # Examples
  233. *
  234. * Using `insertContent()` with a manually created model structure:
  235. *
  236. * // Let's create a document fragment containing such content as:
  237. * //
  238. * // <paragrap>foo</paragraph>
  239. * // <blockQuote>
  240. * // <paragraph>bar</paragraph>
  241. * // </blockQuote>
  242. * const docFrag = editor.model.change( writer => {
  243. * const p1 = writer.createElement( 'paragraph' );
  244. * const p2 = writer.createElement( 'paragraph' );
  245. * const blockQuote = writer.createElement( 'blockQuote' );
  246. * const docFrag = writer.createDocumentFragment();
  247. *
  248. * writer.append( p1, docFrag );
  249. * writer.append( blockQuote, docFrag );
  250. * writer.append( p2, blockQuote );
  251. * writer.insertText( 'foo', p1 );
  252. * writer.insertText( 'bar', p2 );
  253. *
  254. * return docFrag;
  255. * } );
  256. *
  257. * // insertContent() does not have to be used in a change() block. It can, though,
  258. * // so this code could be moved to the callback defined above.
  259. * editor.model.insertContent( docFrag );
  260. *
  261. * Using `insertContent()` with an HTML string converted to a model document fragment (similar to the pasting mechanism):
  262. *
  263. * // You can create your own HtmlDataProcessor instance or use editor.data.processor
  264. * // if you have not overridden the default one (which is the HtmlDataProcessor instance).
  265. * const htmlDP = new HtmlDataProcessor();
  266. *
  267. * // Convert an HTML string to a view document fragment:
  268. * const viewFragment = htmlDP.toView( htmlString );
  269. *
  270. * // Convert the view document fragment to a model document fragment
  271. * // in the context of $root. This conversion takes the schema into
  272. * // account so if, for example, the view document fragment contained a bare text node,
  273. * // this text node cannot be a child of $root, so it will be automatically
  274. * // wrapped with a <paragraph>. You can define the context yourself (in the second parameter),
  275. * // and e.g. convert the content like it would happen in a <paragraph>.
  276. * // Note: The clipboard feature uses a custom context called $clipboardHolder
  277. * // which has a loosened schema.
  278. * const modelFragment = editor.data.toModel( viewFragment );
  279. *
  280. * editor.model.insertContent( modelFragment );
  281. *
  282. * By default this method will use the document selection but it can also be used with a position, range or selection instance.
  283. *
  284. * // Insert text at the current document selection position.
  285. * editor.model.change( writer => {
  286. * editor.model.insertContent( writer.createText( 'x' ) );
  287. * } );
  288. *
  289. * // Insert text at a given position - the document selection will not be modified.
  290. * editor.model.change( writer => {
  291. * editor.model.insertContent( writer.createText( 'x' ), doc.getRoot(), 2 );
  292. *
  293. * // Which is a shorthand for:
  294. * editor.model.insertContent( writer.createText( 'x' ), writer.createPositionAt( doc.getRoot(), 2 ) );
  295. * } );
  296. *
  297. * If an instance of {@link module:engine/model/selection~Selection} is passed as `selectable`
  298. * it will be moved to the target position (where the document selection should be moved after the insertion).
  299. *
  300. * editor.model.change( writer => {
  301. * // Insert text replacing the given selection instance.
  302. * const selection = writer.createSelection( paragraph, 'in' );
  303. *
  304. * editor.model.insertContent( writer.createText( 'x' ), selection );
  305. *
  306. * // insertContent() modifies the passed selection instance so it can be used to set the document selection.
  307. * // Note: This is not necessary when you passed the document selection to insertContent().
  308. * writer.setSelection( selection );
  309. * } );
  310. *
  311. * @fires insertContent
  312. * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
  313. * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection]
  314. * The selection into which the content should be inserted. If not provided the current model document selection will be used.
  315. * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] To be used when a model item was passed as `selectable`.
  316. * This param defines a position in relation to that item.
  317. */
  318. insertContent( content, selectable, placeOrOffset ) {
  319. insertContent( this, content, selectable, placeOrOffset );
  320. }
  321. /**
  322. * Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
  323. *
  324. * **Note:** For the sake of predictability, the resulting selection should always be collapsed.
  325. * In cases where a feature wants to modify deleting behavior so selection isn't collapsed
  326. * (e.g. a table feature may want to keep row selection after pressing <kbd>Backspace</kbd>),
  327. * then that behavior should be implemented in the view's listener. At the same time, the table feature
  328. * will need to modify this method's behavior too, e.g. to "delete contents and then collapse
  329. * the selection inside the last selected cell" or "delete the row and collapse selection somewhere near".
  330. * That needs to be done in order to ensure that other features which use `deleteContent()` will work well with tables.
  331. *
  332. * @fires deleteContent
  333. * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
  334. * Selection of which the content should be deleted.
  335. * @param {Object} [options]
  336. * @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection.
  337. *
  338. * For example `<heading1>x[x</heading1><paragraph>y]y</paragraph>` will become:
  339. *
  340. * * `<heading1>x^y</heading1>` with the option disabled (`leaveUnmerged == false`)
  341. * * `<heading1>x^</heading1><paragraph>y</paragraph>` with enabled (`leaveUnmerged == true`).
  342. *
  343. * Note: {@link module:engine/model/schema~Schema#isObject object} and {@link module:engine/model/schema~Schema#isLimit limit}
  344. * elements will not be merged.
  345. *
  346. * @param {Boolean} [options.doNotResetEntireContent=false] Whether to skip replacing the entire content with a
  347. * paragraph when the entire content was selected.
  348. *
  349. * For example `<heading1>[x</heading1><paragraph>y]</paragraph>` will become:
  350. *
  351. * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
  352. * * `<heading1>^</heading1>` with enabled (`doNotResetEntireContent == true`)
  353. */
  354. deleteContent( selection, options ) {
  355. deleteContent( this, selection, options );
  356. }
  357. /**
  358. * Modifies the selection. Currently, the supported modifications are:
  359. *
  360. * * Extending. The selection focus is moved in the specified `options.direction` with a step specified in `options.unit`.
  361. * Possible values for `unit` are:
  362. * * `'character'` (default) - moves selection by one user-perceived character. In most cases this means moving by one
  363. * character in `String` sense. However, unicode also defines "combing marks". These are special symbols, that combines
  364. * with a symbol before it ("base character") to create one user-perceived character. For example, `q̣̇` is a normal
  365. * letter `q` with two "combining marks": upper dot (`Ux0307`) and lower dot (`Ux0323`). For most actions, i.e. extending
  366. * selection by one position, it is correct to include both "base character" and all of it's "combining marks". That is
  367. * why `'character'` value is most natural and common method of modifying selection.
  368. * * `'codePoint'` - moves selection by one unicode code point. In contrary to, `'character'` unit, this will insert
  369. * selection between "base character" and "combining mark", because "combining marks" have their own unicode code points.
  370. * However, for technical reasons, unicode code points with values above `UxFFFF` are represented in native `String` by
  371. * two characters, called "surrogate pairs". Halves of "surrogate pairs" have a meaning only when placed next to each other.
  372. * For example `𨭎` is represented in `String` by `\uD862\uDF4E`. Both `\uD862` and `\uDF4E` do not have any meaning
  373. * outside the pair (are rendered as ? when alone). Position between them would be incorrect. In this case, selection
  374. * extension will include whole "surrogate pair".
  375. * * `'word'` - moves selection by a whole word.
  376. *
  377. * **Note:** if you extend a forward selection in a backward direction you will in fact shrink it.
  378. *
  379. * @fires modifySelection
  380. * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
  381. * The selection to modify.
  382. * @param {Object} [options]
  383. * @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
  384. * @param {'character'|'codePoint'|'word'} [options.unit='character'] The unit by which selection should be modified.
  385. */
  386. modifySelection( selection, options ) {
  387. modifySelection( this, selection, options );
  388. }
  389. /**
  390. * Gets a clone of the selected content.
  391. *
  392. * For example, for the following selection:
  393. *
  394. * ```html
  395. * <paragraph>x</paragraph>
  396. * <blockQuote>
  397. * <paragraph>y</paragraph>
  398. * <heading1>fir[st</heading1>
  399. * </blockQuote>
  400. * <paragraph>se]cond</paragraph>
  401. * <paragraph>z</paragraph>
  402. * ```
  403. *
  404. * It will return a document fragment with such a content:
  405. *
  406. * ```html
  407. * <blockQuote>
  408. * <heading1>st</heading1>
  409. * </blockQuote>
  410. * <paragraph>se</paragraph>
  411. * ```
  412. *
  413. * @fires getSelectedContent
  414. * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
  415. * The selection of which content will be returned.
  416. * @returns {module:engine/model/documentfragment~DocumentFragment}
  417. */
  418. getSelectedContent( selection ) {
  419. return getSelectedContent( this, selection );
  420. }
  421. /**
  422. * Checks whether the given {@link module:engine/model/range~Range range} or
  423. * {@link module:engine/model/element~Element element}
  424. * has any content.
  425. *
  426. * Content is any text node or element which is registered in the {@link module:engine/model/schema~Schema schema}.
  427. *
  428. * @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
  429. * @returns {Boolean}
  430. */
  431. hasContent( rangeOrElement ) {
  432. if ( rangeOrElement instanceof ModelElement ) {
  433. rangeOrElement = ModelRange._createIn( rangeOrElement );
  434. }
  435. if ( rangeOrElement.isCollapsed ) {
  436. return false;
  437. }
  438. for ( const item of rangeOrElement.getItems() ) {
  439. // Remember, `TreeWalker` returns always `textProxy` nodes.
  440. if ( item.is( 'textProxy' ) || this.schema.isObject( item ) ) {
  441. return true;
  442. }
  443. }
  444. return false;
  445. }
  446. /**
  447. * Creates a position from the given root and path in that root.
  448. *
  449. * Note: This method is also available as
  450. * {@link module:engine/model/writer~Writer#createPositionFromPath `Writer#createPositionFromPath()`}.
  451. *
  452. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} root Root of the position.
  453. * @param {Array.<Number>} path Position path. See {@link module:engine/model/position~Position#path}.
  454. * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
  455. * See {@link module:engine/model/position~PositionStickiness}.
  456. * @returns {module:engine/model/position~Position}
  457. */
  458. createPositionFromPath( root, path, stickiness ) {
  459. return new ModelPosition( root, path, stickiness );
  460. }
  461. /**
  462. * Creates position at the given location. The location can be specified as:
  463. *
  464. * * a {@link module:engine/model/position~Position position},
  465. * * a parent element and offset in that element,
  466. * * a parent element and `'end'` (the position will be set at the end of that element),
  467. * * a {@link module:engine/model/item~Item model item} and `'before'` or `'after'`
  468. * (the position will be set before or after the given model item).
  469. *
  470. * This method is a shortcut to other factory methods such as:
  471. *
  472. * * {@link module:engine/model/model~Model#createPositionBefore `createPositionBefore()`},
  473. * * {@link module:engine/model/model~Model#createPositionAfter `createPositionAfter()`}.
  474. *
  475. * Note: This method is also available as
  476. * {@link module:engine/model/writer~Writer#createPositionAt `Writer#createPositionAt()`},
  477. *
  478. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  479. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  480. * first parameter is a {@link module:engine/model/item~Item model item}.
  481. */
  482. createPositionAt( itemOrPosition, offset ) {
  483. return ModelPosition._createAt( itemOrPosition, offset );
  484. }
  485. /**
  486. * Creates a new position after the given {@link module:engine/model/item~Item model item}.
  487. *
  488. * Note: This method is also available as
  489. * {@link module:engine/model/writer~Writer#createPositionAfter `Writer#createPositionAfter()`}.
  490. *
  491. * @param {module:engine/model/item~Item} item Item after which the position should be placed.
  492. * @returns {module:engine/model/position~Position}
  493. */
  494. createPositionAfter( item ) {
  495. return ModelPosition._createAfter( item );
  496. }
  497. /**
  498. * Creates a new position before the given {@link module:engine/model/item~Item model item}.
  499. *
  500. * Note: This method is also available as
  501. * {@link module:engine/model/writer~Writer#createPositionBefore `Writer#createPositionBefore()`}.
  502. *
  503. * @param {module:engine/model/item~Item} item Item before which the position should be placed.
  504. * @returns {module:engine/model/position~Position}
  505. */
  506. createPositionBefore( item ) {
  507. return ModelPosition._createBefore( item );
  508. }
  509. /**
  510. * Creates a range spanning from the `start` position to the `end` position.
  511. *
  512. * Note: This method is also available as
  513. * {@link module:engine/model/writer~Writer#createRange `Writer#createRange()`}:
  514. *
  515. * model.change( writer => {
  516. * const range = writer.createRange( start, end );
  517. * } );
  518. *
  519. * @param {module:engine/model/position~Position} start Start position.
  520. * @param {module:engine/model/position~Position} [end] End position. If not set, the range will be collapsed
  521. * to the `start` position.
  522. * @returns {module:engine/model/range~Range}
  523. */
  524. createRange( start, end ) {
  525. return new ModelRange( start, end );
  526. }
  527. /**
  528. * Creates a range inside the given element which starts before the first child of
  529. * that element and ends after the last child of that element.
  530. *
  531. * Note: This method is also available as
  532. * {@link module:engine/model/writer~Writer#createRangeIn `Writer#createRangeIn()`}:
  533. *
  534. * model.change( writer => {
  535. * const range = writer.createRangeIn( paragraph );
  536. * } );
  537. *
  538. * @param {module:engine/model/element~Element} element Element which is a parent for the range.
  539. * @returns {module:engine/model/range~Range}
  540. */
  541. createRangeIn( element ) {
  542. return ModelRange._createIn( element );
  543. }
  544. /**
  545. * Creates a range that starts before the given {@link module:engine/model/item~Item model item} and ends after it.
  546. *
  547. * Note: This method is also available on `writer` instance as
  548. * {@link module:engine/model/writer~Writer#createRangeOn `Writer.createRangeOn()`}:
  549. *
  550. * model.change( writer => {
  551. * const range = writer.createRangeOn( paragraph );
  552. * } );
  553. *
  554. * @param {module:engine/model/item~Item} item
  555. * @returns {module:engine/model/range~Range}
  556. */
  557. createRangeOn( item ) {
  558. return ModelRange._createOn( item );
  559. }
  560. /**
  561. * Creates a new selection instance based on the given {@link module:engine/model/selection~Selectable selectable}
  562. * or creates an empty selection if no arguments were passed.
  563. *
  564. * Note: This method is also available as
  565. * {@link module:engine/model/writer~Writer#createSelection `Writer#createSelection()`}.
  566. *
  567. * // Creates empty selection without ranges.
  568. * const selection = writer.createSelection();
  569. *
  570. * // Creates selection at the given range.
  571. * const range = writer.createRange( start, end );
  572. * const selection = writer.createSelection( range );
  573. *
  574. * // Creates selection at the given ranges
  575. * const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
  576. * const selection = writer.createSelection( ranges );
  577. *
  578. * // Creates selection from the other selection.
  579. * // Note: It doesn't copies selection attributes.
  580. * const otherSelection = writer.createSelection();
  581. * const selection = writer.createSelection( otherSelection );
  582. *
  583. * // Creates selection from the given document selection.
  584. * // Note: It doesn't copies selection attributes.
  585. * const documentSelection = model.document.selection;
  586. * const selection = writer.createSelection( documentSelection );
  587. *
  588. * // Creates selection at the given position.
  589. * const position = writer.createPositionFromPath( root, path );
  590. * const selection = writer.createSelection( position );
  591. *
  592. * // Creates selection at the given offset in the given element.
  593. * const paragraph = writer.createElement( 'paragraph' );
  594. * const selection = writer.createSelection( paragraph, offset );
  595. *
  596. * // Creates a range inside an {@link module:engine/model/element~Element element} which starts before the
  597. * // first child of that element and ends after the last child of that element.
  598. * const selection = writer.createSelection( paragraph, 'in' );
  599. *
  600. * // Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends
  601. * // just after the item.
  602. * const selection = writer.createSelection( paragraph, 'on' );
  603. *
  604. * // Additional options (`'backward'`) can be specified as the last argument.
  605. *
  606. * // Creates backward selection.
  607. * const selection = writer.createSelection( range, { backward: true } );
  608. *
  609. * @param {module:engine/model/selection~Selectable} selectable
  610. * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
  611. * @param {Object} [options]
  612. * @param {Boolean} [options.backward] Sets this selection instance to be backward.
  613. * @returns {module:engine/model/selection~Selection}
  614. */
  615. createSelection( selectable, placeOrOffset, options ) {
  616. return new ModelSelection( selectable, placeOrOffset, options );
  617. }
  618. /**
  619. * Creates a {@link module:engine/model/batch~Batch} instance.
  620. *
  621. * **Note:** In most cases creating a batch instance is not necessary as they are created when using:
  622. *
  623. * * {@link #change `change()`},
  624. * * {@link #enqueueChange `enqueueChange()`}.
  625. *
  626. * @returns {module:engine/model/batch~Batch}
  627. */
  628. createBatch() {
  629. return new Batch();
  630. }
  631. /**
  632. * Removes all events listeners set by model instance and destroys {@link module:engine/model/document~Document}.
  633. */
  634. destroy() {
  635. this.document.destroy();
  636. this.stopListening();
  637. }
  638. /**
  639. * Common part of {@link module:engine/model/model~Model#change} and {@link module:engine/model/model~Model#enqueueChange}
  640. * which calls callbacks and returns array of values returned by these callbacks.
  641. *
  642. * @private
  643. * @returns {Array.<*>} Array of values returned by callbacks.
  644. */
  645. _runPendingChanges() {
  646. const ret = [];
  647. this.fire( '_beforeChanges' );
  648. while ( this._pendingChanges.length ) {
  649. // Create a new writer using batch instance created for this chain of changes.
  650. const currentBatch = this._pendingChanges[ 0 ].batch;
  651. this._currentWriter = new Writer( this, currentBatch );
  652. // Execute changes callback and gather the returned value.
  653. const callbackReturnValue = this._pendingChanges[ 0 ].callback( this._currentWriter );
  654. ret.push( callbackReturnValue );
  655. // Fire '_change' event before resetting differ.
  656. this.fire( '_change', this._currentWriter );
  657. this.document._handleChangeBlock( this._currentWriter );
  658. this._pendingChanges.shift();
  659. this._currentWriter = null;
  660. }
  661. this.fire( '_afterChanges' );
  662. return ret;
  663. }
  664. /**
  665. * Fired after leaving each {@link module:engine/model/model~Model#enqueueChange} block or outermost
  666. * {@link module:engine/model/model~Model#change} block.
  667. *
  668. * **Note:** This is an internal event! Use {@link module:engine/model/document~Document#event:change} instead.
  669. *
  670. * @deprecated
  671. * @protected
  672. * @event _change
  673. * @param {module:engine/model/writer~Writer} writer `Writer` instance that has been used in the change block.
  674. */
  675. /**
  676. * Fired when entering the outermost {@link module:engine/model/model~Model#enqueueChange} or
  677. * {@link module:engine/model/model~Model#change} block.
  678. *
  679. * @protected
  680. * @event _beforeChanges
  681. */
  682. /**
  683. * Fired when leaving the outermost {@link module:engine/model/model~Model#enqueueChange} or
  684. * {@link module:engine/model/model~Model#change} block.
  685. *
  686. * @protected
  687. * @event _afterChanges
  688. */
  689. /**
  690. * Fired every time any {@link module:engine/model/operation/operation~Operation operation} is applied on the model
  691. * using {@link #applyOperation}.
  692. *
  693. * Note that this event is suitable only for very specific use-cases. Use it if you need to listen to every single operation
  694. * applied on the document. However, in most cases {@link module:engine/model/document~Document#event:change} should
  695. * be used.
  696. *
  697. * A few callbacks are already added to this event by engine internal classes:
  698. *
  699. * * with `highest` priority operation is validated,
  700. * * with `normal` priority operation is executed,
  701. * * with `low` priority the {@link module:engine/model/document~Document} updates its version,
  702. * * with `low` priority {@link module:engine/model/liveposition~LivePosition} and {@link module:engine/model/liverange~LiveRange}
  703. * update themselves.
  704. *
  705. * @event applyOperation
  706. * @param {Array} args Arguments of the `applyOperation` which is an array with a single element - applied
  707. * {@link module:engine/model/operation/operation~Operation operation}.
  708. */
  709. /**
  710. * Event fired when {@link #insertContent} method is called.
  711. *
  712. * The {@link #insertContent default action of that method} is implemented as a
  713. * listener to this event so it can be fully customized by the features.
  714. *
  715. * **Note** The `selectable` parameter for the {@link #insertContent} is optional. When `undefined` value is passed the method uses
  716. * `model.document.selection`.
  717. *
  718. * @event insertContent
  719. * @param {Array} args The arguments passed to the original method.
  720. */
  721. /**
  722. * Event fired when {@link #deleteContent} method is called.
  723. *
  724. * The {@link #deleteContent default action of that method} is implemented as a
  725. * listener to this event so it can be fully customized by the features.
  726. *
  727. * @event deleteContent
  728. * @param {Array} args The arguments passed to the original method.
  729. */
  730. /**
  731. * Event fired when {@link #modifySelection} method is called.
  732. *
  733. * The {@link #modifySelection default action of that method} is implemented as a
  734. * listener to this event so it can be fully customized by the features.
  735. *
  736. * @event modifySelection
  737. * @param {Array} args The arguments passed to the original method.
  738. */
  739. /**
  740. * Event fired when {@link #getSelectedContent} method is called.
  741. *
  742. * The {@link #getSelectedContent default action of that method} is implemented as a
  743. * listener to this event so it can be fully customized by the features.
  744. *
  745. * @event getSelectedContent
  746. * @param {Array} args The arguments passed to the original method.
  747. */
  748. }
  749. mix( Model, ObservableMixin );