This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 40
/
model.js
240 lines (214 loc) · 7.75 KB
/
model.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
/**
* @license Copyright (c) 2003-2017, 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';
/**
* 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.
*/
export default class Model {
constructor() {
/**
* 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 = [];
/**
* Editors document model.
*
* @member {module:engine/model/document~Document}
*/
this.document = new Document( this );
/**
* Schema for editors model.
*
* @member {module:engine/model/schema~Schema}
*/
this.schema = new Schema();
/**
* Models markers' collection.
*
* @readonly
* @member {module:engine/model/markercollection~MarkerCollection}
*/
this.markers = new MarkerCollection();
this.decorate( 'applyOperation' );
}
/**
* 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}.
*
* model.change( writer => {
* writer.insertText( 'foo', paragraph, 'end' );
* } );
*
* All changes inside the change block use the same {@link module:engine/model/batch~Batch} so share the same
* 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
* } );
*
* Change block is executed imminently.
*
* You can also return a value from the change block.
*
* const img = model.change( writer => {
* return writer.createElement( 'img' );
* } );
*
* When the outermost block is done the {@link #event:change} event is fired.
*
* @see #enqueueChange
* @fires event:change
* @fires event:changesDone
* @param {Function} callback Callback function which may modify the model.
* @returns {*} Value returned by the callback
*/
change( callback ) {
if ( this._pendingChanges.length === 0 ) {
this._pendingChanges.push( { batch: new Batch(), callback } );
return this._runPendingChanges()[ 0 ];
} else {
return callback( this._currentWriter );
}
}
/**
* `enqueueChange` method is very similar to 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
* 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.
*
* model.change( writer => {
* console.log( 1 );
*
* model.enqueueChange( writer => {
* console.log( 3 );
* } );
*
* console.log( 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
* 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.
*
* model.enqueueChange( batch, writer => {
* writer.insertText( 'foo', paragraph, 'end' );
* } );
*
* @fires event:change
* @fires event:changesDone
* @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.
* @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();
}
}
/**
* 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 = [];
while ( this._pendingChanges.length ) {
this._currentWriter = new Writer( this, this._pendingChanges[ 0 ].batch );
ret.push( this._pendingChanges[ 0 ].callback( this._currentWriter ) );
this.fire( 'change' );
this._pendingChanges.shift();
this._currentWriter = null;
}
this.fire( 'changesDone' );
return ret;
}
/**
* {@link module:utils/observablemixin~ObservableMixin#decorate Decorated} function to apply
* {@link module:engine/model/operation/operation~Operation operations} on the model.
*
* @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 ) {
return operation._execute();
}
/**
* Removes all events listeners set by model instance and destroy Document.
*/
destroy() {
this.document.destroy();
this.stopListening();
}
/**
* 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 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 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}.
*/
}
mix( Model, ObservableMixin );