| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602 |
- /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module engine/model/model
- */
- import Batch from './batch';
- import Writer from './writer';
- import Schema from './schema';
- import Document from './document';
- import MarkerCollection from './markercollection';
- import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
- import mix from '@ckeditor/ckeditor5-utils/src/mix';
- import ModelElement from './element';
- import ModelRange from './range';
- import insertContent from './utils/insertcontent';
- import deleteContent from './utils/deletecontent';
- import modifySelection from './utils/modifyselection';
- import getSelectedContent from './utils/getselectedcontent';
- import { injectSelectionPostFixer } from './utils/selection-post-fixer';
- /**
- * Editor's data model. Read about the model in the
- * {@glink framework/guides/architecture/editing-engine engine architecture guide}.
- *
- * @mixes module:utils/observablemixin~ObservableMixin
- */
- export default class Model {
- constructor() {
- /**
- * Model's marker collection.
- *
- * @readonly
- * @member {module:engine/model/markercollection~MarkerCollection}
- */
- this.markers = new MarkerCollection();
- /**
- * Model's document.
- *
- * @readonly
- * @member {module:engine/model/document~Document}
- */
- this.document = new Document( this );
- /**
- * Model's schema.
- *
- * @readonly
- * @member {module:engine/model/schema~Schema}
- */
- this.schema = new Schema();
- /**
- * All callbacks added by {@link module:engine/model/model~Model#change} or
- * {@link module:engine/model/model~Model#enqueueChange} methods waiting to be executed.
- *
- * @private
- * @type {Array.<Function>}
- */
- this._pendingChanges = [];
- /**
- * The last created and currently used writer instance.
- *
- * @private
- * @member {module:engine/model/writer~Writer}
- */
- this._currentWriter = null;
- [ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation' ]
- .forEach( methodName => this.decorate( methodName ) );
- // Adding operation validation with `highest` priority, so it is called before any other feature would like
- // to do anything with the operation. If the operation has incorrect parameters it should throw on the earliest occasion.
- this.on( 'applyOperation', ( evt, args ) => {
- const operation = args[ 0 ];
- operation._validate();
- }, { priority: 'highest' } );
- // Register some default abstract entities.
- this.schema.register( '$root', {
- isLimit: true
- } );
- this.schema.register( '$block', {
- allowIn: '$root',
- isBlock: true
- } );
- this.schema.register( '$text', {
- allowIn: '$block'
- } );
- this.schema.register( '$clipboardHolder', {
- allowContentOf: '$root',
- isLimit: true
- } );
- this.schema.extend( '$text', { allowIn: '$clipboardHolder' } );
- // Element needed by `upcastElementToMarker` converter.
- // This element temporarily represents marker bound during conversion process and is removed
- // at the end of conversion. `UpcastDispatcher` or at least `Conversion` class looks like a better for this
- // registration but both know nothing about Schema.
- this.schema.register( '$marker', {
- allowIn: [ '$root', '$block' ]
- } );
- injectSelectionPostFixer( this );
- }
- /**
- * The `change()` method is the primary way of changing the model. You should use it to modify all document nodes
- * (including detached nodes – i.e. nodes not added to the {@link module:engine/model/model~Model#document model document}),
- * the {@link module:engine/model/document~Document#selection document's selection}, and
- * {@link module:engine/model/model~Model#markers model markers}.
- *
- * model.change( writer => {
- * writer.insertText( 'foo', paragraph, 'end' );
- * } );
- *
- * All changes inside the change block use the same {@link module:engine/model/batch~Batch} so they are combined
- * into a single undo step.
- *
- * model.change( writer => {
- * writer.insertText( 'foo', paragraph, 'end' ); // foo.
- *
- * model.change( writer => {
- * writer.insertText( 'bar', paragraph, 'end' ); // foobar.
- * } );
- *
- * writer.insertText( 'bom', paragraph, 'end' ); // foobarbom.
- * } );
- *
- * The callback of the `change()` block is executed synchronously.
- *
- * You can also return a value from the change block.
- *
- * const img = model.change( writer => {
- * return writer.createElement( 'img' );
- * } );
- *
- * @see #enqueueChange
- * @param {Function} callback Callback function which may modify the model.
- * @returns {*} Value returned by the callback.
- */
- change( callback ) {
- if ( this._pendingChanges.length === 0 ) {
- // If this is the outermost block, create a new batch and start `_runPendingChanges` execution flow.
- this._pendingChanges.push( { batch: new Batch(), callback } );
- return this._runPendingChanges()[ 0 ];
- } else {
- // If this is not the outermost block, just execute the callback.
- return callback( this._currentWriter );
- }
- }
- /**
- * The `enqueueChange()` method performs similar task as the {@link #change `change()` method}, with two major differences.
- *
- * First, the callback of `enqueueChange()` is executed when all other enqueued changes are done. It might be executed
- * immediately if it is not nested in any other change block, but if it is nested in another (enqueue)change block,
- * it will be delayed and executed after the outermost block.
- *
- * model.change( writer => {
- * console.log( 1 );
- *
- * model.enqueueChange( writer => {
- * console.log( 2 );
- * } );
- *
- * console.log( 3 );
- * } ); // Will log: 1, 3, 2.
- *
- * Second, it lets you define the {@link module:engine/model/batch~Batch} into which you want to add your changes.
- * By default, a new batch is created. In the sample above, `change` and `enqueueChange` blocks use a different
- * batch (and different {@link module:engine/model/writer~Writer} since each of them operates on the separate batch).
- *
- * When using the `enqueueChange()` block you can also add some changes to the batch you used before.
- *
- * model.enqueueChange( batch, writer => {
- * writer.insertText( 'foo', paragraph, 'end' );
- * } );
- *
- * The batch instance can be obtained from {@link module:engine/model/writer~Writer#batch the writer}.
- *
- * @param {module:engine/model/batch~Batch|'transparent'|'default'} batchOrType Batch or batch type should be used in the callback.
- * If not defined, a new batch will be created.
- * @param {Function} callback Callback function which may modify the model.
- */
- enqueueChange( batchOrType, callback ) {
- if ( typeof batchOrType === 'string' ) {
- batchOrType = new Batch( batchOrType );
- } else if ( typeof batchOrType == 'function' ) {
- callback = batchOrType;
- batchOrType = new Batch();
- }
- this._pendingChanges.push( { batch: batchOrType, callback } );
- if ( this._pendingChanges.length == 1 ) {
- this._runPendingChanges();
- }
- }
- /**
- * {@link module:utils/observablemixin~ObservableMixin#decorate Decorated} function for applying
- * {@link module:engine/model/operation/operation~Operation operations} to the model.
- *
- * This is a low-level way of changing the model. It is exposed for very specific use cases (like the undo feature).
- * Normally, to modify the model, you will want to use {@link module:engine/model/writer~Writer `Writer`}.
- * See also {@glink framework/guides/architecture/editing-engine#changing-the-model Changing the model} section
- * of the {@glink framework/guides/architecture/editing-engine Editing architecture} guide.
- *
- * @param {module:engine/model/operation/operation~Operation} operation The operation to apply.
- */
- applyOperation( operation ) {
- operation._execute();
- }
- /**
- * Inserts content into the editor (specified selection) as one would expect the paste
- * functionality to work.
- *
- * This is a high-level method. It takes the {@link #schema schema} into consideration when inserting
- * the content, clears the given selection's content before inserting nodes and moves the selection
- * to its target position at the end of the process.
- * It can split elements, merge them, wrap bare text nodes in paragraphs, etc. – just like the
- * pasting feature should do.
- *
- * For lower-level methods see {@link module:engine/model/writer~Writer `Writer`}.
- *
- * This method, unlike {@link module:engine/model/writer~Writer `Writer`}'s methods, does not have to be used
- * inside a {@link #change `change()` block}.
- *
- * # Conversion and schema
- *
- * Inserting elements and text nodes into the model is not enough to make CKEditor 5 render that content
- * to the user. CKEditor 5 implements a model-view-controller architecture and what `model.insertContent()` does
- * is only adding nodes to the model. Additionally, you need to define
- * {@glink framework/guides/architecture/editing-engine#conversion converters} between the model and view
- * and define those nodes in the {@glink framework/guides/architecture/editing-engine#schema schema}.
- *
- * So, while this method may seem similar to CKEditor 4's `editor.insertHtml()` (in fact, both methods
- * are used for paste-like content insertion), CKEditor 5's method cannot be use to insert arbitrary HTML
- * unless converters are defined for all elements and attributes in that HTML.
- *
- * # Examples
- *
- * Using `insertContent()` with a manually created model structure:
- *
- * // Let's create a document fragment containing such a content:
- * //
- * // <paragrap>foo</paragraph>
- * // <blockQuote>
- * // <paragraph>bar</paragraph>
- * // </blockQuote>
- * const docFrag = editor.model.change( writer => {
- * const p1 = writer.createElement( 'paragraph' );
- * const p2 = writer.createElement( 'paragraph' );
- * const blockQuote = writer.createElement( 'blockQuote' );
- * const docFrag = writer.createDocumentFragment();
- *
- * writer.append( p1, docFrag );
- * writer.append( blockQuote, docFrag );
- * writer.append( p2, blockQuote );
- * writer.insertText( 'foo', p1 );
- * writer.insertText( 'bar', p2 );
- *
- * return docFrag;
- * } );
- *
- * // insertContent() doesn't have to be used in a change() block. It can, though,
- * // so this code could be moved to the callback defined above.
- * editor.model.insertContent( docFrag );
- *
- * Using `insertContent()` with HTML string converted to a model document fragment (similar to the pasting mechanism):
- *
- * // You can create your own HtmlDataProcessor instance or use editor.data.processor
- * // if you haven't overridden the default one (which is HtmlDataProcessor instance).
- * const htmlDP = new HtmlDataProcessor();
- *
- * // Convert an HTML string to a view document fragment.
- * const viewFragment = htmlDP.toView( htmlString );
- *
- * // Convert a view document fragment to a model document fragment
- * // in the context of $root. This conversion takes schema into
- * // the account so if e.g. the view document fragment contained a bare text node
- * // then that text node cannot be a child of $root, so it will be automatically
- * // wrapped with a <paragraph>. You can define the context yourself (in the 2nd parameter),
- * // and e.g. convert the content like it would happen in a <paragraph>.
- * // Note: the clipboard feature uses a custom context called $clipboardHolder
- * // which has a loosened schema.
- * const modelFragment = editor.data.toModel( viewFragment );
- *
- * editor.model.insertContent( modelFragment );
- *
- * By default this method will use the document selection but it can also be used with a position, range or selection instance.
- *
- * // Insert text at the current document selection position.
- * editor.model.change( writer => {
- * editor.model.insertContent( writer.createText( 'x' ) );
- * } );
- *
- * // Insert text at given position - document selection will not be modified.
- * editor.model.change( writer => {
- * editor.model.insertContent( writer.createText( 'x' ), Position.createAt( doc.getRoot(), 2 ) );
- * } );
- *
- * If an instance of {@link module:engine/model/selection~Selection} is passed as `selectable`
- * it will be moved to the target position (where the document selection should be moved after the insertion).
- *
- * // Insert text replacing given selection instance.
- * const selection = new Selection( paragraph, 'in' );
- *
- * editor.model.change( writer => {
- * editor.model.insertContent( writer.createText( 'x' ), selection );
- *
- * // insertContent() modifies the passed selection instance so it can be used to set the document selection.
- * // Note: This is not necessary when you passed document selection to insertContent().
- * writer.setSelection( selection );
- * } );
- *
- * @fires insertContent
- * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
- * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
- * module:engine/model/position~Position|module:engine/model/element~Element|
- * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} [selectable=model.document.selection]
- * Selection into which the content should be inserted. If not provided the current model document selection will be used.
- */
- insertContent( content, selectable ) {
- insertContent( this, content, selectable );
- }
- /**
- * Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
- *
- * **Note:** For the sake of predictability, the resulting selection should always be collapsed.
- * In cases where a feature wants to modify deleting behavior so selection isn't collapsed
- * (e.g. a table feature may want to keep row selection after pressing <kbd>Backspace</kbd>),
- * then that behavior should be implemented in the view's listener. At the same time, the table feature
- * will need to modify this method's behavior too, e.g. to "delete contents and then collapse
- * the selection inside the last selected cell" or "delete the row and collapse selection somewhere near".
- * That needs to be done in order to ensure that other features which use `deleteContent()` will work well with tables.
- *
- * @fires deleteContent
- * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
- * Selection of which the content should be deleted.
- * @param {Object} [options]
- * @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection.
- *
- * For example `<heading1>x[x</heading1><paragraph>y]y</paragraph>` will become:
- *
- * * `<heading1>x^y</heading1>` with the option disabled (`leaveUnmerged == false`)
- * * `<heading1>x^</heading1><paragraph>y</paragraph>` with enabled (`leaveUnmerged == true`).
- *
- * Note: {@link module:engine/model/schema~Schema#isObject object} and {@link module:engine/model/schema~Schema#isLimit limit}
- * elements will not be merged.
- *
- * @param {Boolean} [options.doNotResetEntireContent=false] Whether to skip replacing the entire content with a
- * paragraph when the entire content was selected.
- *
- * For example `<heading1>[x</heading1><paragraph>y]</paragraph>` will become:
- *
- * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
- * * `<heading1>^</heading1>` with enabled (`doNotResetEntireContent == true`)
- */
- deleteContent( selection, options ) {
- deleteContent( this, selection, options );
- }
- /**
- * Modifies the selection. Currently, the supported modifications are:
- *
- * * Extending. The selection focus is moved in the specified `options.direction` with a step specified in `options.unit`.
- * Possible values for `unit` are:
- * * `'character'` (default) - moves selection by one user-perceived character. In most cases this means moving by one
- * character in `String` sense. However, unicode also defines "combing marks". These are special symbols, that combines
- * with a symbol before it ("base character") to create one user-perceived character. For example, `q̣̇` is a normal
- * letter `q` with two "combining marks": upper dot (`Ux0307`) and lower dot (`Ux0323`). For most actions, i.e. extending
- * selection by one position, it is correct to include both "base character" and all of it's "combining marks". That is
- * why `'character'` value is most natural and common method of modifying selection.
- * * `'codePoint'` - moves selection by one unicode code point. In contrary to, `'character'` unit, this will insert
- * selection between "base character" and "combining mark", because "combining marks" have their own unicode code points.
- * However, for technical reasons, unicode code points with values above `UxFFFF` are represented in native `String` by
- * two characters, called "surrogate pairs". Halves of "surrogate pairs" have a meaning only when placed next to each other.
- * For example `𨭎` is represented in `String` by `\uD862\uDF4E`. Both `\uD862` and `\uDF4E` do not have any meaning
- * outside the pair (are rendered as ? when alone). Position between them would be incorrect. In this case, selection
- * extension will include whole "surrogate pair".
- * * `'word'` - moves selection by a whole word.
- *
- * **Note:** if you extend a forward selection in a backward direction you will in fact shrink it.
- *
- * @fires modifySelection
- * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
- * The selection to modify.
- * @param {Object} [options]
- * @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
- * @param {'character'|'codePoint'|'word'} [options.unit='character'] The unit by which selection should be modified.
- */
- modifySelection( selection, options ) {
- modifySelection( this, selection, options );
- }
- /**
- * Gets a clone of the selected content.
- *
- * For example, for the following selection:
- *
- * ```html
- * <paragraph>x</paragraph>
- * <blockQuote>
- * <paragraph>y</paragraph>
- * <heading1>fir[st</heading1>
- * </blockQuote>
- * <paragraph>se]cond</paragraph>
- * <paragraph>z</paragraph>
- * ```
- *
- * It will return a document fragment with such a content:
- *
- * ```html
- * <blockQuote>
- * <heading1>st</heading1>
- * </blockQuote>
- * <paragraph>se</paragraph>
- * ```
- *
- * @fires getSelectedContent
- * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
- * The selection of which content will be returned.
- * @returns {module:engine/model/documentfragment~DocumentFragment}
- */
- getSelectedContent( selection ) {
- return getSelectedContent( this, selection );
- }
- /**
- * Checks whether given {@link module:engine/model/range~Range range} or {@link module:engine/model/element~Element element}
- * has any content.
- *
- * Content is any text node or element which is registered in {@link module:engine/model/schema~Schema schema}.
- *
- * @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
- * @returns {Boolean}
- */
- hasContent( rangeOrElement ) {
- if ( rangeOrElement instanceof ModelElement ) {
- rangeOrElement = ModelRange.createIn( rangeOrElement );
- }
- if ( rangeOrElement.isCollapsed ) {
- return false;
- }
- for ( const item of rangeOrElement.getItems() ) {
- // Remember, `TreeWalker` returns always `textProxy` nodes.
- if ( item.is( 'textProxy' ) || this.schema.isObject( item ) ) {
- return true;
- }
- }
- return false;
- }
- /**
- * Removes all events listeners set by model instance and destroys {@link module:engine/model/document~Document}.
- */
- destroy() {
- this.document.destroy();
- this.stopListening();
- }
- /**
- * Common part of {@link module:engine/model/model~Model#change} and {@link module:engine/model/model~Model#enqueueChange}
- * which calls callbacks and returns array of values returned by these callbacks.
- *
- * @private
- * @returns {Array.<*>} Array of values returned by callbacks.
- */
- _runPendingChanges() {
- const ret = [];
- this.fire( '_beforeChanges' );
- while ( this._pendingChanges.length ) {
- // Create a new writer using batch instance created for this chain of changes.
- const currentBatch = this._pendingChanges[ 0 ].batch;
- this._currentWriter = new Writer( this, currentBatch );
- // Execute changes callback and gather the returned value.
- const callbackReturnValue = this._pendingChanges[ 0 ].callback( this._currentWriter );
- ret.push( callbackReturnValue );
- // Fire internal `_change` event.
- this.fire( '_change', this._currentWriter );
- this._pendingChanges.shift();
- this._currentWriter = null;
- }
- this.fire( '_afterChanges' );
- return ret;
- }
- /**
- * Fired after leaving each {@link module:engine/model/model~Model#enqueueChange} block or outermost
- * {@link module:engine/model/model~Model#change} block.
- *
- * **Note:** This is an internal event! Use {@link module:engine/model/document~Document#event:change} instead.
- *
- * @protected
- * @event _change
- * @param {module:engine/model/writer~Writer} writer `Writer` instance that has been used in the change block.
- */
- /**
- * Fired when entering the outermost {@link module:engine/model/model~Model#enqueueChange} or
- * {@link module:engine/model/model~Model#change} block.
- *
- * @protected
- * @event _beforeChanges
- */
- /**
- * Fired when leaving the outermost {@link module:engine/model/model~Model#enqueueChange} or
- * {@link module:engine/model/model~Model#change} block.
- *
- * @protected
- * @event _afterChanges
- */
- /**
- * Fired every time any {@link module:engine/model/operation/operation~Operation operation} is applied on the model
- * using {@link #applyOperation}.
- *
- * Note that this event is suitable only for very specific use-cases. Use it if you need to listen to every single operation
- * applied on the document. However, in most cases {@link module:engine/model/document~Document#event:change} should
- * be used.
- *
- * A few callbacks are already added to this event by engine internal classes:
- *
- * * with `highest` priority operation is validated,
- * * with `normal` priority operation is executed,
- * * with `low` priority the {@link module:engine/model/document~Document} updates its version,
- * * with `low` priority {@link module:engine/model/liveposition~LivePosition} and {@link module:engine/model/liverange~LiveRange}
- * update themselves.
- *
- * @event applyOperation
- * @param {Array} args Arguments of the `applyOperation` which is an array with a single element - applied
- * {@link module:engine/model/operation/operation~Operation operation}.
- */
- /**
- * Event fired when {@link #insertContent} method is called.
- *
- * The {@link #insertContent default action of that method} is implemented as a
- * listener to this event so it can be fully customized by the features.
- *
- * **Note** The `selectable` parameter for the {@link #insertContent} is optional. When `undefined` value is passed the method uses
- * `model.document.selection`.
- *
- * @event insertContent
- * @param {Array} args The arguments passed to the original method.
- */
- /**
- * Event fired when {@link #deleteContent} method is called.
- *
- * The {@link #deleteContent default action of that method} is implemented as a
- * listener to this event so it can be fully customized by the features.
- *
- * @event deleteContent
- * @param {Array} args The arguments passed to the original method.
- */
- /**
- * Event fired when {@link #modifySelection} method is called.
- *
- * The {@link #modifySelection default action of that method} is implemented as a
- * listener to this event so it can be fully customized by the features.
- *
- * @event modifySelection
- * @param {Array} args The arguments passed to the original method.
- */
- /**
- * Event fired when {@link #getSelectedContent} method is called.
- *
- * The {@link #getSelectedContent default action of that method} is implemented as a
- * listener to this event so it can be fully customized by the features.
- *
- * @event getSelectedContent
- * @param {Array} args The arguments passed to the original method.
- */
- }
- mix( Model, ObservableMixin );
|