8
0

document.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. // Load all basic deltas and transformations, they register themselves, but they need to be imported somewhere.
  6. import deltas from './delta/basic-deltas.js'; // jshint ignore:line
  7. import transformations from './delta/basic-transformations.js'; // jshint ignore:line
  8. import Range from './range.js';
  9. import Position from './position.js';
  10. import RootElement from './rootelement.js';
  11. import Batch from './batch.js';
  12. import History from './history.js';
  13. import LiveSelection from './liveselection.js';
  14. import Schema from './schema.js';
  15. import TreeWalker from './treewalker.js';
  16. import clone from '../../utils/lib/lodash/clone.js';
  17. import EmitterMixin from '../../utils/emittermixin.js';
  18. import CKEditorError from '../../utils/ckeditorerror.js';
  19. import mix from '../../utils/mix.js';
  20. import { isInsideSurrogatePair, isInsideCombinedSymbol } from '../../utils/unicode.js';
  21. const graveyardName = '$graveyard';
  22. /**
  23. * Document tree model describes all editable data in the editor. It may contain multiple
  24. * {@link engine.model.Document#roots root elements}, for example if the editor have multiple editable areas, each area will be
  25. * represented by the separate root.
  26. *
  27. * All changes in the document are done by {@link engine.model.operation.Operation operations}. To create operations in
  28. * a simple way, use the {@link engine.model.Batch} API, for example:
  29. *
  30. * doc.batch().insert( position, nodes ).split( otherPosition );
  31. *
  32. * @see engine.model.Document#batch
  33. *
  34. * @memberOf engine.model
  35. */
  36. export default class Document {
  37. /**
  38. * Creates an empty document instance with no {@link engine.model.Document#roots} (other than
  39. * a {@link engine.model.Document#graveyard graveyard root}).
  40. */
  41. constructor() {
  42. /**
  43. * Document version. It starts from `0` and every operation increases the version number. It is used to ensure that
  44. * operations are applied on the proper document version. If the {@link engine.model.operation.Operation#baseVersion} will
  45. * not match document version the {@link document-applyOperation-wrong-version} error is thrown.
  46. *
  47. * @readonly
  48. * @member {Number} engine.model.Document#version
  49. */
  50. this.version = 0;
  51. /**
  52. * Selection done on this document.
  53. *
  54. * @readonly
  55. * @member {engine.model.LiveSelection} engine.model.Document#selection
  56. */
  57. this.selection = new LiveSelection( this );
  58. /**
  59. * Schema for this document.
  60. *
  61. * @member {engine.model.Schema} engine.model.Document#schema
  62. */
  63. this.schema = new Schema();
  64. /**
  65. * Document's history.
  66. *
  67. * **Note:** Be aware that deltas applied to the stored deltas might be removed or changed.
  68. *
  69. * @readonly
  70. * @member {engine.model.History} engine.model.Document#history
  71. */
  72. this.history = new History( this );
  73. /**
  74. * Array of pending changes. See: {@link engine.model.Document#enqueueChanges}.
  75. *
  76. * @private
  77. * @member {Array.<Function>} engine.model.Document#_pendingChanges
  78. */
  79. this._pendingChanges = [];
  80. /**
  81. * List of roots that are owned and managed by this document. Use {@link engine.model.document#createRoot} and
  82. * {@link engine.model.document#getRoot} to manipulate it.
  83. *
  84. * @readonly
  85. * @protected
  86. * @member {Map} engine.model.Document#roots
  87. */
  88. this._roots = new Map();
  89. // Add events that will ensure selection correctness.
  90. this.selection.on( 'change:range', () => {
  91. for ( let range of this.selection.getRanges() ) {
  92. if ( !this._validateSelectionRange( range ) ) {
  93. /**
  94. * Range from document selection starts or ends at incorrect position.
  95. *
  96. * @error document-selection-wrong-position
  97. * @param {engine.model.Range} range
  98. */
  99. throw new CKEditorError( 'document-selection-wrong-position: ' +
  100. 'Range from document selection starts or ends at incorrect position.', { range } );
  101. }
  102. }
  103. } );
  104. // Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
  105. this.createRoot( '$root', graveyardName );
  106. }
  107. /**
  108. * Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
  109. *
  110. * @readonly
  111. * @type {engine.model.RootElement}
  112. */
  113. get graveyard() {
  114. return this.getRoot( graveyardName );
  115. }
  116. /**
  117. * This is the entry point for all document changes. All changes on the document are done using
  118. * {@link engine.model.operation.Operation operations}. To create operations in the simple way use the
  119. * {@link engine.model.Batch} API available via {@link engine.model.Document#batch} method.
  120. *
  121. * @fires @link engine.model.Document#change
  122. * @param {engine.model.operation.Operation} operation Operation to be applied.
  123. */
  124. applyOperation( operation ) {
  125. if ( operation.baseVersion !== this.version ) {
  126. /**
  127. * Only operations with matching versions can be applied.
  128. *
  129. * @error document-applyOperation-wrong-version
  130. * @param {engine.model.operation.Operation} operation
  131. */
  132. throw new CKEditorError(
  133. 'model-document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
  134. { operation: operation } );
  135. }
  136. let changes = operation._execute();
  137. this.version++;
  138. this.history.addDelta( operation.delta );
  139. const batch = operation.delta && operation.delta.batch;
  140. if ( changes ) {
  141. // `NoOperation` returns no changes, do not fire event for it.
  142. this.fire( 'change', operation.type, changes, batch );
  143. }
  144. }
  145. /**
  146. * Creates a {@link engine.model.Batch} instance which allows to change the document.
  147. *
  148. * @param {String} [type] Batch type. See {@link engine.model.Batch#type}.
  149. * @returns {engine.model.Batch} Batch instance.
  150. */
  151. batch( type ) {
  152. return new Batch( this, type );
  153. }
  154. /**
  155. * Creates a new top-level root.
  156. *
  157. * @param {String} [elementName='$root'] Element name. Defaults to `'$root'` which also have
  158. * some basic schema defined (`$block`s are allowed inside the `$root`). Make sure to define a proper
  159. * schema if you use a different name.
  160. * @param {String} [rootName='main'] Unique root name.
  161. * @returns {engine.model.RootElement} Created root.
  162. */
  163. createRoot( elementName = '$root', rootName = 'main' ) {
  164. if ( this._roots.has( rootName ) ) {
  165. /**
  166. * Root with specified name already exists.
  167. *
  168. * @error document-createRoot-name-exists
  169. * @param {engine.model.Document} doc
  170. * @param {String} name
  171. */
  172. throw new CKEditorError(
  173. 'model-document-createRoot-name-exists: Root with specified name already exists.',
  174. { name: rootName }
  175. );
  176. }
  177. const root = new RootElement( this, elementName, rootName );
  178. this._roots.set( rootName, root );
  179. return root;
  180. }
  181. /**
  182. * Removes all events listeners set by document instance.
  183. */
  184. destroy() {
  185. this.selection.destroy();
  186. this.stopListening();
  187. }
  188. /**
  189. * Enqueues document changes. Any changes to be done on document (mostly using {@link engine.model.Document#batch}
  190. * should be placed in the queued callback. If no other plugin is changing document at the moment, the callback will be
  191. * called immediately. Otherwise it will wait for all previously queued changes to finish happening. This way
  192. * queued callback will not interrupt other callbacks.
  193. *
  194. * When all queued changes are done {@link engine.model.Document#changesDone} event is fired.
  195. *
  196. * @fires @link engine.model.Document#changesDone
  197. * @param {Function} callback Callback to enqueue.
  198. */
  199. enqueueChanges( callback ) {
  200. this._pendingChanges.push( callback );
  201. if ( this._pendingChanges.length == 1 ) {
  202. while ( this._pendingChanges.length ) {
  203. this._pendingChanges[ 0 ]();
  204. this._pendingChanges.shift();
  205. }
  206. this.fire( 'changesDone' );
  207. }
  208. }
  209. /**
  210. * Returns top-level root by its name.
  211. *
  212. * @param {String} [name='main'] Unique root name.
  213. * @returns {engine.model.RootElement} Root registered under given name.
  214. */
  215. getRoot( name = 'main' ) {
  216. if ( !this._roots.has( name ) ) {
  217. /**
  218. * Root with specified name does not exist.
  219. *
  220. * @error document-getRoot-root-not-exist
  221. * @param {String} name
  222. */
  223. throw new CKEditorError(
  224. 'model-document-getRoot-root-not-exist: Root with specified name does not exist.',
  225. { name: name }
  226. );
  227. }
  228. return this._roots.get( name );
  229. }
  230. /**
  231. * Checks if root with given name is defined.
  232. *
  233. * @param {String} name Name of root to check.
  234. * @returns {Boolean}
  235. */
  236. hasRoot( name ) {
  237. return this._roots.has( name );
  238. }
  239. /**
  240. * Returns array with names of all roots (without the {@link engine.model.Document#graveyard}) added to the document.
  241. *
  242. * @returns {Array.<String>} Roots names.
  243. */
  244. getRootNames() {
  245. return Array.from( this._roots.keys() ).filter( ( name ) => name != graveyardName );
  246. }
  247. /**
  248. * Basing on given `position`, finds and returns a {@link engine.model.Position Position} instance that is nearest
  249. * to that `position` and is a correct position for selection. A position is correct for selection if
  250. * text node can be placed at that position.
  251. *
  252. * If no correct position is found, the first position in given `position` root is returned. This can happen if no node
  253. * has been added to the root or it may mean incorrect model document state.
  254. *
  255. * @param {engine.model.Position} position Reference position where selection position should be looked for.
  256. * @returns {engine.model.Position|null} Nearest selection position.
  257. */
  258. getNearestSelectionPosition( position ) {
  259. if ( this.schema.check( { name: '$text', inside: position } ) ) {
  260. return Position.createFromPosition( position );
  261. }
  262. const backwardWalker = new TreeWalker( { startPosition: position, direction: 'backward' } );
  263. const forwardWalker = new TreeWalker( { startPosition: position } );
  264. let done = false;
  265. while ( !done ) {
  266. done = true;
  267. let step = backwardWalker.next();
  268. if ( !step.done ) {
  269. if ( this.schema.check( { name: '$text', inside: step.value.nextPosition } ) ) {
  270. return step.value.nextPosition;
  271. }
  272. done = false;
  273. }
  274. step = forwardWalker.next();
  275. if ( !step.done ) {
  276. if ( this.schema.check( { name: '$text', inside: step.value.nextPosition } ) ) {
  277. return step.value.nextPosition;
  278. }
  279. done = false;
  280. }
  281. }
  282. return new Position( position.root, [ 0 ] );
  283. }
  284. /**
  285. * Custom toJSON method to solve child-parent circular dependencies.
  286. *
  287. * @returns {Object} Clone of this object with the document property changed to string.
  288. */
  289. toJSON() {
  290. const json = clone( this );
  291. // Due to circular references we need to remove parent reference.
  292. json.selection = '[engine.model.LiveSelection]';
  293. return json;
  294. }
  295. /**
  296. * Returns default root for this document which is either the first root that was added to the the document using
  297. * {@link engine.model.Document#createRoot} or the {@link engine.model.Document#graveyard graveyard root} if
  298. * no other roots were created.
  299. *
  300. * @protected
  301. * @returns {engine.model.RootElement} The default root for this document.
  302. */
  303. _getDefaultRoot() {
  304. for ( let root of this._roots.values() ) {
  305. if ( root !== this.graveyard ) {
  306. return root;
  307. }
  308. }
  309. return this.graveyard;
  310. }
  311. /**
  312. * Returns a default range for this selection. The default range is a collapsed range that starts and ends
  313. * at the beginning of this selection's document's {@link engine.model.Document#_getDefaultRoot default root}.
  314. *
  315. * @protected
  316. * @returns {engine.model.Range}
  317. */
  318. _getDefaultRange() {
  319. const defaultRoot = this._getDefaultRoot();
  320. // Find the first position where the selection can be put.
  321. const position = new Position( defaultRoot, [ 0 ] );
  322. const selectionPosition = this.getNearestSelectionPosition( position );
  323. return new Range( selectionPosition );
  324. }
  325. /**
  326. * Checks whether given {@link engine.model.Range range} is a valid range for
  327. * {@link engine.model.Document#selection document's selection}.
  328. *
  329. * @private
  330. * @param {engine.model.Range} range Range to check.
  331. * @returns {Boolean} `true` if `range` is valid, `false` otherwise.
  332. */
  333. _validateSelectionRange( range ) {
  334. return validateTextNodePosition( range.start ) && validateTextNodePosition( range.end );
  335. }
  336. /**
  337. * Fired when document changes by applying an operation.
  338. *
  339. * There are 5 types of change:
  340. *
  341. * * 'insert' when nodes are inserted,
  342. * * 'remove' when nodes are removed,
  343. * * 'reinsert' when remove is undone,
  344. * * 'move' when nodes are moved,
  345. * * 'addAttribute' when attributes are added,
  346. * * 'removeAttribute' when attributes are removed,
  347. * * 'changeAttribute' when attributes change,
  348. * * 'addRootAttribute' when attribute for root is added,
  349. * * 'removeRootAttribute' when attribute for root is removed,
  350. * * 'changeRootAttribute' when attribute for root changes.
  351. *
  352. * @event engine.model.Document#change
  353. * @param {String} type Change type, possible option: 'insert', 'remove', 'reinsert', 'move', 'attribute'.
  354. * @param {Object} data Additional information about the change.
  355. * @param {engine.model.Range} data.range Range in model containing changed nodes. Note that the range state is
  356. * after changes has been done, i.e. for 'remove' the range will be in the {@link engine.model.Document#graveyard graveyard root}.
  357. * This is `undefined` for "...root..." types.
  358. * @param {engine.model.Position} [data.sourcePosition] Change source position. Exists for 'remove', 'reinsert' and 'move'.
  359. * Note that this position state is before changes has been done, i.e. for 'reinsert' the source position will be in the
  360. * {@link engine.model.Document#graveyard graveyard root}.
  361. * @param {String} [data.key] Only for attribute types. Key of changed / inserted / removed attribute.
  362. * @param {*} [data.oldValue] Only for 'removeAttribute', 'removeRootAttribute', 'changeAttribute' or
  363. * 'changeRootAttribute' type.
  364. * @param {*} [data.newValue] Only for 'addAttribute', 'addRootAttribute', 'changeAttribute' or
  365. * 'changeRootAttribute' type.
  366. * @param {engine.model.RootElement} [changeInfo.root] Root element which attributes got changed. This is defined
  367. * only for root types.
  368. * @param {engine.model.Batch} batch A {@link engine.model.Batch batch} of changes which this change is a part of.
  369. */
  370. /**
  371. * Fired when all queued document changes are done. See {@link engine.model.Document#enqueueChanges}.
  372. *
  373. * @event engine.model.Document#changesDone
  374. */
  375. }
  376. mix( Document, EmitterMixin );
  377. // Checks whether given range boundary position is valid for document selection, meaning that is not between
  378. // unicode surrogate pairs or base character and combining marks.
  379. function validateTextNodePosition( rangeBoundary ) {
  380. const textNode = rangeBoundary.textNode;
  381. if ( textNode ) {
  382. const data = textNode.data;
  383. const offset = rangeBoundary.offset - textNode.startOffset;
  384. return !isInsideSurrogatePair( data, offset ) && !isInsideCombinedSymbol( data, offset );
  385. }
  386. return true;
  387. }