|
@@ -1,5 +1,5 @@
|
|
|
/**
|
|
/**
|
|
|
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
|
|
|
|
|
|
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
|
|
|
* For licensing, see LICENSE.md.
|
|
* For licensing, see LICENSE.md.
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
@@ -28,10 +28,12 @@ import modifySelection from './utils/modifyselection';
|
|
|
import getSelectedContent from './utils/getselectedcontent';
|
|
import getSelectedContent from './utils/getselectedcontent';
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Editors data model class. Model defines all data: either nodes users see in editable roots, grouped as the
|
|
|
|
|
- * {@link module:engine/model/model~Model#document}, and all detached nodes, used to data manipulation. All of them are
|
|
|
|
|
- * created and modified by the {@link module:engine/model/writer~Writer}, which can be get using
|
|
|
|
|
- * {@link module:engine/model/model~Model#change} or {@link module:engine/model/model~Model#enqueueChange} methods.
|
|
|
|
|
|
|
+ * Editor's data model class. Model defines all the data: both nodes that are attached to the roots of the
|
|
|
|
|
+ * {@link module:engine/model/model~Model#document model document}, and also all detached nodes which has not been yet
|
|
|
|
|
+ * added to the document.
|
|
|
|
|
+ *
|
|
|
|
|
+ * All those nodes are created and modified by the {@link module:engine/model/writer~Writer model writer}, which can be
|
|
|
|
|
+ * accessed by using {@link module:engine/model/model~Model#change} or {@link module:engine/model/model~Model#enqueueChange} methods.
|
|
|
*
|
|
*
|
|
|
* @mixes module:utils/observablemixin~ObservableMixin
|
|
* @mixes module:utils/observablemixin~ObservableMixin
|
|
|
*/
|
|
*/
|
|
@@ -46,6 +48,14 @@ export default class Model {
|
|
|
*/
|
|
*/
|
|
|
this._pendingChanges = [];
|
|
this._pendingChanges = [];
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Models markers' collection.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @readonly
|
|
|
|
|
+ * @member {module:engine/model/markercollection~MarkerCollection}
|
|
|
|
|
+ */
|
|
|
|
|
+ this.markers = new MarkerCollection();
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Editors document model.
|
|
* Editors document model.
|
|
|
*
|
|
*
|
|
@@ -54,23 +64,31 @@ export default class Model {
|
|
|
this.document = new Document( this );
|
|
this.document = new Document( this );
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Schema for editors model.
|
|
|
|
|
|
|
+ * The last created and currently used writer instance.
|
|
|
*
|
|
*
|
|
|
- * @member {module:engine/model/schema~Schema}
|
|
|
|
|
|
|
+ * @private
|
|
|
|
|
+ * @member {module:engine/model/writer~Writer}
|
|
|
*/
|
|
*/
|
|
|
- this.schema = new Schema();
|
|
|
|
|
|
|
+ this._currentWriter = null;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Models markers' collection.
|
|
|
|
|
|
|
+ * Schema for editors model.
|
|
|
*
|
|
*
|
|
|
- * @readonly
|
|
|
|
|
- * @member {module:engine/model/markercollection~MarkerCollection}
|
|
|
|
|
|
|
+ * @member {module:engine/model/schema~Schema}
|
|
|
*/
|
|
*/
|
|
|
- this.markers = new MarkerCollection();
|
|
|
|
|
|
|
+ this.schema = new Schema();
|
|
|
|
|
|
|
|
[ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation' ]
|
|
[ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation' ]
|
|
|
.forEach( methodName => this.decorate( methodName ) );
|
|
.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.
|
|
// Register some default abstract entities.
|
|
|
this.schema.register( '$root', {
|
|
this.schema.register( '$root', {
|
|
|
isLimit: true
|
|
isLimit: true
|
|
@@ -91,26 +109,26 @@ export default class Model {
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Change method is the primary way of changing the model. You should use it to modify any node, including detached
|
|
* Change method is the primary way of changing the model. You should use it to modify any node, including detached
|
|
|
- * nodes, not added to the {@link module:engine/model/model~Model#document}.
|
|
|
|
|
|
|
+ * nodes (not added to the {@link module:engine/model/model~Model#document model document}).
|
|
|
*
|
|
*
|
|
|
* model.change( writer => {
|
|
* model.change( writer => {
|
|
|
* writer.insertText( 'foo', paragraph, 'end' );
|
|
* writer.insertText( 'foo', paragraph, 'end' );
|
|
|
* } );
|
|
* } );
|
|
|
*
|
|
*
|
|
|
- * All changes inside the change block use the same {@link module:engine/model/batch~Batch} so share the same
|
|
|
|
|
|
|
+ * All changes inside the change block use the same {@link module:engine/model/batch~Batch} so they share the same
|
|
|
* undo step.
|
|
* undo step.
|
|
|
*
|
|
*
|
|
|
* model.change( writer => {
|
|
* model.change( writer => {
|
|
|
- * writer.insertText( 'foo', paragraph, 'end' ); // foo
|
|
|
|
|
|
|
+ * writer.insertText( 'foo', paragraph, 'end' ); // foo.
|
|
|
*
|
|
*
|
|
|
* model.change( writer => {
|
|
* model.change( writer => {
|
|
|
- * writer.insertText( 'bar', paragraph, 'end' ); // foobar
|
|
|
|
|
|
|
+ * writer.insertText( 'bar', paragraph, 'end' ); // foobar.
|
|
|
* } );
|
|
* } );
|
|
|
*
|
|
*
|
|
|
- * writer.insertText( 'bom', paragraph, 'end' ); // foobarbom
|
|
|
|
|
|
|
+ * writer.insertText( 'bom', paragraph, 'end' ); // foobarbom.
|
|
|
* } );
|
|
* } );
|
|
|
*
|
|
*
|
|
|
- * Change block is executed imminently.
|
|
|
|
|
|
|
+ * Change block is executed immediately.
|
|
|
*
|
|
*
|
|
|
* You can also return a value from the change block.
|
|
* You can also return a value from the change block.
|
|
|
*
|
|
*
|
|
@@ -118,43 +136,43 @@ export default class Model {
|
|
|
* return writer.createElement( 'img' );
|
|
* return writer.createElement( 'img' );
|
|
|
* } );
|
|
* } );
|
|
|
*
|
|
*
|
|
|
- * When the outermost block is done the {@link #event:change} event is fired.
|
|
|
|
|
|
|
+ * When the outermost block is done the {@link #event:_change} event is fired.
|
|
|
*
|
|
*
|
|
|
* @see #enqueueChange
|
|
* @see #enqueueChange
|
|
|
- * @fires event:change
|
|
|
|
|
- * @fires event:changesDone
|
|
|
|
|
* @param {Function} callback Callback function which may modify the model.
|
|
* @param {Function} callback Callback function which may modify the model.
|
|
|
- * @returns {*} Value returned by the callback
|
|
|
|
|
|
|
+ * @returns {*} Value returned by the callback.
|
|
|
*/
|
|
*/
|
|
|
change( callback ) {
|
|
change( callback ) {
|
|
|
if ( this._pendingChanges.length === 0 ) {
|
|
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 } );
|
|
this._pendingChanges.push( { batch: new Batch(), callback } );
|
|
|
|
|
|
|
|
return this._runPendingChanges()[ 0 ];
|
|
return this._runPendingChanges()[ 0 ];
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ // If this is not the outermost block, just execute the callback.
|
|
|
return callback( this._currentWriter );
|
|
return callback( this._currentWriter );
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * `enqueueChange` method is very similar to the {@link #change change method}, with two major differences.
|
|
|
|
|
|
|
+ * `enqueueChange` method performs similar task as the {@link #change change method}, with two major differences.
|
|
|
*
|
|
*
|
|
|
* First, the callback of the `enqueueChange` is executed when all other changes are done. It might be executed
|
|
* First, the callback of the `enqueueChange` is executed when all other changes are done. It might be executed
|
|
|
- * imminently if it is not nested in any other change block, but if it is nested in another change it will be delayed
|
|
|
|
|
- * and executed after the outermost block. If will be also executed after all previous `enqueueChange` blocks.
|
|
|
|
|
|
|
+ * 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 => {
|
|
* model.change( writer => {
|
|
|
* console.log( 1 );
|
|
* console.log( 1 );
|
|
|
*
|
|
*
|
|
|
* model.enqueueChange( writer => {
|
|
* model.enqueueChange( writer => {
|
|
|
- * console.log( 3 );
|
|
|
|
|
|
|
+ * console.log( 2 );
|
|
|
* } );
|
|
* } );
|
|
|
*
|
|
*
|
|
|
- * console.log( 2 );
|
|
|
|
|
- * } );
|
|
|
|
|
|
|
+ * console.log( 3 );
|
|
|
|
|
+ * } ); // Will log: 1, 3, 2.
|
|
|
*
|
|
*
|
|
|
- * Second, it let you define the {@link module:engine/model/batch~Batch} to which you want to add your changes.
|
|
|
|
|
- * By default it creates a new batch, note that in the sample above `change` and `enqueueChange` blocks use a different
|
|
|
|
|
|
|
+ * 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).
|
|
* batch (and different {@link module:engine/model/writer~Writer} since each of them operates on the separate batch).
|
|
|
*
|
|
*
|
|
|
* Using `enqueueChange` block you can also add some changes to the batch you used before.
|
|
* Using `enqueueChange` block you can also add some changes to the batch you used before.
|
|
@@ -163,10 +181,10 @@ export default class Model {
|
|
|
* writer.insertText( 'foo', paragraph, 'end' );
|
|
* writer.insertText( 'foo', paragraph, 'end' );
|
|
|
* } );
|
|
* } );
|
|
|
*
|
|
*
|
|
|
- * @fires event:change
|
|
|
|
|
- * @fires event:changesDone
|
|
|
|
|
|
|
+ * `Batch` instance can be obtained from {@link module:engine/model/writer~Writer#batch the writer}.
|
|
|
|
|
+ *
|
|
|
* @param {module:engine/model/batch~Batch|String} batchOrType Batch or batch type should be used in the callback.
|
|
* @param {module:engine/model/batch~Batch|String} batchOrType Batch or batch type should be used in the callback.
|
|
|
- * If not defined new batch will be created.
|
|
|
|
|
|
|
+ * If not defined, a new batch will be created.
|
|
|
* @param {Function} callback Callback function which may modify the model.
|
|
* @param {Function} callback Callback function which may modify the model.
|
|
|
*/
|
|
*/
|
|
|
enqueueChange( batchOrType, callback ) {
|
|
enqueueChange( batchOrType, callback ) {
|
|
@@ -195,19 +213,21 @@ export default class Model {
|
|
|
const ret = [];
|
|
const ret = [];
|
|
|
|
|
|
|
|
while ( this._pendingChanges.length ) {
|
|
while ( this._pendingChanges.length ) {
|
|
|
- this._currentWriter = new Writer( this, this._pendingChanges[ 0 ].batch );
|
|
|
|
|
|
|
+ // 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 );
|
|
|
|
|
|
|
|
- ret.push( this._pendingChanges[ 0 ].callback( this._currentWriter ) );
|
|
|
|
|
|
|
+ // Execute changes callback and gather the returned value.
|
|
|
|
|
+ const callbackReturnValue = this._pendingChanges[ 0 ].callback( this._currentWriter );
|
|
|
|
|
+ ret.push( callbackReturnValue );
|
|
|
|
|
|
|
|
- this.fire( 'change' );
|
|
|
|
|
|
|
+ // Fire internal `_change` event.
|
|
|
|
|
+ this.fire( '_change', this._currentWriter );
|
|
|
|
|
|
|
|
this._pendingChanges.shift();
|
|
this._pendingChanges.shift();
|
|
|
-
|
|
|
|
|
this._currentWriter = null;
|
|
this._currentWriter = null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this.fire( 'changesDone' );
|
|
|
|
|
-
|
|
|
|
|
return ret;
|
|
return ret;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -216,11 +236,9 @@ export default class Model {
|
|
|
* {@link module:engine/model/operation/operation~Operation operations} on the model.
|
|
* {@link module:engine/model/operation/operation~Operation operations} on the model.
|
|
|
*
|
|
*
|
|
|
* @param {module:engine/model/operation/operation~Operation} operation Operation to apply
|
|
* @param {module:engine/model/operation/operation~Operation} operation Operation to apply
|
|
|
- * @returns {Object} Object with additional information about the applied changes. It properties depends on the
|
|
|
|
|
- * operation type.
|
|
|
|
|
*/
|
|
*/
|
|
|
applyOperation( operation ) {
|
|
applyOperation( operation ) {
|
|
|
- return operation._execute();
|
|
|
|
|
|
|
+ operation._execute();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -324,89 +342,84 @@ export default class Model {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Removes all events listeners set by model instance and destroy Document.
|
|
|
|
|
|
|
+ * Removes all events listeners set by model instance and destroys {@link module:engine/model/document~Document}.
|
|
|
*/
|
|
*/
|
|
|
destroy() {
|
|
destroy() {
|
|
|
this.document.destroy();
|
|
this.document.destroy();
|
|
|
this.stopListening();
|
|
this.stopListening();
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
-mix( Model, ObservableMixin );
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * Fired after leaving each {@link module:engine/model/model~Model#enqueueChange} block or outermost
|
|
|
|
|
- * {@link module:engine/model/model~Model#change} block.
|
|
|
|
|
- * Have the same parameters as {@link module:engine/model/model~Model#change}.
|
|
|
|
|
- *
|
|
|
|
|
- * @event change
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 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 all queued model changes are done.
|
|
|
|
|
- *
|
|
|
|
|
- * @see #change
|
|
|
|
|
- * @see #enqueueChange
|
|
|
|
|
- * @event changesDone
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 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}.
|
|
|
|
|
+ */
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * Fired every time any {@link module:engine/model/operation/operation~Operation operation} is applied on the model
|
|
|
|
|
- * using {@link #applyOperation}.
|
|
|
|
|
- *
|
|
|
|
|
- * Note that this is an internal event for the specific use-cases. You can use it if you need to know about each operation
|
|
|
|
|
- * applied on the document, but in most cases {@link #change} event which is fired when all changes in a
|
|
|
|
|
- * {@link module:engine/model/batch~Batch} are applied, is a better choice.
|
|
|
|
|
- *
|
|
|
|
|
- * With the high priority operation is validated.
|
|
|
|
|
- *
|
|
|
|
|
- * With the normal priority operation is executed. After that priority you will be able to get additional
|
|
|
|
|
- * information about the applied changes returned by {@link module:engine/model/operation/operation~Operation#_execute}
|
|
|
|
|
- * as `evt.return`.
|
|
|
|
|
- *
|
|
|
|
|
- * With the low priority the {@link module:engine/model/document~Document} listen on this event and updates its version.
|
|
|
|
|
- *
|
|
|
|
|
- * @event applyOperation
|
|
|
|
|
- * @param {Array} args Arguments of the `applyOperation` which are an array with a single element:
|
|
|
|
|
- * {@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.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @event insertContent
|
|
|
|
|
+ * @param {Array} args The arguments passed to the original method.
|
|
|
|
|
+ */
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 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.
|
|
|
|
|
- *
|
|
|
|
|
- * @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 #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 #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.
|
|
|
|
|
+ */
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 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 );
|