8
0

documentfragment.js 10 KB

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