documentfragment.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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 module:engine/model/documentfragment
  7. */
  8. import NodeList from './nodelist';
  9. import Element from './element';
  10. import Text from './text';
  11. import TextProxy from './textproxy';
  12. import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  13. // @if CK_DEBUG_ENGINE // const { stringifyMap } = require( '../dev-utils/utils' );
  14. /**
  15. * DocumentFragment represents a part of model which does not have a common root but it's top-level nodes
  16. * can be seen as siblings. In other words, it is a detached part of model tree, without a root.
  17. *
  18. * DocumentFragment has own {@link module:engine/model/markercollection~MarkerCollection}. Markers from this collection
  19. * will be set to the {@link module:engine/model/model~Model#markers model markers} by a
  20. * {@link module:engine/model/writer~Writer#insert} function.
  21. */
  22. export default class DocumentFragment {
  23. /**
  24. * Creates an empty `DocumentFragment`.
  25. *
  26. * **Note:** Constructor of this class shouldn't be used directly in the code.
  27. * Use the {@link module:engine/model/writer~Writer#createDocumentFragment} method instead.
  28. *
  29. * @protected
  30. * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
  31. * Nodes to be contained inside the `DocumentFragment`.
  32. */
  33. constructor( children ) {
  34. /**
  35. * DocumentFragment static markers map. This is a list of names and {@link module:engine/model/range~Range ranges}
  36. * which will be set as Markers to {@link module:engine/model/model~Model#markers model markers collection}
  37. * when DocumentFragment will be inserted to the document.
  38. *
  39. * @readonly
  40. * @member {Map<String,module:engine/model/range~Range>} module:engine/model/documentfragment~DocumentFragment#markers
  41. */
  42. this.markers = new Map();
  43. /**
  44. * List of nodes contained inside the document fragment.
  45. *
  46. * @private
  47. * @member {module:engine/model/nodelist~NodeList} module:engine/model/documentfragment~DocumentFragment#_children
  48. */
  49. this._children = new NodeList();
  50. if ( children ) {
  51. this._insertChild( 0, children );
  52. }
  53. }
  54. /**
  55. * Returns an iterator that iterates over all nodes contained inside this document fragment.
  56. *
  57. * @returns {Iterable.<module:engine/model/node~Node>}
  58. */
  59. [ Symbol.iterator ]() {
  60. return this.getChildren();
  61. }
  62. /**
  63. * Number of this document fragment's children.
  64. *
  65. * @readonly
  66. * @type {Number}
  67. */
  68. get childCount() {
  69. return this._children.length;
  70. }
  71. /**
  72. * Sum of {@link module:engine/model/node~Node#offsetSize offset sizes} of all of this document fragment's children.
  73. *
  74. * @readonly
  75. * @type {Number}
  76. */
  77. get maxOffset() {
  78. return this._children.maxOffset;
  79. }
  80. /**
  81. * Is `true` if there are no nodes inside this document fragment, `false` otherwise.
  82. *
  83. * @readonly
  84. * @type {Boolean}
  85. */
  86. get isEmpty() {
  87. return this.childCount === 0;
  88. }
  89. /**
  90. * Artificial root of `DocumentFragment`. Returns itself. Added for compatibility reasons.
  91. *
  92. * @readonly
  93. * @type {module:engine/model/documentfragment~DocumentFragment}
  94. */
  95. get root() {
  96. return this;
  97. }
  98. /**
  99. * Artificial parent of `DocumentFragment`. Returns `null`. Added for compatibility reasons.
  100. *
  101. * @readonly
  102. * @type {null}
  103. */
  104. get parent() {
  105. return null;
  106. }
  107. /**
  108. * Checks whether this object is of the given type.
  109. *
  110. * docFrag.is( 'documentFragment' ); // -> true
  111. * docFrag.is( 'model:documentFragment' ); // -> true
  112. *
  113. * docFrag.is( 'view:documentFragment' ); // -> false
  114. * docFrag.is( 'element' ); // -> false
  115. * docFrag.is( 'node' ); // -> false
  116. *
  117. * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
  118. *
  119. * @param {String} type
  120. * @returns {Boolean}
  121. */
  122. is( type ) {
  123. return type === 'documentFragment' || type === 'model:documentFragment';
  124. }
  125. /**
  126. * Gets the child at the given index. Returns `null` if incorrect index was passed.
  127. *
  128. * @param {Number} index Index of child.
  129. * @returns {module:engine/model/node~Node|null} Child node.
  130. */
  131. getChild( index ) {
  132. return this._children.getNode( index );
  133. }
  134. /**
  135. * Returns an iterator that iterates over all of this document fragment's children.
  136. *
  137. * @returns {Iterable.<module:engine/model/node~Node>}
  138. */
  139. getChildren() {
  140. return this._children[ Symbol.iterator ]();
  141. }
  142. /**
  143. * Returns an index of the given child node. Returns `null` if given node is not a child of this document fragment.
  144. *
  145. * @param {module:engine/model/node~Node} node Child node to look for.
  146. * @returns {Number|null} Child node's index.
  147. */
  148. getChildIndex( node ) {
  149. return this._children.getNodeIndex( node );
  150. }
  151. /**
  152. * Returns the starting offset of given child. Starting offset is equal to the sum of
  153. * {@link module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
  154. * given node is not a child of this document fragment.
  155. *
  156. * @param {module:engine/model/node~Node} node Child node to look for.
  157. * @returns {Number|null} Child node's starting offset.
  158. */
  159. getChildStartOffset( node ) {
  160. return this._children.getNodeStartOffset( node );
  161. }
  162. /**
  163. * Returns path to a `DocumentFragment`, which is an empty array. Added for compatibility reasons.
  164. *
  165. * @returns {Array}
  166. */
  167. getPath() {
  168. return [];
  169. }
  170. /**
  171. * Returns a descendant node by its path relative to this element.
  172. *
  173. * // <this>a<b>c</b></this>
  174. * this.getNodeByPath( [ 0 ] ); // -> "a"
  175. * this.getNodeByPath( [ 1 ] ); // -> <b>
  176. * this.getNodeByPath( [ 1, 0 ] ); // -> "c"
  177. *
  178. * @param {Array.<Number>} relativePath Path of the node to find, relative to this element.
  179. * @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment}
  180. */
  181. getNodeByPath( relativePath ) {
  182. let node = this; // eslint-disable-line consistent-this
  183. for ( const index of relativePath ) {
  184. node = node.getChild( node.offsetToIndex( index ) );
  185. }
  186. return node;
  187. }
  188. /**
  189. * Converts offset "position" to index "position".
  190. *
  191. * Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is
  192. * too high, returns index after last child}.
  193. *
  194. * const textNode = new Text( 'foo' );
  195. * const pElement = new Element( 'p' );
  196. * const docFrag = new DocumentFragment( [ textNode, pElement ] );
  197. * docFrag.offsetToIndex( -1 ); // Returns 0, because offset is too low.
  198. * docFrag.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0.
  199. * docFrag.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too.
  200. * docFrag.offsetToIndex( 2 ); // Returns 0.
  201. * docFrag.offsetToIndex( 3 ); // Returns 1.
  202. * docFrag.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned.
  203. *
  204. * @param {Number} offset Offset to look for.
  205. * @returns {Number} Index of a node that occupies given offset.
  206. */
  207. offsetToIndex( offset ) {
  208. return this._children.offsetToIndex( offset );
  209. }
  210. /**
  211. * Converts `DocumentFragment` instance to plain object and returns it.
  212. * Takes care of converting all of this document fragment's children.
  213. *
  214. * @returns {Object} `DocumentFragment` instance converted to plain object.
  215. */
  216. toJSON() {
  217. const json = [];
  218. for ( const node of this._children ) {
  219. json.push( node.toJSON() );
  220. }
  221. return json;
  222. }
  223. /**
  224. * Creates a `DocumentFragment` instance from given plain object (i.e. parsed JSON string).
  225. * Converts `DocumentFragment` children to proper nodes.
  226. *
  227. * @param {Object} json Plain object to be converted to `DocumentFragment`.
  228. * @returns {module:engine/model/documentfragment~DocumentFragment} `DocumentFragment` instance created using given plain object.
  229. */
  230. static fromJSON( json ) {
  231. const children = [];
  232. for ( const child of json ) {
  233. if ( child.name ) {
  234. // If child has name property, it is an Element.
  235. children.push( Element.fromJSON( child ) );
  236. } else {
  237. // Otherwise, it is a Text node.
  238. children.push( Text.fromJSON( child ) );
  239. }
  240. }
  241. return new DocumentFragment( children );
  242. }
  243. /**
  244. * {@link #_insertChild Inserts} one or more nodes at the end of this document fragment.
  245. *
  246. * @protected
  247. * @param {module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>} items Items to be inserted.
  248. */
  249. _appendChild( items ) {
  250. this._insertChild( this.childCount, items );
  251. }
  252. /**
  253. * Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
  254. * to this document fragment.
  255. *
  256. * @protected
  257. * @param {Number} index Index at which nodes should be inserted.
  258. * @param {module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>} items Items to be inserted.
  259. */
  260. _insertChild( index, items ) {
  261. const nodes = normalize( items );
  262. for ( const node of nodes ) {
  263. // If node that is being added to this element is already inside another element, first remove it from the old parent.
  264. if ( node.parent !== null ) {
  265. node._remove();
  266. }
  267. node.parent = this;
  268. }
  269. this._children._insertNodes( index, nodes );
  270. }
  271. /**
  272. * Removes one or more nodes starting at the given index
  273. * and sets {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
  274. *
  275. * @protected
  276. * @param {Number} index Index of the first node to remove.
  277. * @param {Number} [howMany=1] Number of nodes to remove.
  278. * @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
  279. */
  280. _removeChildren( index, howMany = 1 ) {
  281. const nodes = this._children._removeNodes( index, howMany );
  282. for ( const node of nodes ) {
  283. node.parent = null;
  284. }
  285. return nodes;
  286. }
  287. // @if CK_DEBUG_ENGINE // toString() {
  288. // @if CK_DEBUG_ENGINE // return 'documentFragment';
  289. // @if CK_DEBUG_ENGINE // }
  290. // @if CK_DEBUG_ENGINE // log() {
  291. // @if CK_DEBUG_ENGINE // console.log( 'ModelDocumentFragment: ' + this );
  292. // @if CK_DEBUG_ENGINE // }
  293. // @if CK_DEBUG_ENGINE // printTree() {
  294. // @if CK_DEBUG_ENGINE // let string = 'ModelDocumentFragment: [';
  295. // @if CK_DEBUG_ENGINE // for ( const child of this.getChildren() ) {
  296. // @if CK_DEBUG_ENGINE // string += '\n';
  297. // @if CK_DEBUG_ENGINE // if ( child.is( '$text' ) ) {
  298. // @if CK_DEBUG_ENGINE // const textAttrs = stringifyMap( child._attrs );
  299. // @if CK_DEBUG_ENGINE // string += '\t'.repeat( 1 );
  300. // @if CK_DEBUG_ENGINE // if ( textAttrs !== '' ) {
  301. // @if CK_DEBUG_ENGINE // string += `<$text${ textAttrs }>` + child.data + '</$text>';
  302. // @if CK_DEBUG_ENGINE // } else {
  303. // @if CK_DEBUG_ENGINE // string += child.data;
  304. // @if CK_DEBUG_ENGINE // }
  305. // @if CK_DEBUG_ENGINE // } else {
  306. // @if CK_DEBUG_ENGINE // string += child.printTree( 1 );
  307. // @if CK_DEBUG_ENGINE // }
  308. // @if CK_DEBUG_ENGINE // }
  309. // @if CK_DEBUG_ENGINE // string += '\n]';
  310. // @if CK_DEBUG_ENGINE // return string;
  311. // @if CK_DEBUG_ENGINE // }
  312. // @if CK_DEBUG_ENGINE // logTree() {
  313. // @if CK_DEBUG_ENGINE // console.log( this.printTree() );
  314. // @if CK_DEBUG_ENGINE // }
  315. }
  316. // Converts strings to Text and non-iterables to arrays.
  317. //
  318. // @param {String|module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>}
  319. // @returns {Iterable.<module:engine/model/node~Node>}
  320. function normalize( nodes ) {
  321. // Separate condition because string is iterable.
  322. if ( typeof nodes == 'string' ) {
  323. return [ new Text( nodes ) ];
  324. }
  325. if ( !isIterable( nodes ) ) {
  326. nodes = [ nodes ];
  327. }
  328. // Array.from to enable .map() on non-arrays.
  329. return Array.from( nodes )
  330. .map( node => {
  331. if ( typeof node == 'string' ) {
  332. return new Text( node );
  333. }
  334. if ( node instanceof TextProxy ) {
  335. return new Text( node.data, node.getAttributes() );
  336. }
  337. return node;
  338. } );
  339. }