model.js 38 KB

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